using System.Text.Json; using System.Text.Json.Serialization; using Serilog; namespace Automata.Memory; public sealed class GameOffsets { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never }; public string ProcessName { get; set; } = "PathOfExileSteam"; /// Pattern to find GameState global. Use ^ to mark the RIP displacement position. public string GameStatePattern { get; set; } = "48 83 EC ?? 48 8B F1 33 ED 48 39 2D ^"; /// Fallback: manual offset from module base to GameState global (hex). Used when pattern is empty or fails. public int GameStateGlobalOffset { get; set; } /// Bytes to add to pattern scan result to reach the actual global. public int PatternResultAdjust { get; set; } = 0x18; // ── GameState → States ── /// Offset to States begin/end pair in GameState (dump: 0x48). public int StatesBeginOffset { get; set; } = 0x48; /// Bytes per state entry (16 for inline slots, 8 for vector of pointers). public int StateStride { get; set; } = 0x10; /// Offset within each state entry to the actual state pointer. public int StatePointerOffset { get; set; } = 0; /// Total number of state slots (ExileCore: 12 states, State0-State11). public int StateCount { get; set; } = 12; /// Which state index is InGameState (typically 4). public int InGameStateIndex { get; set; } = 4; /// Offset from controller to active states vector begin/end pair (ExileCore: 0x20). public int ActiveStatesOffset { get; set; } = 0x20; /// Offset from controller to the active state pointer. When it != InGameState, we're loading. 0 = disabled. public int IsLoadingOffset { get; set; } = 0; /// If true, states are inline in the controller struct. If false, StatesBeginOffset points to a begin/end vector pair. public bool StatesInline { get; set; } = true; /// Direct offset from controller to InGameState pointer (bypasses state array). 0 = use state array instead. CE confirmed: 0x210. public int InGameStateDirectOffset { get; set; } = 0x210; // ── InGameState → sub-structures ── /// InGameState → EscapeState int32 flag (0=closed, 1=open). Diff-scan confirmed: 0x20C. 0 = disabled. public int EscapeStateOffset { get; set; } = 0x20C; /// InGameState → AreaInstance (IngameData) pointer (dump: 0x298, CE confirmed: 0x290). public int IngameDataFromStateOffset { get; set; } = 0x290; /// InGameState → WorldData pointer (dump: 0x2F8). public int WorldDataFromStateOffset { get; set; } = 0x2F8; // ── AreaInstance (IngameData) → sub-structures ── /// AreaInstance → CurrentAreaLevel (dump: byte at 0xAC, CE confirmed: byte at 0xC4). public int AreaLevelOffset { get; set; } = 0xC4; /// If true, AreaLevel is a byte. If false, read as int. public bool AreaLevelIsByte { get; set; } = true; /// Static offset from module base to cached area level int (CE: exe+3E84B78). 0 = disabled. public int AreaLevelStaticOffset { get; set; } = 0; /// AreaInstance → CurrentAreaHash uint (dump: 0xEC). public int AreaHashOffset { get; set; } = 0xEC; /// AreaInstance → ServerData pointer (dump: 0x9F0 via LocalPlayerStruct.ServerDataPtr). public int ServerDataOffset { get; set; } = 0x9F0; /// AreaInstance → LocalPlayer entity pointer (dump: 0x9F0+0x20 = 0xA10 via LocalPlayerStruct.LocalPlayerPtr). public int LocalPlayerDirectOffset { get; set; } = 0xA10; /// AreaInstance → EntityListStruct offset (dump: 0xAF8, CE confirmed: 0xB50). public int EntityListOffset { get; set; } = 0xB50; /// Offset within StdMap to _Mysize (entity count). MSVC std::map: head(8) + size(8). public int EntityCountInternalOffset { get; set; } = 0x08; // ── Entity list node layout (MSVC std::map red-black tree) ── /// Tree node → left child pointer. public int EntityNodeLeftOffset { get; set; } = 0x00; /// Tree node → parent pointer. public int EntityNodeParentOffset { get; set; } = 0x08; /// Tree node → right child pointer. public int EntityNodeRightOffset { get; set; } = 0x10; /// Tree node → entity pointer (pair value at +0x28). public int EntityNodeValueOffset { get; set; } = 0x28; /// Entity → uint ID offset (EntityOffsets: +0x80). public int EntityIdOffset { get; set; } = 0x80; /// Entity → IsValid byte offset (EntityOffsets: +0x84). public int EntityFlagsOffset { get; set; } = 0x84; /// Entity → EntityDetailsPtr (Head/MainObject pointer, +0x08). public int EntityDetailsOffset { get; set; } = 0x08; /// EntityDetails → std::string path (MSVC layout). Offset within EntityDetails struct. public int EntityPathStringOffset { get; set; } = 0x08; // ServerData → fields /// ServerData → LocalPlayer entity pointer (fallback if LocalPlayerDirectOffset is 0). public int LocalPlayerOffset { get; set; } = 0x20; // ── Entity / Component ── public int ComponentListOffset { get; set; } = 0x10; /// Entity → ObjectHeader pointer (for alternative component lookup via name→index map). ExileCore: 0x08. public int EntityHeaderOffset { get; set; } = 0x08; /// EntityDetails → ComponentLookup object pointer. Confirmed: 0x28. public int ComponentLookupOffset { get; set; } = 0x28; /// ComponentLookup object → Vec2 begin/end (name entry array). public int ComponentLookupVec2Offset { get; set; } = 0x28; /// Size of each entry in Vec2 (bytes). Confirmed: 16 = { char* name (8), int32 index (4), int32 flags (4) }. public int ComponentLookupEntrySize { get; set; } = 16; /// Offset to the char* name pointer within each lookup entry. public int ComponentLookupNameOffset { get; set; } = 0; /// Offset to the component index (int32) within each lookup entry. public int ComponentLookupIndexOffset { get; set; } = 8; /// Index of Life component in entity's component list. -1 = auto-discover via pattern scan. public int LifeComponentIndex { get; set; } = -1; /// Index of Render/Position component in entity's component list. -1 = unknown. public int RenderComponentIndex { get; set; } = -1; // ── Life component ── /// First offset from AreaInstance to reach Life component (AreaInstance → ptr). 0 = use entity component list instead. public int LifeComponentOffset1 { get; set; } = 0x420; /// Second offset from intermediate pointer to Life component (ptr → Life). public int LifeComponentOffset2 { get; set; } = 0x98; public int LifeHealthOffset { get; set; } = 0x1A8; public int LifeManaOffset { get; set; } = 0x1F8; public int LifeEsOffset { get; set; } = 0x230; public int VitalCurrentOffset { get; set; } = 0x30; public int VitalTotalOffset { get; set; } = 0x2C; // ── Render/Position component ── public int PositionXOffset { get; set; } = 0x138; public int PositionYOffset { get; set; } = 0x13C; public int PositionZOffset { get; set; } = 0x140; // ── Camera (for WorldToScreen projection) ── /// Offset from InGameState to Camera pointer. 0 = disabled (use ScanCamera to discover). public int CameraOffset { get; set; } = 0x308; /// Offset within Camera struct to the Matrix4x4 (64 bytes). 0 = disabled. public int CameraMatrixOffset { get; set; } = 0x1A0; // ── Terrain (inline in AreaInstance) ── /// Offset from AreaInstance to inline TerrainStruct (dump: 0xCC0). public int TerrainListOffset { get; set; } = 0xCC0; /// If true, terrain is inline in AreaInstance (no pointer dereference). If false, follow pointer. public bool TerrainInline { get; set; } = true; /// TerrainStruct → TotalTiles offset (scan: 0x90, StdTuple2D of long). public int TerrainDimensionsOffset { get; set; } = 0x90; /// TerrainStruct → GridWalkableData StdVector offset (scan: 0x148). public int TerrainWalkableGridOffset { get; set; } = 0x148; /// TerrainStruct → BytesPerRow (scan: 0x1A8). public int TerrainBytesPerRowOffset { get; set; } = 0x1A8; /// Kept for pointer-based terrain mode (TerrainInline=false). public int TerrainGridPtrOffset { get; set; } = 0x08; public int SubTilesPerCell { get; set; } = 23; public static GameOffsets Load(string path) { if (!File.Exists(path)) { Log.Information("Offsets file not found at '{Path}', using defaults", path); var defaults = new GameOffsets(); defaults.Save(path); return defaults; } try { var json = File.ReadAllText(path); var offsets = JsonSerializer.Deserialize(json, JsonOptions); if (offsets is null) { Log.Warning("Failed to deserialize '{Path}', using defaults", path); return new GameOffsets(); } Log.Information("Loaded offsets from '{Path}'", path); return offsets; } catch (Exception ex) { Log.Error(ex, "Error loading offsets from '{Path}'", path); return new GameOffsets(); } } public void Save(string path) { try { var json = JsonSerializer.Serialize(this, JsonOptions); File.WriteAllText(path, json); Log.Debug("Saved offsets to '{Path}'", path); } catch (Exception ex) { Log.Error(ex, "Error saving offsets to '{Path}'", path); } } }