huge refactor
This commit is contained in:
parent
e5ebe05571
commit
a8341e8232
29 changed files with 3184 additions and 340 deletions
63
src/Roboto.Memory/States/AreaInstanceState.cs
Normal file
63
src/Roboto.Memory/States/AreaInstanceState.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Reads fields from the AreaInstance (IngameData) address.
|
||||
/// Individual field reads — the full struct is 3280B, too large to bulk-read.
|
||||
/// Uses GameOffsets for configurable offsets.
|
||||
/// </summary>
|
||||
public sealed class AreaInstanceState : RemoteObject
|
||||
{
|
||||
public int AreaLevel { get; private set; }
|
||||
public uint AreaHash { get; private set; }
|
||||
public nint ServerDataPtr { get; private set; }
|
||||
public nint LocalPlayerPtr { get; private set; }
|
||||
public int EntityCount { get; private set; }
|
||||
|
||||
public AreaInstanceState(MemoryContext ctx) : base(ctx) { }
|
||||
|
||||
protected override bool ReadData()
|
||||
{
|
||||
var mem = Ctx.Memory;
|
||||
var offsets = Ctx.Offsets;
|
||||
|
||||
// Area level
|
||||
if (offsets.AreaLevelIsByte)
|
||||
{
|
||||
var level = mem.Read<byte>(Address + offsets.AreaLevelOffset);
|
||||
AreaLevel = level is > 0 and < 200 ? level : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var level = mem.Read<int>(Address + offsets.AreaLevelOffset);
|
||||
AreaLevel = level is > 0 and < 200 ? level : 0;
|
||||
}
|
||||
|
||||
// Area hash
|
||||
AreaHash = mem.Read<uint>(Address + offsets.AreaHashOffset);
|
||||
|
||||
// ServerData pointer
|
||||
ServerDataPtr = mem.ReadPointer(Address + offsets.ServerDataOffset);
|
||||
|
||||
// LocalPlayer — try direct offset first, fallback to ServerData chain
|
||||
LocalPlayerPtr = 0;
|
||||
if (offsets.LocalPlayerDirectOffset > 0)
|
||||
LocalPlayerPtr = mem.ReadPointer(Address + offsets.LocalPlayerDirectOffset);
|
||||
if (LocalPlayerPtr == 0 && ServerDataPtr != 0)
|
||||
LocalPlayerPtr = mem.ReadPointer(ServerDataPtr + offsets.LocalPlayerOffset);
|
||||
|
||||
// Entity count from std::map _Mysize
|
||||
var count = (int)mem.Read<long>(Address + offsets.EntityListOffset + offsets.EntityCountInternalOffset);
|
||||
EntityCount = count is > 0 and < 50000 ? count : 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Clear()
|
||||
{
|
||||
AreaLevel = 0;
|
||||
AreaHash = 0;
|
||||
ServerDataPtr = 0;
|
||||
LocalPlayerPtr = 0;
|
||||
EntityCount = 0;
|
||||
}
|
||||
}
|
||||
36
src/Roboto.Memory/States/AreaLoadingState.cs
Normal file
36
src/Roboto.Memory/States/AreaLoadingState.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Roboto.GameOffsets.States;
|
||||
|
||||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Reads AreaLoading state (slot 0). Individual field reads — the full struct is 3672B, wasteful to bulk-read.
|
||||
/// </summary>
|
||||
public sealed class AreaLoadingState : RemoteObject
|
||||
{
|
||||
// AreaLoading struct field offsets
|
||||
private const int IsLoadingOffset = 0x660;
|
||||
private const int TotalLoadingScreenTimeMsOffset = 0xDB8;
|
||||
|
||||
public bool IsLoading { get; private set; }
|
||||
public long TotalLoadingScreenTimeMs { get; private set; }
|
||||
|
||||
public AreaLoadingState(MemoryContext ctx) : base(ctx) { }
|
||||
|
||||
protected override bool ReadData()
|
||||
{
|
||||
var mem = Ctx.Memory;
|
||||
|
||||
var loadingFlag = mem.Read<int>(Address + IsLoadingOffset);
|
||||
IsLoading = loadingFlag != 0;
|
||||
|
||||
TotalLoadingScreenTimeMs = mem.Read<long>(Address + TotalLoadingScreenTimeMsOffset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Clear()
|
||||
{
|
||||
IsLoading = false;
|
||||
TotalLoadingScreenTimeMs = 0;
|
||||
}
|
||||
}
|
||||
22
src/Roboto.Memory/States/GameStateType.cs
Normal file
22
src/Roboto.Memory/States/GameStateType.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Game state types by slot index. Order must match the state array in the controller.
|
||||
/// Matches ExileCore/GameHelper2 slot ordering.
|
||||
/// </summary>
|
||||
public enum GameStateType
|
||||
{
|
||||
AreaLoadingState, // 0
|
||||
WaitingState, // 1
|
||||
CreditsState, // 2
|
||||
EscapeState, // 3
|
||||
InGameState, // 4
|
||||
ChangePasswordState, // 5
|
||||
LoginState, // 6
|
||||
PreGameState, // 7
|
||||
CreateCharacterState, // 8
|
||||
SelectCharacterState, // 9
|
||||
DeleteCharacterState, // 10
|
||||
LoadingState, // 11
|
||||
GameNotLoaded, // sentinel — no valid state resolved
|
||||
}
|
||||
437
src/Roboto.Memory/States/GameStates.cs
Normal file
437
src/Roboto.Memory/States/GameStates.cs
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <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 AreaLoadingState AreaLoading { get; }
|
||||
public InGameStateReader 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)
|
||||
{
|
||||
_ctx = ctx;
|
||||
AreaLoading = new AreaLoadingState(ctx);
|
||||
InGame = new InGameStateReader(ctx);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
51
src/Roboto.Memory/States/InGameStateReader.cs
Normal file
51
src/Roboto.Memory/States/InGameStateReader.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using Roboto.GameOffsets.States;
|
||||
|
||||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Reads InGameState struct (784B, 1 RPM instead of 4 individual reads).
|
||||
/// Named "Reader" to avoid collision with <see cref="Roboto.GameOffsets.States.InGameState"/> struct.
|
||||
/// Cascades to AreaInstanceState and WorldDataState children.
|
||||
/// </summary>
|
||||
public sealed class InGameStateReader : RemoteObject
|
||||
{
|
||||
private InGameState _data;
|
||||
|
||||
public bool IsEscapeOpen { get; private set; }
|
||||
public AreaInstanceState AreaInstance { get; }
|
||||
public WorldDataState WorldData { get; }
|
||||
|
||||
public InGameStateReader(MemoryContext ctx) : base(ctx)
|
||||
{
|
||||
AreaInstance = new AreaInstanceState(ctx);
|
||||
WorldData = new WorldDataState(ctx);
|
||||
}
|
||||
|
||||
protected override bool ReadData()
|
||||
{
|
||||
var mem = Ctx.Memory;
|
||||
|
||||
// Read the full InGameState struct (0x310 = 784 bytes, 1 RPM)
|
||||
_data = mem.Read<InGameState>(Address);
|
||||
|
||||
// Escape state
|
||||
IsEscapeOpen = _data.EscapeStateFlag != 0;
|
||||
|
||||
// Cascade to AreaInstance
|
||||
AreaInstance.Update(_data.AreaInstanceDataPtr);
|
||||
|
||||
// Cascade to WorldData — set fallback camera before update
|
||||
WorldData.FallbackCameraPtr = _data.CameraPtr;
|
||||
WorldData.Update(_data.WorldDataPtr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Clear()
|
||||
{
|
||||
_data = default;
|
||||
IsEscapeOpen = false;
|
||||
AreaInstance.Reset();
|
||||
WorldData.Reset();
|
||||
}
|
||||
}
|
||||
44
src/Roboto.Memory/States/RemoteObject.cs
Normal file
44
src/Roboto.Memory/States/RemoteObject.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for state objects that read a section of game memory.
|
||||
/// Each subclass reads its own struct/fields from a remote address.
|
||||
/// </summary>
|
||||
public abstract class RemoteObject
|
||||
{
|
||||
protected readonly MemoryContext Ctx;
|
||||
|
||||
public nint Address { get; protected set; }
|
||||
public bool IsValid => Address != 0;
|
||||
|
||||
protected RemoteObject(MemoryContext ctx) => Ctx = ctx;
|
||||
|
||||
/// <summary>
|
||||
/// Update this object from a new address. Returns false if address is 0 or read fails.
|
||||
/// </summary>
|
||||
public bool Update(nint address)
|
||||
{
|
||||
Address = address;
|
||||
if (address == 0)
|
||||
{
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
return ReadData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset address to 0 and clear all cached data.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Address = 0;
|
||||
Clear();
|
||||
}
|
||||
|
||||
/// <summary>Read data from the remote process at <see cref="Address"/>.</summary>
|
||||
protected abstract bool ReadData();
|
||||
|
||||
/// <summary>Zero out all cached fields.</summary>
|
||||
protected abstract void Clear();
|
||||
}
|
||||
60
src/Roboto.Memory/States/WorldDataState.cs
Normal file
60
src/Roboto.Memory/States/WorldDataState.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System.Numerics;
|
||||
using Roboto.GameOffsets.States;
|
||||
|
||||
namespace Roboto.Memory.States;
|
||||
|
||||
/// <summary>
|
||||
/// Reads WorldData struct (168B, 1 RPM) and resolves the camera matrix.
|
||||
/// Primary camera source: WorldData.CameraPtr. Fallback: InGameState.CameraPtr (set via FallbackCameraPtr).
|
||||
/// </summary>
|
||||
public sealed class WorldDataState : RemoteObject
|
||||
{
|
||||
private WorldData _data;
|
||||
|
||||
/// <summary>Camera pointer from InGameState, set by InGameStateReader before Update() is called.</summary>
|
||||
public nint FallbackCameraPtr { get; set; }
|
||||
|
||||
public Matrix4x4? CameraMatrix { get; private set; }
|
||||
|
||||
/// <summary>Resolved address of the camera matrix for hot-path caching.</summary>
|
||||
public nint CameraMatrixAddress { get; private set; }
|
||||
|
||||
public WorldDataState(MemoryContext ctx) : base(ctx) { }
|
||||
|
||||
protected override bool ReadData()
|
||||
{
|
||||
var mem = Ctx.Memory;
|
||||
var offsets = Ctx.Offsets;
|
||||
|
||||
// Read the full WorldData struct (0xA8 = 168 bytes, 1 RPM)
|
||||
_data = mem.Read<WorldData>(Address);
|
||||
|
||||
// Resolve camera: primary from WorldData, fallback from InGameState
|
||||
if (offsets.CameraMatrixOffset <= 0)
|
||||
return true;
|
||||
|
||||
var camPtr = _data.CameraPtr;
|
||||
if (camPtr == 0)
|
||||
camPtr = FallbackCameraPtr;
|
||||
if (camPtr == 0)
|
||||
return true;
|
||||
|
||||
var matrixAddr = camPtr + offsets.CameraMatrixOffset;
|
||||
CameraMatrixAddress = matrixAddr;
|
||||
|
||||
var m = mem.Read<Matrix4x4>(matrixAddr);
|
||||
if (float.IsNaN(m.M11) || float.IsInfinity(m.M11))
|
||||
return true;
|
||||
|
||||
CameraMatrix = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Clear()
|
||||
{
|
||||
_data = default;
|
||||
FallbackCameraPtr = 0;
|
||||
CameraMatrix = null;
|
||||
CameraMatrixAddress = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue