using Serilog; namespace Roboto.Memory; /// /// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state. /// public sealed class GameStateReader { private readonly MemoryContext _ctx; public GameStateReader(MemoryContext ctx) { _ctx = ctx; } /// /// Resolves InGameState pointer from the GameState controller. /// public nint ResolveInGameState(GameStateSnapshot snap) { var mem = _ctx.Memory; var offsets = _ctx.Offsets; var controller = mem.ReadPointer(_ctx.GameStateBase); if (controller == 0) return 0; snap.ControllerPtr = controller; // Direct offset mode: read InGameState straight from controller if (offsets.InGameStateDirectOffset > 0) { var igs = mem.ReadPointer(controller + offsets.InGameStateDirectOffset); if (igs != 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; snap.StatesCount++; } return igs; } } if (offsets.StatesInline) { var inlineOffset = offsets.StatesBeginOffset + offsets.InGameStateIndex * offsets.StateStride + offsets.StatePointerOffset; 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; snap.StatesCount++; } return mem.ReadPointer(controller + inlineOffset); } else { var statesBegin = mem.ReadPointer(controller + offsets.StatesBeginOffset); if (statesBegin == 0) return 0; var statesEnd = mem.ReadPointer(controller + offsets.StatesBeginOffset + 8); if (statesEnd > statesBegin && statesEnd - statesBegin < 0x1000 && offsets.StateStride > 0) { snap.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; snap.StatesCount++; } } if (offsets.InGameStateIndex < 0 || offsets.InGameStateIndex >= snap.StatesCount) return 0; return mem.ReadPointer(statesBegin + offsets.InGameStateIndex * offsets.StateStride + offsets.StatePointerOffset); } } /// /// Reads all state slot pointers and active states vector from the controller. /// public void ReadStateSlots(GameStateSnapshot snap) { var controller = snap.ControllerPtr; if (controller == 0) return; var mem = _ctx.Memory; var offsets = _ctx.Offsets; var count = offsets.StateCount; var slots = new nint[count]; for (var i = 0; i < count; i++) { var slotOffset = offsets.StatesBeginOffset + i * offsets.StateStride + offsets.StatePointerOffset; slots[i] = mem.ReadPointer(controller + slotOffset); } snap.StateSlots = slots; var values = new int[count]; for (var i = 0; i < count; i++) { if (slots[i] != 0) values[i] = mem.Read(slots[i] + 0x08); } snap.StateSlotValues = values; // Read active states vector — scan controller for {begin, end} pairs // containing known state slot pointers (auto-discovers layout) if (offsets.ActiveStatesOffset > 0) { // Collect known state slot pointers for matching var knownSlots = new HashSet(); foreach (var s in slots) if (s != 0) knownSlots.Add(s); // Try the configured offset with end at both +8 and +16 var beginPtr = mem.ReadPointer(controller + offsets.ActiveStatesOffset); snap.ActiveStatesBegin = beginPtr; nint endPtr = 0; foreach (var endDelta in new[] { 8, 16 }) { var candidate = mem.ReadPointer(controller + offsets.ActiveStatesOffset + endDelta); if (candidate > beginPtr && candidate - beginPtr < 0x1000) { endPtr = candidate; break; } } snap.ActiveStatesEnd = endPtr; if (beginPtr != 0 && endPtr > beginPtr) { var size = (int)(endPtr - beginPtr); var data = mem.ReadBytes(beginPtr, size); if (data is not null) { var rawList = new List(); for (var i = 0; i + 8 <= data.Length; i += 8) { var ptr = (nint)BitConverter.ToInt64(data, i); rawList.Add(ptr); if (ptr != 0) snap.ActiveStates.Add(ptr); } snap.ActiveStatesRaw = rawList.ToArray(); } } } // Read all non-null pointer-like qwords from controller (outside state array) var stateArrayStart = offsets.StatesBeginOffset; var stateArrayEnd = stateArrayStart + count * offsets.StateStride; var watches = new List<(int, nint)>(); var ctrlData = mem.ReadBytes(controller, 0x350); if (ctrlData is not null) { for (var offset = 0; offset + 8 <= ctrlData.Length; offset += 8) { if (offset >= stateArrayStart && offset < stateArrayEnd) continue; var value = (nint)BitConverter.ToInt64(ctrlData, offset); if (value == 0) continue; var high = (ulong)value >> 32; if (high > 0 && high < 0x7FFF && (value & 0x3) == 0) watches.Add((offset, value)); } } snap.WatchOffsets = watches.ToArray(); } /// /// Detects loading by comparing the active state pointer to InGameStatePtr. /// public void ReadIsLoading(GameStateSnapshot snap) { var controller = snap.ControllerPtr; if (controller == 0 || _ctx.Offsets.IsLoadingOffset <= 0) return; var value = _ctx.Memory.ReadPointer(controller + _ctx.Offsets.IsLoadingOffset); if (value == snap.InGameStatePtr && snap.InGameStatePtr != 0) snap.IsLoading = false; else if (value == 0) snap.IsLoading = false; else snap.IsLoading = true; } /// /// Reads escape menu state from active states vector or InGameState flag. /// public void ReadEscapeState(GameStateSnapshot snap) { if (snap.ActiveStates.Count > 0 && snap.StateSlots.Length > 3 && snap.StateSlots[3] != 0) { snap.IsEscapeOpen = snap.ActiveStates.Contains(snap.StateSlots[3]); return; } if (snap.InGameStatePtr == 0 || _ctx.Offsets.EscapeStateOffset <= 0) return; var value = _ctx.Memory.Read(snap.InGameStatePtr + _ctx.Offsets.EscapeStateOffset); snap.IsEscapeOpen = value != 0; } }