439 lines
17 KiB
C#
439 lines
17 KiB
C#
using Roboto.Memory;
|
|
|
|
namespace Roboto.Memory.Objects;
|
|
|
|
/// <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 AreaLoading AreaLoading { get; }
|
|
public InGameState 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, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
|
|
{
|
|
_ctx = ctx;
|
|
AreaLoading = new AreaLoading(ctx);
|
|
InGame = new InGameState(ctx, components, strings, questNames);
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
}
|