313 lines
10 KiB
C#
313 lines
10 KiB
C#
using System.Numerics;
|
|
using Serilog;
|
|
|
|
namespace Roboto.Memory;
|
|
|
|
public class GameMemoryReader : IDisposable
|
|
{
|
|
// ExileCore state slot names (index → name)
|
|
public static readonly string[] StateNames =
|
|
[
|
|
"AreaLoading", // 0
|
|
"Waiting", // 1
|
|
"Credits", // 2
|
|
"Escape", // 3
|
|
"InGame", // 4
|
|
"ChangePassword", // 5
|
|
"Login", // 6
|
|
"PreGame", // 7
|
|
"CreateChar", // 8
|
|
"SelectChar", // 9
|
|
"DeleteChar", // 10
|
|
"Loading", // 11
|
|
];
|
|
|
|
private readonly GameOffsets _offsets;
|
|
private readonly ObjectRegistry _registry;
|
|
private bool _disposed;
|
|
|
|
// Sub-readers (created on Attach)
|
|
private MemoryContext? _ctx;
|
|
private GameStateReader? _stateReader;
|
|
private nint _cachedCameraMatrixAddr;
|
|
private nint _lastInGameState;
|
|
private nint _lastController;
|
|
private ComponentReader? _components;
|
|
private EntityReader? _entities;
|
|
private TerrainReader? _terrain;
|
|
private MsvcStringReader? _strings;
|
|
private RttiResolver? _rtti;
|
|
|
|
public ObjectRegistry Registry => _registry;
|
|
public MemoryDiagnostics? Diagnostics { get; private set; }
|
|
public MemoryContext? Context => _ctx;
|
|
public ComponentReader? Components => _components;
|
|
public GameStateReader? StateReader => _stateReader;
|
|
|
|
public GameMemoryReader()
|
|
{
|
|
_offsets = GameOffsets.Load("offsets.json");
|
|
_registry = new ObjectRegistry();
|
|
}
|
|
|
|
public bool IsAttached => _ctx != null;
|
|
|
|
public bool Attach()
|
|
{
|
|
Detach();
|
|
var memory = ProcessMemory.Attach(_offsets.ProcessName);
|
|
if (memory is null)
|
|
return false;
|
|
|
|
_ctx = new MemoryContext(memory, _offsets, _registry);
|
|
|
|
var module = memory.GetMainModule();
|
|
if (module is not null)
|
|
{
|
|
_ctx.ModuleBase = module.Value.Base;
|
|
_ctx.ModuleSize = module.Value.Size;
|
|
}
|
|
|
|
// Try pattern scan first
|
|
if (!string.IsNullOrWhiteSpace(_offsets.GameStatePattern))
|
|
{
|
|
var scanner = new PatternScanner(memory);
|
|
_ctx.GameStateBase = scanner.FindPatternRip(_offsets.GameStatePattern);
|
|
if (_ctx.GameStateBase != 0)
|
|
{
|
|
_ctx.GameStateBase += _offsets.PatternResultAdjust;
|
|
Log.Information("GameState base (pattern+adjust): 0x{Address:X}", _ctx.GameStateBase);
|
|
}
|
|
}
|
|
|
|
// Fallback: manual offset from module base
|
|
if (_ctx.GameStateBase == 0 && _offsets.GameStateGlobalOffset > 0)
|
|
{
|
|
_ctx.GameStateBase = _ctx.ModuleBase + _offsets.GameStateGlobalOffset;
|
|
Log.Information("GameState base (manual): 0x{Address:X}", _ctx.GameStateBase);
|
|
}
|
|
|
|
// Create sub-readers
|
|
_strings = new MsvcStringReader(_ctx);
|
|
_rtti = new RttiResolver(_ctx);
|
|
_stateReader = new GameStateReader(_ctx);
|
|
_components = new ComponentReader(_ctx, _strings);
|
|
_entities = new EntityReader(_ctx, _components, _strings);
|
|
_terrain = new TerrainReader(_ctx);
|
|
Diagnostics = new MemoryDiagnostics(_ctx, _stateReader, _components, _entities, _strings, _rtti);
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Detach()
|
|
{
|
|
_ctx?.Memory.Dispose();
|
|
_ctx = null;
|
|
_stateReader = null;
|
|
_components = null;
|
|
_entities = null;
|
|
_terrain = null;
|
|
_strings = null;
|
|
_rtti = null;
|
|
Diagnostics = null;
|
|
}
|
|
|
|
public GameStateSnapshot ReadSnapshot()
|
|
{
|
|
var snap = new GameStateSnapshot();
|
|
|
|
if (_ctx is null)
|
|
{
|
|
snap.Error = "Not attached";
|
|
return snap;
|
|
}
|
|
|
|
var mem = _ctx.Memory;
|
|
var offsets = _ctx.Offsets;
|
|
|
|
snap.Attached = true;
|
|
snap.ProcessId = mem.ProcessId;
|
|
snap.ModuleBase = _ctx.ModuleBase;
|
|
snap.ModuleSize = _ctx.ModuleSize;
|
|
snap.OffsetsConfigured = _ctx.GameStateBase != 0;
|
|
snap.GameStateBase = _ctx.GameStateBase;
|
|
|
|
if (_ctx.GameStateBase == 0)
|
|
return snap;
|
|
|
|
// Static area level — direct module offset, always reliable
|
|
if (offsets.AreaLevelStaticOffset > 0 && _ctx.ModuleBase != 0)
|
|
{
|
|
var level = mem.Read<int>(_ctx.ModuleBase + offsets.AreaLevelStaticOffset);
|
|
if (level > 0 && level < 200)
|
|
snap.AreaLevel = level;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Resolve InGameState from controller
|
|
var inGameState = _stateReader!.ResolveInGameState(snap);
|
|
if (inGameState == 0)
|
|
return snap;
|
|
snap.InGameStatePtr = inGameState;
|
|
_lastInGameState = inGameState;
|
|
_lastController = snap.ControllerPtr;
|
|
|
|
// Read all state slot pointers
|
|
_stateReader.ReadStateSlots(snap);
|
|
|
|
// InGameState → AreaInstance
|
|
var ingameData = mem.ReadPointer(inGameState + offsets.IngameDataFromStateOffset);
|
|
snap.AreaInstancePtr = ingameData;
|
|
|
|
if (ingameData != 0)
|
|
{
|
|
// Area level
|
|
if (offsets.AreaLevelIsByte)
|
|
{
|
|
var level = mem.Read<byte>(ingameData + offsets.AreaLevelOffset);
|
|
if (level > 0 && level < 200)
|
|
snap.AreaLevel = level;
|
|
}
|
|
else
|
|
{
|
|
var level = mem.Read<int>(ingameData + offsets.AreaLevelOffset);
|
|
if (level > 0 && level < 200)
|
|
snap.AreaLevel = level;
|
|
}
|
|
|
|
// Area hash
|
|
snap.AreaHash = mem.Read<uint>(ingameData + offsets.AreaHashOffset);
|
|
|
|
// ServerData pointer
|
|
var serverData = mem.ReadPointer(ingameData + offsets.ServerDataOffset);
|
|
snap.ServerDataPtr = serverData;
|
|
|
|
// LocalPlayer — try direct offset first, fallback to ServerData chain
|
|
if (offsets.LocalPlayerDirectOffset > 0)
|
|
snap.LocalPlayerPtr = mem.ReadPointer(ingameData + offsets.LocalPlayerDirectOffset);
|
|
if (snap.LocalPlayerPtr == 0 && serverData != 0)
|
|
snap.LocalPlayerPtr = mem.ReadPointer(serverData + offsets.LocalPlayerOffset);
|
|
|
|
// Entity count and list
|
|
var entityCount = (int)mem.Read<long>(ingameData + offsets.EntityListOffset + offsets.EntityCountInternalOffset);
|
|
if (entityCount > 0 && entityCount < 50000)
|
|
{
|
|
snap.EntityCount = entityCount;
|
|
_entities!.ReadEntities(snap, ingameData);
|
|
}
|
|
|
|
// Player vitals & position — ECS
|
|
if (snap.LocalPlayerPtr != 0)
|
|
{
|
|
// Invalidate caches if LocalPlayer entity changed (zone change)
|
|
if (snap.LocalPlayerPtr != _components!.LastLocalPlayer)
|
|
_terrain!.InvalidateCache();
|
|
_components.InvalidateCaches(snap.LocalPlayerPtr);
|
|
_components.ReadPlayerVitals(snap);
|
|
_components.ReadPlayerPosition(snap);
|
|
}
|
|
|
|
// Camera matrix
|
|
ReadCameraMatrix(snap, inGameState);
|
|
|
|
// Loading and escape state
|
|
_stateReader.ReadIsLoading(snap);
|
|
_stateReader.ReadEscapeState(snap);
|
|
|
|
// Read state flag bytes
|
|
if (snap.InGameStatePtr != 0)
|
|
snap.StateFlagBytes = mem.ReadBytes(snap.InGameStatePtr + snap.StateFlagBaseOffset, 0x30);
|
|
|
|
// Terrain
|
|
_terrain!.ReadTerrain(snap, ingameData);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Debug(ex, "Error reading snapshot");
|
|
}
|
|
|
|
// Update edge detection for next tick
|
|
_terrain!.UpdateLoadingEdge(snap.IsLoading);
|
|
|
|
return snap;
|
|
}
|
|
|
|
private void ReadCameraMatrix(GameStateSnapshot snap, nint inGameState)
|
|
{
|
|
var mem = _ctx!.Memory;
|
|
var offsets = _ctx.Offsets;
|
|
|
|
if (offsets.CameraMatrixOffset <= 0) return;
|
|
|
|
// If CameraOffset > 0: follow pointer from InGameState, then read matrix
|
|
// If CameraOffset == 0: matrix is inline in InGameState at CameraMatrixOffset
|
|
nint matrixAddr;
|
|
if (offsets.CameraOffset > 0)
|
|
{
|
|
var cam = mem.ReadPointer(inGameState + offsets.CameraOffset);
|
|
if (cam == 0) return;
|
|
matrixAddr = cam + offsets.CameraMatrixOffset;
|
|
}
|
|
else
|
|
{
|
|
matrixAddr = inGameState + offsets.CameraMatrixOffset;
|
|
}
|
|
|
|
// Cache the resolved address for fast per-frame reads
|
|
_cachedCameraMatrixAddr = matrixAddr;
|
|
|
|
// Read 64-byte Matrix4x4 as a single struct (System.Numerics.Matrix4x4 is already unmanaged/sequential)
|
|
var m = mem.Read<Matrix4x4>(matrixAddr);
|
|
|
|
// Quick sanity check
|
|
if (float.IsNaN(m.M11) || float.IsInfinity(m.M11)) return;
|
|
|
|
snap.CameraMatrix = m;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolved addresses for hot-path reads (camera, player position, player vitals, InGameState).
|
|
/// </summary>
|
|
public readonly struct HotAddresses
|
|
{
|
|
public readonly nint CameraMatrixAddr;
|
|
public readonly nint PlayerRenderAddr;
|
|
public readonly nint PlayerLifeAddr;
|
|
public readonly nint InGameStateAddr;
|
|
public readonly nint ControllerAddr;
|
|
public readonly bool IsValid;
|
|
|
|
public HotAddresses(nint cameraMatrix, nint playerRender, nint playerLife, nint inGameState, nint controller)
|
|
{
|
|
CameraMatrixAddr = cameraMatrix;
|
|
PlayerRenderAddr = playerRender;
|
|
PlayerLifeAddr = playerLife;
|
|
InGameStateAddr = inGameState;
|
|
ControllerAddr = controller;
|
|
IsValid = cameraMatrix != 0 || playerRender != 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns resolved addresses for the hot path.
|
|
/// Call after ReadSnapshot() has populated the cached addresses.
|
|
/// </summary>
|
|
public HotAddresses ResolveHotAddresses()
|
|
{
|
|
return new HotAddresses(
|
|
_cachedCameraMatrixAddr,
|
|
_components?.CachedRenderComponentAddr ?? 0,
|
|
_components?.CachedLifeComponentAddr ?? 0,
|
|
_lastInGameState,
|
|
_lastController);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
Detach();
|
|
}
|
|
}
|