cleanup
This commit is contained in:
parent
8a0e4bb481
commit
0df70abad7
24 changed files with 0 additions and 1225 deletions
437
src/Roboto.Memory/Objects/GameStates.cs
Normal file
437
src/Roboto.Memory/Objects/GameStates.cs
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Root state orchestrator. Reads controller from GameStateBase, resolves state slot pointers,
|
||||
/// builds address→GameStateType dictionary, resolves current state, and cascades to children.
|
||||
/// NOT a RemoteObject — owns the top-level resolution logic.
|
||||
/// </summary>
|
||||
public sealed class GameStates
|
||||
{
|
||||
private readonly MemoryContext _ctx;
|
||||
private readonly Dictionary<nint, GameStateType> _allStates = new();
|
||||
private nint[] _slotPointers = [];
|
||||
private nint[] _prevPreSlotValues = [];
|
||||
|
||||
public nint ControllerPtr { get; private set; }
|
||||
public int StatesCount { get; private set; }
|
||||
public GameStateType CurrentState { get; private set; } = GameStateType.GameNotLoaded;
|
||||
public IReadOnlyDictionary<nint, GameStateType> AllStates => _allStates;
|
||||
public AreaLoadingState AreaLoading { get; }
|
||||
public InGameStateReader InGame { get; }
|
||||
|
||||
/// <summary>Raw qwords from controller 0x00-0x48 (before state slots), for UI diagnostics.</summary>
|
||||
public (int Offset, nint Value, string? Match, bool Changed, string? DerefInfo)[] ControllerPreSlots { get; private set; } = [];
|
||||
|
||||
public GameStates(MemoryContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
AreaLoading = new AreaLoadingState(ctx);
|
||||
InGame = new InGameStateReader(ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads controller, resolves state slots, builds state dictionary, cascades to children,
|
||||
/// then resolves current state from child flags. Returns true if InGameState was resolved.
|
||||
/// </summary>
|
||||
public bool Update()
|
||||
{
|
||||
ControllerPtr = 0;
|
||||
StatesCount = 0;
|
||||
_allStates.Clear();
|
||||
CurrentState = GameStateType.GameNotLoaded;
|
||||
|
||||
var mem = _ctx.Memory;
|
||||
var offsets = _ctx.Offsets;
|
||||
|
||||
if (_ctx.GameStateBase == 0)
|
||||
return false;
|
||||
|
||||
var controller = mem.ReadPointer(_ctx.GameStateBase);
|
||||
if (controller == 0)
|
||||
return false;
|
||||
ControllerPtr = controller;
|
||||
|
||||
nint igsPtr = 0;
|
||||
|
||||
// Mode 1: Direct offset — InGameState pointer at a known offset from controller
|
||||
if (offsets.InGameStateDirectOffset > 0)
|
||||
{
|
||||
igsPtr = mem.ReadPointer(controller + offsets.InGameStateDirectOffset);
|
||||
if (igsPtr != 0)
|
||||
ReadSlotPointers(controller);
|
||||
}
|
||||
|
||||
// Mode 2: Inline states — states are inline in the controller struct
|
||||
if (igsPtr == 0 && offsets.StatesInline)
|
||||
{
|
||||
ReadSlotPointers(controller);
|
||||
|
||||
var inlineOffset = offsets.StatesBeginOffset
|
||||
+ offsets.InGameStateIndex * offsets.StateStride
|
||||
+ offsets.StatePointerOffset;
|
||||
igsPtr = mem.ReadPointer(controller + inlineOffset);
|
||||
}
|
||||
|
||||
// Mode 3: Vector of pointers — StatesBeginOffset points to begin/end pair
|
||||
if (igsPtr == 0 && !offsets.StatesInline)
|
||||
{
|
||||
var statesBegin = mem.ReadPointer(controller + offsets.StatesBeginOffset);
|
||||
if (statesBegin == 0)
|
||||
return false;
|
||||
|
||||
var statesEnd = mem.ReadPointer(controller + offsets.StatesBeginOffset + 8);
|
||||
if (statesEnd > statesBegin && statesEnd - statesBegin < 0x1000 && offsets.StateStride > 0)
|
||||
StatesCount = (int)((statesEnd - statesBegin) / offsets.StateStride);
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
if (mem.ReadPointer(statesBegin + i * offsets.StateStride + offsets.StatePointerOffset) == 0) break;
|
||||
StatesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (_slotPointers.Length < StatesCount)
|
||||
_slotPointers = new nint[StatesCount];
|
||||
for (var i = 0; i < StatesCount; i++)
|
||||
_slotPointers[i] = mem.ReadPointer(statesBegin + i * offsets.StateStride + offsets.StatePointerOffset);
|
||||
|
||||
BuildStateDictionary();
|
||||
|
||||
if (offsets.InGameStateIndex >= 0 && offsets.InGameStateIndex < StatesCount)
|
||||
igsPtr = _slotPointers[offsets.InGameStateIndex];
|
||||
}
|
||||
|
||||
// Dump controller pre-slots region for diagnostics
|
||||
DumpControllerPreSlots(controller);
|
||||
|
||||
// Cascade to children FIRST — we need their flags for current state resolution
|
||||
var areaLoadingPtr = StatesCount > 0 ? _slotPointers[0] : (nint)0;
|
||||
AreaLoading.Update(areaLoadingPtr);
|
||||
|
||||
if (igsPtr == 0)
|
||||
{
|
||||
InGame.Reset();
|
||||
ResolveCurrentState(controller, igsPtr);
|
||||
return false;
|
||||
}
|
||||
|
||||
InGame.Update(igsPtr);
|
||||
|
||||
// Resolve current state AFTER children have read their flags
|
||||
ResolveCurrentState(controller, igsPtr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read slot pointers from inline state array in the controller.
|
||||
/// </summary>
|
||||
private void ReadSlotPointers(nint controller)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
var offsets = _ctx.Offsets;
|
||||
|
||||
StatesCount = 0;
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var slotOffset = offsets.StatesBeginOffset + i * offsets.StateStride + offsets.StatePointerOffset;
|
||||
var ptr = mem.ReadPointer(controller + slotOffset);
|
||||
if (ptr == 0) break;
|
||||
StatesCount++;
|
||||
}
|
||||
|
||||
if (_slotPointers.Length < StatesCount)
|
||||
_slotPointers = new nint[StatesCount];
|
||||
for (var i = 0; i < StatesCount; i++)
|
||||
{
|
||||
var slotOffset = offsets.StatesBeginOffset + i * offsets.StateStride + offsets.StatePointerOffset;
|
||||
_slotPointers[i] = mem.ReadPointer(controller + slotOffset);
|
||||
}
|
||||
|
||||
BuildStateDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dump the controller region before the state slots for UI diagnostics.
|
||||
/// Tracks which values changed since last frame.
|
||||
/// Dereferences pointer-like values and checks for indirect state slot matches.
|
||||
/// </summary>
|
||||
private void DumpControllerPreSlots(nint controller)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
var offsets = _ctx.Offsets;
|
||||
var preSize = offsets.StatesBeginOffset;
|
||||
if (preSize <= 0) { ControllerPreSlots = []; return; }
|
||||
|
||||
var data = mem.ReadBytes(controller, preSize);
|
||||
if (data is null) { ControllerPreSlots = []; return; }
|
||||
|
||||
var count = preSize / 8;
|
||||
var entries = new (int, nint, string?, bool, string?)[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var off = i * 8;
|
||||
var val = (nint)BitConverter.ToInt64(data, off);
|
||||
string? match = null;
|
||||
if (val != 0 && _allStates.TryGetValue(val, out var st))
|
||||
match = st.ToString();
|
||||
var changed = i < _prevPreSlotValues.Length && _prevPreSlotValues[i] != val;
|
||||
|
||||
// Annotate discovered StdVector fields
|
||||
string? derefInfo = null;
|
||||
if (_discoveredVectorOffset >= 0)
|
||||
{
|
||||
if (off == _discoveredVectorOffset)
|
||||
derefInfo = "vec.First";
|
||||
else if (off == _discoveredVectorOffset + 8)
|
||||
derefInfo = "vec.Last";
|
||||
else if (off == _discoveredVectorOffset + 16)
|
||||
derefInfo = "vec.End";
|
||||
}
|
||||
|
||||
// Dereference pointer-like values and scan for state slot matches
|
||||
if (derefInfo == null && val != 0 && match == null)
|
||||
{
|
||||
var high = (ulong)val >> 32;
|
||||
if (high > 0 && high < 0x7FFF)
|
||||
{
|
||||
// Read first 16 qwords (128 bytes) of the target object
|
||||
var targetData = mem.ReadBytes(val, 128);
|
||||
if (targetData is not null)
|
||||
{
|
||||
for (var qi = 0; qi + 8 <= targetData.Length; qi += 8)
|
||||
{
|
||||
var innerVal = (nint)BitConverter.ToInt64(targetData, qi);
|
||||
if (innerVal != 0 && _allStates.TryGetValue(innerVal, out var innerState))
|
||||
{
|
||||
derefInfo = $"*+0x{qi:X2} → {innerState}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no state match, show first qword as context
|
||||
if (derefInfo == null)
|
||||
{
|
||||
var first = (nint)BitConverter.ToInt64(targetData, 0);
|
||||
if (first != 0)
|
||||
derefInfo = $"*→ 0x{first:X}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries[i] = (off, val, match, changed, derefInfo);
|
||||
}
|
||||
ControllerPreSlots = entries;
|
||||
|
||||
// Save for next frame diff
|
||||
if (_prevPreSlotValues.Length != count)
|
||||
_prevPreSlotValues = new nint[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
_prevPreSlotValues[i] = entries[i].Item2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build address→GameStateType dictionary from collected slot pointers.
|
||||
/// </summary>
|
||||
private void BuildStateDictionary()
|
||||
{
|
||||
_allStates.Clear();
|
||||
var maxType = (int)GameStateType.GameNotLoaded;
|
||||
for (var i = 0; i < StatesCount && i < maxType; i++)
|
||||
{
|
||||
if (_slotPointers[i] != 0)
|
||||
_allStates[_slotPointers[i]] = (GameStateType)i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached base address and offset for the active states vector.
|
||||
/// -1 = not yet scanned, -2 = scan failed.
|
||||
/// </summary>
|
||||
private int _discoveredVectorOffset = -1;
|
||||
private nint _discoveredVectorBase;
|
||||
|
||||
/// <summary>
|
||||
/// Resolve which GameStateType is currently active using the GameHelper2 approach:
|
||||
/// Find the StdVector {First, Last, End}, read *(Last - 0x10) (second-to-last entry).
|
||||
/// Searches BOTH the GameState object (*(originalPatternResult)) AND the controller,
|
||||
/// since POE2 may have separated these into different objects.
|
||||
/// </summary>
|
||||
private void ResolveCurrentState(nint controller, nint igsPtr)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
var offsets = _ctx.Offsets;
|
||||
|
||||
// Fast path: use cached vector location
|
||||
if (_discoveredVectorOffset >= 0 && _discoveredVectorBase != 0)
|
||||
{
|
||||
var state = ReadStateFromVector(_discoveredVectorBase, _discoveredVectorOffset);
|
||||
if (state != GameStateType.GameNotLoaded)
|
||||
{
|
||||
CurrentState = state;
|
||||
return;
|
||||
}
|
||||
// Vector went invalid (zone change, etc) — re-scan
|
||||
_discoveredVectorOffset = -1;
|
||||
_discoveredVectorBase = 0;
|
||||
}
|
||||
|
||||
// Scan for the active states StdVector
|
||||
if (_discoveredVectorOffset == -1 && _allStates.Count > 0)
|
||||
{
|
||||
// === Priority 1: GameState object (original pattern result, before PatternResultAdjust) ===
|
||||
// GameHelper2 reads CurrentStatePtr from GameState+0x08. In POE2, the controller
|
||||
// is at pattern_result+0x18, but the "real" GameState object may be at *(pattern_result+0x00).
|
||||
if (offsets.PatternResultAdjust > 0)
|
||||
{
|
||||
var originalPatternResult = _ctx.GameStateBase - offsets.PatternResultAdjust;
|
||||
|
||||
// Try each pointer in the static region as a potential GameState object
|
||||
for (var ptrOff = 0; ptrOff < offsets.PatternResultAdjust; ptrOff += 8)
|
||||
{
|
||||
var gameStateObj = mem.ReadPointer(originalPatternResult + ptrOff);
|
||||
if (gameStateObj == 0 || gameStateObj == controller) continue;
|
||||
|
||||
// Scan this object for StdVector containing state slot pointers
|
||||
for (var off = 0; off + 24 <= 0x100; off += 8)
|
||||
{
|
||||
var state = ReadStateFromVector(gameStateObj, off);
|
||||
if (state != GameStateType.GameNotLoaded)
|
||||
{
|
||||
_discoveredVectorBase = gameStateObj;
|
||||
_discoveredVectorOffset = off;
|
||||
CurrentState = state;
|
||||
Serilog.Log.Information(
|
||||
"Active states vector at GameState(patResult+0x{PtrOff:X})+0x{Offset:X} → {State} (obj=0x{Obj:X})",
|
||||
ptrOff, off, state, gameStateObj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try reading the vector directly from the static region itself
|
||||
for (var off = 0; off + 24 <= offsets.PatternResultAdjust + 0x40; off += 8)
|
||||
{
|
||||
var state = ReadStateFromVector(originalPatternResult, off);
|
||||
if (state != GameStateType.GameNotLoaded)
|
||||
{
|
||||
_discoveredVectorBase = originalPatternResult;
|
||||
_discoveredVectorOffset = off;
|
||||
CurrentState = state;
|
||||
Serilog.Log.Information(
|
||||
"Active states vector at staticRegion+0x{Offset:X} → {State}",
|
||||
off, state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Priority 2: Controller pre-slots region ===
|
||||
var stateArrayStart = offsets.StatesBeginOffset;
|
||||
if (offsets.ActiveStatesOffset > 0)
|
||||
{
|
||||
var state = ReadStateFromVector(controller, offsets.ActiveStatesOffset);
|
||||
if (state != GameStateType.GameNotLoaded)
|
||||
{
|
||||
_discoveredVectorBase = controller;
|
||||
_discoveredVectorOffset = offsets.ActiveStatesOffset;
|
||||
CurrentState = state;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (var off = 0; off + 24 <= stateArrayStart; off += 8)
|
||||
{
|
||||
if (off == offsets.ActiveStatesOffset) continue;
|
||||
var state = ReadStateFromVector(controller, off);
|
||||
if (state != GameStateType.GameNotLoaded)
|
||||
{
|
||||
_discoveredVectorBase = controller;
|
||||
_discoveredVectorOffset = off;
|
||||
CurrentState = state;
|
||||
Serilog.Log.Information(
|
||||
"Active states vector at controller+0x{Offset:X} → {State}", off, state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark scan as failed so we don't re-scan every frame
|
||||
_discoveredVectorOffset = -2;
|
||||
}
|
||||
|
||||
// Fallback: IsLoadingOffset (legacy, if configured)
|
||||
if (offsets.IsLoadingOffset > 0)
|
||||
{
|
||||
var currentStateAddr = mem.ReadPointer(controller + offsets.IsLoadingOffset);
|
||||
if (currentStateAddr != 0 && _allStates.TryGetValue(currentStateAddr, out var stateType))
|
||||
{
|
||||
CurrentState = stateType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: default to InGameState if resolved (preliminary — GameMemoryReader
|
||||
// will reconcile with reliable snap.IsLoading / snap.IsEscapeOpen afterwards)
|
||||
if (igsPtr != 0)
|
||||
CurrentState = GameStateType.InGameState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to read the current state from a StdVector at the given controller offset.
|
||||
/// GameHelper2 approach: StdVector = {First, Last, End}, current = *(Last - 0x10).
|
||||
/// Returns GameNotLoaded if the vector is invalid or doesn't contain state matches.
|
||||
/// </summary>
|
||||
private GameStateType ReadStateFromVector(nint controller, int vectorOffset)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
var first = mem.ReadPointer(controller + vectorOffset);
|
||||
var last = mem.ReadPointer(controller + vectorOffset + 8);
|
||||
|
||||
if (first == 0 || last <= first) return GameStateType.GameNotLoaded;
|
||||
|
||||
var size = (int)(last - first);
|
||||
if (size < 16 || size > 0x400) return GameStateType.GameNotLoaded; // need ≥2 entries for 2nd-to-last
|
||||
|
||||
// Read the full vector buffer
|
||||
var buf = mem.ReadBytes(first, size);
|
||||
if (buf is null) return GameStateType.GameNotLoaded;
|
||||
|
||||
// Validate: at least 1 entry must be a known state slot address
|
||||
var matchCount = 0;
|
||||
for (var i = 0; i + 8 <= buf.Length; i += 8)
|
||||
{
|
||||
var val = (nint)BitConverter.ToInt64(buf, i);
|
||||
if (val != 0 && _allStates.ContainsKey(val))
|
||||
matchCount++;
|
||||
}
|
||||
if (matchCount == 0) return GameStateType.GameNotLoaded;
|
||||
|
||||
// GameHelper2: current state = *(Last - 0x10) = second-to-last entry
|
||||
var secondToLast = (nint)BitConverter.ToInt64(buf, buf.Length - 16);
|
||||
if (secondToLast != 0 && _allStates.TryGetValue(secondToLast, out var stateType))
|
||||
return stateType;
|
||||
|
||||
// Fallback: try last entry if second-to-last didn't match
|
||||
var lastEntry = (nint)BitConverter.ToInt64(buf, buf.Length - 8);
|
||||
if (lastEntry != 0 && _allStates.TryGetValue(lastEntry, out stateType))
|
||||
return stateType;
|
||||
|
||||
return GameStateType.GameNotLoaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset all state to zero.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
ControllerPtr = 0;
|
||||
StatesCount = 0;
|
||||
CurrentState = GameStateType.GameNotLoaded;
|
||||
_discoveredVectorOffset = -1;
|
||||
_discoveredVectorBase = 0;
|
||||
_allStates.Clear();
|
||||
AreaLoading.Reset();
|
||||
InGame.Reset();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue