217 lines
7.6 KiB
C#
217 lines
7.6 KiB
C#
using Serilog;
|
|
|
|
namespace Roboto.Memory;
|
|
|
|
/// <summary>
|
|
/// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state.
|
|
/// </summary>
|
|
public sealed class GameStateReader
|
|
{
|
|
private readonly MemoryContext _ctx;
|
|
|
|
public GameStateReader(MemoryContext ctx)
|
|
{
|
|
_ctx = ctx;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves InGameState pointer from the GameState controller.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads all state slot pointers and active states vector from the controller.
|
|
/// </summary>
|
|
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<int>(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<nint>();
|
|
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<nint>();
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects loading by comparing the active state pointer to InGameStatePtr.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads escape menu state from active states vector or InGameState flag.
|
|
/// </summary>
|
|
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<int>(snap.InGameStatePtr + _ctx.Offsets.EscapeStateOffset);
|
|
snap.IsEscapeOpen = value != 0;
|
|
}
|
|
}
|