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;
}
}