using Roboto.Memory; namespace Roboto.Memory.Objects; /// /// 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. /// public sealed class GameStates { private readonly MemoryContext _ctx; private readonly Dictionary _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 AllStates => _allStates; public AreaLoading AreaLoading { get; } public InGameState InGame { get; } /// Raw qwords from controller 0x00-0x48 (before state slots), for UI diagnostics. 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); } /// /// Reads controller, resolves state slots, builds state dictionary, cascades to children, /// then resolves current state from child flags. Returns true if InGameState was resolved. /// 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; } /// /// Read slot pointers from inline state array in the controller. /// 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(); } /// /// 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. /// 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; } /// /// Build address→GameStateType dictionary from collected slot pointers. /// 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; } } /// /// Cached base address and offset for the active states vector. /// -1 = not yet scanned, -2 = scan failed. /// private int _discoveredVectorOffset = -1; private nint _discoveredVectorBase; /// /// 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. /// 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; } /// /// 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. /// 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; } /// /// Reset all state to zero. /// public void Reset() { ControllerPtr = 0; StatesCount = 0; CurrentState = GameStateType.GameNotLoaded; _discoveredVectorOffset = -1; _discoveredVectorBase = 0; _allStates.Clear(); AreaLoading.Reset(); InGame.Reset(); } }