This commit is contained in:
Boki 2026-03-04 16:49:30 -05:00
parent 0df70abad7
commit 0c14d78d8a
25 changed files with 1487 additions and 179 deletions

View file

@ -7,7 +7,7 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Roboto.Memory; using Roboto.Memory;
using Roboto.Memory.States; using Roboto.Memory.Objects;
namespace Automata.Ui.ViewModels; namespace Automata.Ui.ViewModels;

View file

@ -1,6 +1,3 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components; namespace Roboto.GameOffsets.Components;
/// <summary> /// <summary>
@ -14,76 +11,3 @@ public static class ActorOffsets
public const int CooldownsVector = 0xB18; public const int CooldownsVector = 0xB18;
public const int DeployedEntitiesVector = 0xC10; public const int DeployedEntitiesVector = 0xC10;
} }
/// <summary>
/// An entry in the ActiveSkills vector: shared_ptr pair (0x10 bytes).
/// Follow ActiveSkillPtr (first pointer) for skill details.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ActiveSkillEntry
{
public nint ActiveSkillPtr;
public nint ControlBlockPtr; // shared_ptr control block, not used
}
/// <summary>
/// Details of an active skill. The shared_ptr in the ActiveSkills vector points
/// 0x10 bytes into the object (past vtable + UseStage/CastType), so we read from
/// ActiveSkillPtr - 0x10 and all offsets are relative to the true object base.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct ActiveSkillDetails
{
[FieldOffset(0x00)] public nint Vtable;
[FieldOffset(0x08)] public int UseStage;
[FieldOffset(0x0C)] public int CastType;
[FieldOffset(0x20)] public uint UnknownIdAndEquipmentInfo;
[FieldOffset(0x28)] public nint GrantedEffectsPerLevelDatRow;
[FieldOffset(0x30)] public nint ActiveSkillsDatPtr;
[FieldOffset(0x40)] public nint GrantedEffectStatSetsPerLevelDatRow;
[FieldOffset(0xA8)] public int TotalUses;
[FieldOffset(0xB8)] public int TotalCooldownTimeInMs;
}
/// <summary>
/// Cooldown state for a skill. Entries in Actor+0xB18 vector.
/// From ExileCore2 GameOffsets.Objects.Components.ActiveSkillCooldown.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x48)]
public struct ActiveSkillCooldown
{
[FieldOffset(0x08)] public int ActiveSkillsDatId;
[FieldOffset(0x10)] public StdVector CooldownsList; // 0x10-byte entries
[FieldOffset(0x30)] public int MaxUses;
[FieldOffset(0x34)] public int TotalCooldownTimeInMs;
[FieldOffset(0x3C)] public uint UnknownIdAndEquipmentInfo;
/// <summary>Number of active cooldown timer entries.</summary>
public readonly int TotalActiveCooldowns => (int)CooldownsList.TotalElements(0x10);
/// <summary>True if all uses are on cooldown.</summary>
public readonly bool CannotBeUsed => TotalActiveCooldowns >= MaxUses;
}
/// <summary>Vaal soul tracking.</summary>
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct VaalSoulStructure
{
[FieldOffset(0x00)] public nint ActiveSkillsDatPtr;
[FieldOffset(0x08)] public nint UselessPtr;
[FieldOffset(0x10)] public int RequiredSouls;
[FieldOffset(0x14)] public int CurrentSouls;
public readonly bool CannotBeUsed => CurrentSouls < RequiredSouls;
}
/// <summary>A deployed entity (totem, mine, etc.).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DeployedEntityStructure
{
public int EntityId;
public int ActiveSkillsDatId;
public int DeployedObjectType;
public int PAD_0x014;
public int Counter;
}

View file

@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>A deployed entity (totem, mine, etc.).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ActorDeployedEntity
{
public int EntityId;
public int ActiveSkillsDatId;
public int DeployedObjectType;
public int PAD_0x014;
public int Counter;
}

View file

@ -0,0 +1,33 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>
/// An entry in the ActiveSkills vector: shared_ptr pair (0x10 bytes).
/// Follow ActiveSkillPtr (first pointer) for skill details.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ActorSkillEntry
{
public nint ActiveSkillPtr;
public nint ControlBlockPtr; // shared_ptr control block, not used
}
/// <summary>
/// Details of an active skill. The shared_ptr in the ActiveSkills vector points
/// 0x10 bytes into the object (past vtable + UseStage/CastType), so we read from
/// ActiveSkillPtr - 0x10 and all offsets are relative to the true object base.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct ActorSkillDetails
{
[FieldOffset(0x00)] public nint Vtable;
[FieldOffset(0x08)] public int UseStage;
[FieldOffset(0x0C)] public int CastType;
[FieldOffset(0x20)] public uint UnknownIdAndEquipmentInfo;
[FieldOffset(0x28)] public nint GrantedEffectsPerLevelDatRow;
[FieldOffset(0x30)] public nint ActiveSkillsDatPtr;
[FieldOffset(0x40)] public nint GrantedEffectStatSetsPerLevelDatRow;
[FieldOffset(0xA8)] public int TotalUses;
[FieldOffset(0xB8)] public int TotalCooldownTimeInMs;
}

View file

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>
/// Cooldown state for a skill. Entries in Actor+0xB18 vector.
/// From ExileCore2 GameOffsets.Objects.Components.ActiveSkillCooldown.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x48)]
public struct ActorSkillCooldown
{
[FieldOffset(0x08)] public int ActiveSkillsDatId;
[FieldOffset(0x10)] public StdVector CooldownsList; // 0x10-byte entries
[FieldOffset(0x30)] public int MaxUses;
[FieldOffset(0x34)] public int TotalCooldownTimeInMs;
[FieldOffset(0x3C)] public uint UnknownIdAndEquipmentInfo;
/// <summary>Number of active cooldown timer entries.</summary>
public readonly int TotalActiveCooldowns => (int)CooldownsList.TotalElements(0x10);
/// <summary>True if all uses are on cooldown.</summary>
public readonly bool CannotBeUsed => TotalActiveCooldowns >= MaxUses;
}

View file

@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Vaal soul tracking.</summary>
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct ActorVaalSoulStructure
{
[FieldOffset(0x00)] public nint ActiveSkillsDatPtr;
[FieldOffset(0x08)] public nint UselessPtr;
[FieldOffset(0x10)] public int RequiredSouls;
[FieldOffset(0x14)] public int CurrentSouls;
public readonly bool CannotBeUsed => CurrentSouls < RequiredSouls;
}

View file

@ -14,14 +14,16 @@ public struct WorldData
[FieldOffset(0xA0)] public nint CameraPtr; [FieldOffset(0xA0)] public nint CameraPtr;
} }
/// <summary>Details about the current world area (act, waypoint, etc.).</summary> /// <summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)] /// WorldAreaDetails pointer leads to an AreaTemplate struct.
/// Fields are read via configurable offsets in GameOffsets (AreaTemplate* section).
/// ExileCore layout: +0x00 RawName (wchar*), +0x08 Name (wchar*), +0x10 Act (int),
/// +0x14 IsTown (byte), +0x15 HasWaypoint (byte), +0x26 MonsterLevel (int), +0x2A WorldAreaId (int).
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
public struct WorldAreaDetails public struct WorldAreaDetails
{ {
[FieldOffset(0x00)] public nint NamePtr; // All fields read dynamically via GameOffsets — this struct is kept for documentation.
[FieldOffset(0x08)] public nint ActPtr;
[FieldOffset(0x10)] public int IsTown;
[FieldOffset(0x14)] public int HasWaypoint;
} }
/// <summary>Camera structure — contains the world-to-screen projection matrix.</summary> /// <summary>Camera structure — contains the world-to-screen projection matrix.</summary>

View file

@ -3,6 +3,7 @@ using System.Drawing.Imaging;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Roboto.Memory.Objects;
using Serilog; using Serilog;
namespace Roboto.Memory; namespace Roboto.Memory;
@ -16,7 +17,7 @@ public sealed class MemoryDiagnostics
private readonly MemoryContext _ctx; private readonly MemoryContext _ctx;
private readonly GameStateReader _stateReader; private readonly GameStateReader _stateReader;
private readonly ComponentReader _components; private readonly ComponentReader _components;
private readonly EntityReader _entities; private readonly EntityList _entities;
private readonly MsvcStringReader _strings; private readonly MsvcStringReader _strings;
private readonly RttiResolver _rtti; private readonly RttiResolver _rtti;
@ -33,7 +34,7 @@ public sealed class MemoryDiagnostics
MemoryContext ctx, MemoryContext ctx,
GameStateReader stateReader, GameStateReader stateReader,
ComponentReader components, ComponentReader components,
EntityReader entities, EntityList entities,
MsvcStringReader strings, MsvcStringReader strings,
RttiResolver rtti) RttiResolver rtti)
{ {
@ -3713,7 +3714,9 @@ public sealed class MemoryDiagnostics
} }
// Also try to read a few nearby monsters' Actor components for comparison // Also try to read a few nearby monsters' Actor components for comparison
_entities.ReadEntities(snap, ingameData); _entities.ExpectedCount = snap.EntityCount;
_entities.Update(ingameData);
snap.Entities = _entities.Entities;
if (snap.Entities is { Count: > 0 }) if (snap.Entities is { Count: > 0 })
{ {
var monsterCount = 0; var monsterCount = 0;

View file

@ -1,5 +1,5 @@
using System.Numerics; using System.Numerics;
using Roboto.Memory.States; using Roboto.Memory.Objects;
using Serilog; using Serilog;
namespace Roboto.Memory; namespace Roboto.Memory;
@ -35,12 +35,8 @@ public class GameMemoryReader : IDisposable
private nint _lastInGameState; private nint _lastInGameState;
private nint _lastController; private nint _lastController;
private ComponentReader? _components; private ComponentReader? _components;
private EntityReader? _entities;
private TerrainReader? _terrain;
private MsvcStringReader? _strings; private MsvcStringReader? _strings;
private RttiResolver? _rtti; private RttiResolver? _rtti;
private SkillReader? _skills;
private QuestReader? _quests;
private QuestNameLookup? _questNames; private QuestNameLookup? _questNames;
public ObjectRegistry Registry => _registry; public ObjectRegistry Registry => _registry;
@ -92,18 +88,19 @@ public class GameMemoryReader : IDisposable
Log.Information("GameState base (manual): 0x{Address:X}", _ctx.GameStateBase); Log.Information("GameState base (manual): 0x{Address:X}", _ctx.GameStateBase);
} }
// Create sub-readers // Create infrastructure
_gameStates = new GameStates(_ctx);
_strings = new MsvcStringReader(_ctx); _strings = new MsvcStringReader(_ctx);
_rtti = new RttiResolver(_ctx); _rtti = new RttiResolver(_ctx);
_stateReader = new GameStateReader(_ctx); _stateReader = new GameStateReader(_ctx);
_components = new ComponentReader(_ctx, _strings); _components = new ComponentReader(_ctx, _strings);
_entities = new EntityReader(_ctx, _components, _strings);
_terrain = new TerrainReader(_ctx);
_skills = new SkillReader(_ctx, _components, _strings);
_questNames ??= LoadQuestNames(); _questNames ??= LoadQuestNames();
_quests = new QuestReader(_ctx, _strings, _questNames);
Diagnostics = new MemoryDiagnostics(_ctx, _stateReader, _components, _entities, _strings, _rtti); // Hierarchical state tree — owns EntityList, PlayerSkills, QuestFlags, Terrain
_gameStates = new GameStates(_ctx, _components, _strings, _questNames);
// Diagnostics uses the EntityList from the hierarchy
var entityList = _gameStates.InGame.AreaInstance.EntityList;
Diagnostics = new MemoryDiagnostics(_ctx, _stateReader, _components, entityList, _strings, _rtti);
return true; return true;
} }
@ -115,12 +112,8 @@ public class GameMemoryReader : IDisposable
_gameStates = null; _gameStates = null;
_stateReader = null; _stateReader = null;
_components = null; _components = null;
_entities = null;
_terrain = null;
_strings = null; _strings = null;
_rtti = null; _rtti = null;
_skills = null;
_quests = null;
// _questNames intentionally kept — reloaded only once // _questNames intentionally kept — reloaded only once
Diagnostics = null; Diagnostics = null;
} }
@ -169,11 +162,17 @@ public class GameMemoryReader : IDisposable
try try
{ {
// Hierarchical state read — resolves controller, state slots, cascades to children // Hierarchical state read — resolves controller, state slots, cascades to all children
var gs = _gameStates!; var gs = _gameStates!;
// Set loading state on terrain before cascade
gs.InGame.AreaInstance.SetLoadingState(gs.AreaLoading.IsLoading);
if (!gs.Update()) if (!gs.Update())
return snap; return snap;
var ai = gs.InGame.AreaInstance;
// Populate snapshot from state hierarchy // Populate snapshot from state hierarchy
snap.ControllerPtr = gs.ControllerPtr; snap.ControllerPtr = gs.ControllerPtr;
snap.StatesCount = gs.StatesCount; snap.StatesCount = gs.StatesCount;
@ -182,18 +181,31 @@ public class GameMemoryReader : IDisposable
snap.InGameStatePtr = gs.InGame.Address; snap.InGameStatePtr = gs.InGame.Address;
snap.IsLoading = gs.AreaLoading.IsLoading; snap.IsLoading = gs.AreaLoading.IsLoading;
snap.IsEscapeOpen = gs.InGame.IsEscapeOpen; snap.IsEscapeOpen = gs.InGame.IsEscapeOpen;
snap.AreaInstancePtr = gs.InGame.AreaInstance.Address; snap.AreaInstancePtr = ai.Address;
snap.ServerDataPtr = gs.InGame.AreaInstance.ServerDataPtr; snap.ServerDataPtr = ai.ServerDataPtr;
snap.LocalPlayerPtr = gs.InGame.AreaInstance.LocalPlayerPtr; snap.LocalPlayerPtr = ai.LocalPlayerPtr;
snap.EntityCount = gs.InGame.AreaInstance.EntityCount; snap.EntityCount = ai.EntityCount;
// Area level — prefer hierarchical read, keep static offset as fallback // Area level — prefer hierarchical read, keep static offset as fallback
var areaLevel = gs.InGame.AreaInstance.AreaLevel; var areaLevel = ai.AreaLevel;
if (areaLevel > 0) if (areaLevel > 0)
snap.AreaLevel = areaLevel; snap.AreaLevel = areaLevel;
snap.AreaHash = gs.InGame.AreaInstance.AreaHash; snap.AreaHash = ai.AreaHash;
// Camera matrix from WorldDataState // Area template from WorldData
var at = gs.InGame.WorldData.AreaTemplate;
if (at.IsValid)
{
snap.AreaRawName = at.RawName;
snap.AreaName = at.Name;
snap.AreaAct = at.Act;
snap.AreaIsTown = at.IsTown;
snap.AreaHasWaypoint = at.HasWaypoint;
snap.AreaMonsterLevel = at.MonsterLevel;
snap.WorldAreaId = at.WorldAreaId;
}
// Camera matrix from WorldData
if (gs.InGame.WorldData.CameraMatrix.HasValue) if (gs.InGame.WorldData.CameraMatrix.HasValue)
{ {
snap.CameraMatrix = gs.InGame.WorldData.CameraMatrix; snap.CameraMatrix = gs.InGame.WorldData.CameraMatrix;
@ -201,7 +213,6 @@ public class GameMemoryReader : IDisposable
} }
else else
{ {
// Fallback: direct camera read (inline or pointer-based)
ReadCameraMatrix(snap, gs.InGame.Address); ReadCameraMatrix(snap, gs.InGame.Address);
} }
@ -210,53 +221,46 @@ public class GameMemoryReader : IDisposable
// Diagnostic state slots — GameStateReader still used for MemoryDiagnostics compat // Diagnostic state slots — GameStateReader still used for MemoryDiagnostics compat
_stateReader!.ReadStateSlots(snap); _stateReader!.ReadStateSlots(snap);
// Loading/escape overrides from GameStateReader (active states vector method)
_stateReader.ReadIsLoading(snap); _stateReader.ReadIsLoading(snap);
_stateReader.ReadEscapeState(snap); _stateReader.ReadEscapeState(snap);
// Reconcile CurrentGameState with reliable loading/escape detection // Reconcile CurrentGameState with reliable loading/escape detection
if (snap.IsLoading) if (snap.IsLoading)
snap.CurrentGameState = States.GameStateType.AreaLoadingState; snap.CurrentGameState = GameStateType.AreaLoadingState;
else if (snap.IsEscapeOpen) else if (snap.IsEscapeOpen)
snap.CurrentGameState = States.GameStateType.EscapeState; snap.CurrentGameState = GameStateType.EscapeState;
var ingameData = gs.InGame.AreaInstance.Address; if (ai.Address != 0)
if (ingameData != 0)
{ {
// Entity list // Entities — read from hierarchy
if (snap.EntityCount > 0) snap.Entities = ai.EntityList.Entities;
_entities!.ReadEntities(snap, ingameData);
// Player vitals & position — ECS // Player vitals & position — still via ComponentReader (ECS)
if (snap.LocalPlayerPtr != 0) if (snap.LocalPlayerPtr != 0)
{ {
if (snap.LocalPlayerPtr != _components!.LastLocalPlayer) if (snap.LocalPlayerPtr != _components!.LastLocalPlayer)
_terrain!.InvalidateCache(); ai.InvalidateTerrainCache();
_components.InvalidateCaches(snap.LocalPlayerPtr); _components.InvalidateCaches(snap.LocalPlayerPtr);
_components.ReadPlayerVitals(snap); _components.ReadPlayerVitals(snap);
_components.ReadPlayerPosition(snap); _components.ReadPlayerPosition(snap);
snap.CharacterName = _components.ReadPlayerName(snap.LocalPlayerPtr); snap.CharacterName = _components.ReadPlayerName(snap.LocalPlayerPtr);
// Resolve PSD for skill bar + quest reads
nint psdPtr = 0;
if (snap.ServerDataPtr != 0)
{
var psdVecBegin = mem.ReadPointer(snap.ServerDataPtr + offsets.PlayerServerDataOffset);
if (psdVecBegin != 0)
psdPtr = mem.ReadPointer(psdVecBegin);
}
snap.PlayerSkills = _skills!.ReadPlayerSkills(snap.LocalPlayerPtr, psdPtr);
snap.QuestFlags = _quests!.ReadQuestFlags(snap.ServerDataPtr);
} }
// Skills & quests — read from hierarchy
snap.PlayerSkills = ai.PlayerSkills.Skills;
snap.QuestFlags = ai.QuestFlags.Quests;
// Read state flag bytes // Read state flag bytes
if (snap.InGameStatePtr != 0) if (snap.InGameStatePtr != 0)
snap.StateFlagBytes = mem.ReadBytes(snap.InGameStatePtr + snap.StateFlagBaseOffset, 0x30); snap.StateFlagBytes = mem.ReadBytes(snap.InGameStatePtr + snap.StateFlagBaseOffset, 0x30);
// Terrain // Terrain — read from hierarchy
_terrain!.ReadTerrain(snap, ingameData); snap.TerrainCols = ai.Terrain.TerrainCols;
snap.TerrainRows = ai.Terrain.TerrainRows;
snap.TerrainWidth = ai.Terrain.TerrainWidth;
snap.TerrainHeight = ai.Terrain.TerrainHeight;
snap.Terrain = ai.Terrain.Grid;
snap.TerrainWalkablePercent = ai.Terrain.WalkablePercent;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -265,7 +269,7 @@ public class GameMemoryReader : IDisposable
} }
// Update edge detection for next tick // Update edge detection for next tick
_terrain!.UpdateLoadingEdge(snap.IsLoading); _gameStates!.InGame.AreaInstance.Terrain.UpdateLoadingEdge(snap.IsLoading);
return snap; return snap;
} }
@ -277,8 +281,6 @@ public class GameMemoryReader : IDisposable
if (offsets.CameraMatrixOffset <= 0) return; 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; nint matrixAddr;
if (offsets.CameraOffset > 0) if (offsets.CameraOffset > 0)
{ {
@ -291,13 +293,9 @@ public class GameMemoryReader : IDisposable
matrixAddr = inGameState + offsets.CameraMatrixOffset; matrixAddr = inGameState + offsets.CameraMatrixOffset;
} }
// Cache the resolved address for fast per-frame reads
_cachedCameraMatrixAddr = matrixAddr; _cachedCameraMatrixAddr = matrixAddr;
// Read 64-byte Matrix4x4 as a single struct (System.Numerics.Matrix4x4 is already unmanaged/sequential)
var m = mem.Read<Matrix4x4>(matrixAddr); var m = mem.Read<Matrix4x4>(matrixAddr);
// Quick sanity check
if (float.IsNaN(m.M11) || float.IsInfinity(m.M11)) return; if (float.IsNaN(m.M11) || float.IsInfinity(m.M11)) return;
snap.CameraMatrix = m; snap.CameraMatrix = m;
@ -332,7 +330,6 @@ public class GameMemoryReader : IDisposable
/// </summary> /// </summary>
public HotAddresses ResolveHotAddresses() public HotAddresses ResolveHotAddresses()
{ {
// Prefer camera address from hierarchical state, fallback to cached
var cameraAddr = _gameStates?.InGame.WorldData.CameraMatrixAddress ?? 0; var cameraAddr = _gameStates?.InGame.WorldData.CameraMatrixAddress ?? 0;
if (cameraAddr == 0) if (cameraAddr == 0)
cameraAddr = _cachedCameraMatrixAddr; cameraAddr = _cachedCameraMatrixAddr;

View file

@ -235,6 +235,24 @@ public sealed class GameOffsets
/// <summary>Offset within Camera struct to the Matrix4x4 (64 bytes). 0 = disabled.</summary> /// <summary>Offset within Camera struct to the Matrix4x4 (64 bytes). 0 = disabled.</summary>
public int CameraMatrixOffset { get; set; } = 0x1A0; public int CameraMatrixOffset { get; set; } = 0x1A0;
// ── AreaTemplate (WorldData → WorldAreaDetailsPtr → AreaTemplate) ──
/// <summary>WorldData struct → WorldAreaDetailsPtr offset. Already in struct at 0x98.</summary>
public int WorldAreaDetailsOffset { get; set; } = 0x98;
/// <summary>AreaTemplate → RawName wchar_t* pointer.</summary>
public int AreaTemplateRawNameOffset { get; set; } = 0x00;
/// <summary>AreaTemplate → Name wchar_t* pointer (display name).</summary>
public int AreaTemplateNameOffset { get; set; } = 0x08;
/// <summary>AreaTemplate → Act int32.</summary>
public int AreaTemplateActOffset { get; set; } = 0x10;
/// <summary>AreaTemplate → IsTown byte (1 = town).</summary>
public int AreaTemplateIsTownOffset { get; set; } = 0x14;
/// <summary>AreaTemplate → HasWaypoint byte (1 = has waypoint).</summary>
public int AreaTemplateHasWaypointOffset { get; set; } = 0x15;
/// <summary>AreaTemplate → MonsterLevel int32 (nominal area level).</summary>
public int AreaTemplateMonsterLevelOffset { get; set; } = 0x26;
/// <summary>AreaTemplate → WorldAreaId int32.</summary>
public int AreaTemplateWorldAreaIdOffset { get; set; } = 0x2A;
// ── UiRootStruct (InGameState → UI tree roots) ── // ── UiRootStruct (InGameState → UI tree roots) ──
/// <summary>Offset from InGameState to UiRootStruct pointer. GameOverlay2: 0x340.</summary> /// <summary>Offset from InGameState to UiRootStruct pointer. GameOverlay2: 0x340.</summary>
public int UiRootStructOffset { get; set; } = 0x340; public int UiRootStructOffset { get; set; } = 0x340;

View file

@ -1,11 +1,14 @@
namespace Roboto.Memory.States; using Roboto.Memory;
namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Reads fields from the AreaInstance (IngameData) address. /// Reads fields from the AreaInstance (IngameData) address.
/// Individual field reads — the full struct is 3280B, too large to bulk-read. /// Individual field reads — the full struct is 3280B, too large to bulk-read.
/// Uses GameOffsets for configurable offsets. /// Uses GameOffsets for configurable offsets.
/// Owns EntityList, PlayerSkills, QuestFlags, and Terrain children.
/// </summary> /// </summary>
public sealed class AreaInstanceState : RemoteObject public sealed class AreaInstance : RemoteObject
{ {
public int AreaLevel { get; private set; } public int AreaLevel { get; private set; }
public uint AreaHash { get; private set; } public uint AreaHash { get; private set; }
@ -13,7 +16,19 @@ public sealed class AreaInstanceState : RemoteObject
public nint LocalPlayerPtr { get; private set; } public nint LocalPlayerPtr { get; private set; }
public int EntityCount { get; private set; } public int EntityCount { get; private set; }
public AreaInstanceState(MemoryContext ctx) : base(ctx) { } public EntityList EntityList { get; }
public PlayerSkills PlayerSkills { get; }
public QuestFlags QuestFlags { get; }
public Terrain Terrain { get; }
public AreaInstance(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
: base(ctx)
{
EntityList = new EntityList(ctx, components, strings);
PlayerSkills = new PlayerSkills(ctx, components, strings);
QuestFlags = new QuestFlags(ctx, strings, questNames);
Terrain = new Terrain(ctx);
}
protected override bool ReadData() protected override bool ReadData()
{ {
@ -49,9 +64,64 @@ public sealed class AreaInstanceState : RemoteObject
var count = (int)mem.Read<long>(Address + offsets.EntityListOffset + offsets.EntityCountInternalOffset); var count = (int)mem.Read<long>(Address + offsets.EntityListOffset + offsets.EntityCountInternalOffset);
EntityCount = count is > 0 and < 50000 ? count : 0; EntityCount = count is > 0 and < 50000 ? count : 0;
// Cascade to children
if (EntityCount > 0)
{
EntityList.ExpectedCount = EntityCount;
EntityList.Update(Address);
}
else
{
EntityList.Reset();
}
// Resolve PSD for skill bar + quest reads
nint psdPtr = 0;
if (ServerDataPtr != 0)
{
var psdVecBegin = mem.ReadPointer(ServerDataPtr + offsets.PlayerServerDataOffset);
if (psdVecBegin != 0)
psdPtr = mem.ReadPointer(psdVecBegin);
}
if (LocalPlayerPtr != 0)
{
PlayerSkills.PsdPtr = psdPtr;
PlayerSkills.Update(LocalPlayerPtr);
}
else
{
PlayerSkills.Reset();
}
if (ServerDataPtr != 0)
QuestFlags.Update(ServerDataPtr);
else
QuestFlags.Reset();
// Terrain — pass loading/area state before update
Terrain.AreaHash = AreaHash;
Terrain.Update(Address);
return true; return true;
} }
/// <summary>
/// Sets loading state on Terrain (called from InGameState/GameMemoryReader).
/// </summary>
public void SetLoadingState(bool isLoading)
{
Terrain.IsLoading = isLoading;
}
/// <summary>
/// Invalidates terrain cache (called when LocalPlayer changes on zone change).
/// </summary>
public void InvalidateTerrainCache()
{
Terrain.InvalidateCache();
}
protected override void Clear() protected override void Clear()
{ {
AreaLevel = 0; AreaLevel = 0;
@ -59,5 +129,9 @@ public sealed class AreaInstanceState : RemoteObject
ServerDataPtr = 0; ServerDataPtr = 0;
LocalPlayerPtr = 0; LocalPlayerPtr = 0;
EntityCount = 0; EntityCount = 0;
EntityList.Reset();
PlayerSkills.Reset();
QuestFlags.Reset();
Terrain.Reset();
} }
} }

View file

@ -1,11 +1,11 @@
using Roboto.GameOffsets.States; using Roboto.Memory;
namespace Roboto.Memory.States; namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Reads AreaLoading state (slot 0). Individual field reads — the full struct is 3672B, wasteful to bulk-read. /// Reads AreaLoading state (slot 0). Individual field reads — the full struct is 3672B, wasteful to bulk-read.
/// </summary> /// </summary>
public sealed class AreaLoadingState : RemoteObject public sealed class AreaLoading : RemoteObject
{ {
// AreaLoading struct field offsets // AreaLoading struct field offsets
private const int IsLoadingOffset = 0x660; private const int IsLoadingOffset = 0x660;
@ -14,7 +14,7 @@ public sealed class AreaLoadingState : RemoteObject
public bool IsLoading { get; private set; } public bool IsLoading { get; private set; }
public long TotalLoadingScreenTimeMs { get; private set; } public long TotalLoadingScreenTimeMs { get; private set; }
public AreaLoadingState(MemoryContext ctx) : base(ctx) { } public AreaLoading(MemoryContext ctx) : base(ctx) { }
protected override bool ReadData() protected override bool ReadData()
{ {

View file

@ -0,0 +1,57 @@
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads AreaTemplate fields from WorldData → WorldAreaDetailsPtr.
/// ExileCore layout: RawName, Name, Act, IsTown, HasWaypoint, MonsterLevel, WorldAreaId.
/// </summary>
public sealed class AreaTemplate : RemoteObject
{
private readonly MsvcStringReader _strings;
public string? RawName { get; private set; }
public string? Name { get; private set; }
public int Act { get; private set; }
public bool IsTown { get; private set; }
public bool HasWaypoint { get; private set; }
public int MonsterLevel { get; private set; }
public int WorldAreaId { get; private set; }
public AreaTemplate(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
{
_strings = strings;
}
protected override bool ReadData()
{
var mem = Ctx.Memory;
var o = Ctx.Offsets;
// RawName: ptr → wchar_t*
var rawNamePtr = mem.ReadPointer(Address + o.AreaTemplateRawNameOffset);
RawName = _strings.ReadNullTermWString(rawNamePtr);
// Name: ptr → wchar_t*
var namePtr = mem.ReadPointer(Address + o.AreaTemplateNameOffset);
Name = _strings.ReadNullTermWString(namePtr);
// Scalar fields
Act = mem.Read<int>(Address + o.AreaTemplateActOffset);
IsTown = mem.Read<byte>(Address + o.AreaTemplateIsTownOffset) == 1;
HasWaypoint = mem.Read<byte>(Address + o.AreaTemplateHasWaypointOffset) == 1;
MonsterLevel = mem.Read<int>(Address + o.AreaTemplateMonsterLevelOffset);
WorldAreaId = mem.Read<int>(Address + o.AreaTemplateWorldAreaIdOffset);
return true;
}
protected override void Clear()
{
RawName = null;
Name = null;
Act = 0;
IsTown = false;
HasWaypoint = false;
MonsterLevel = 0;
WorldAreaId = 0;
}
}

View file

@ -0,0 +1,391 @@
using Roboto.Memory;
using Roboto.GameOffsets.Components;
using Roboto.GameOffsets.Entities;
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads entity list from AreaInstance's std::map red-black tree.
/// RemoteObject wrapping the previous EntityReader logic.
/// Update(areaInstanceAddr) to read entities.
/// </summary>
public sealed class EntityList : RemoteObject
{
private readonly ComponentReader _components;
private readonly MsvcStringReader _strings;
public List<Entity>? Entities { get; private set; }
public EntityList(MemoryContext ctx, ComponentReader components, MsvcStringReader strings)
: base(ctx)
{
_components = components;
_strings = strings;
}
/// <summary>
/// Reads entities. Requires EntityCount to be set before calling Update().
/// </summary>
public int ExpectedCount { get; set; }
protected override bool ReadData()
{
var mem = Ctx.Memory;
var offsets = Ctx.Offsets;
var registry = Ctx.Registry;
var sentinel = mem.ReadPointer(Address + offsets.EntityListOffset);
if (sentinel == 0) { Entities = null; return true; }
var root = mem.ReadPointer(sentinel + offsets.EntityNodeParentOffset);
var entities = new List<Entity>();
var maxNodes = Math.Min(ExpectedCount + 10, 500);
var hasComponentLookup = offsets.ComponentLookupEntrySize > 0;
var dirty = false;
WalkTreeInOrder(sentinel, root, maxNodes, (_, treeNode) =>
{
var entityPtr = treeNode.Data.EntityPtr;
if (entityPtr == 0) return;
var high = (ulong)entityPtr >> 32;
if (high == 0 || high >= 0x7FFF || (entityPtr & 0x3) != 0) return;
var entityId = treeNode.Data.Key.EntityId;
var path = TryReadEntityPath(entityPtr);
if (IsDoodadPath(path)) return;
var entity = new Entity(entityPtr, entityId, path);
entity.Type = ClassifyType(path);
if (registry["entities"].Register(entity.Metadata))
dirty = true;
if (TryReadEntityPosition(entityPtr, out var x, out var y, out var z))
{
entity.HasPosition = true;
entity.X = x;
entity.Y = y;
entity.Z = z;
}
if (hasComponentLookup && !IsLowPriorityPath(entity.Type))
{
var lookup = _components.ReadComponentLookup(entityPtr);
if (lookup is not null)
{
entity.Components = new HashSet<string>(lookup.Keys);
ReclassifyFromComponents(entity);
if (registry["components"].Register(lookup.Keys))
dirty = true;
var (compFirst, compCount) = _components.FindComponentList(entityPtr);
if (lookup.TryGetValue("Targetable", out var targetIdx) && targetIdx >= 0 && targetIdx < compCount)
{
var targetComp = mem.ReadPointer(compFirst + targetIdx * 8);
if (targetComp != 0)
{
var targetable = mem.Read<Targetable>(targetComp);
entity.IsTargetable = targetable.IsTargetable != 0;
}
}
if (entity.Components.Contains("Monster"))
{
if (lookup.TryGetValue("Life", out var lifeIdx) && lifeIdx >= 0 && lifeIdx < compCount)
{
var lifeComp = mem.ReadPointer(compFirst + lifeIdx * 8);
if (lifeComp != 0)
{
var life = mem.Read<Life>(lifeComp);
if (life.Health.Total > 0 && life.Health.Total < 200000 &&
life.Health.Current >= 0 && life.Health.Current <= life.Health.Total + 1000)
{
entity.HasVitals = true;
entity.LifeCurrent = life.Health.Current;
entity.LifeTotal = life.Health.Total;
}
}
}
if (lookup.TryGetValue("Actor", out var actorIdx) && actorIdx >= 0 && actorIdx < compCount)
{
var actorComp = mem.ReadPointer(compFirst + actorIdx * 8);
if (actorComp != 0)
{
var animId = mem.Read<int>(actorComp + ActorOffsets.AnimationId);
entity.ActionId = (short)(animId & 0xFFFF);
}
}
if (lookup.TryGetValue("Mods", out var modsIdx) && modsIdx >= 0 && modsIdx < compCount)
{
var modsComp = mem.ReadPointer(compFirst + modsIdx * 8);
if (modsComp != 0)
ReadEntityMods(entity, modsComp);
}
}
if (entity.Components.Contains("AreaTransition") &&
lookup.TryGetValue("AreaTransition", out var atIdx) && atIdx >= 0 && atIdx < compCount)
{
var atComp = mem.ReadPointer(compFirst + atIdx * 8);
if (atComp != 0)
entity.TransitionName = ReadAreaTransitionName(atComp);
}
if (entity.Components.Contains("Transitionable") &&
lookup.TryGetValue("Transitionable", out var trIdx) && trIdx >= 0 && trIdx < compCount)
{
var trComp = mem.ReadPointer(compFirst + trIdx * 8);
if (trComp != 0)
{
var tr = mem.Read<Transitionable>(trComp);
entity.TransitionState = tr.CurrentStateEnum;
}
}
}
}
entities.Add(entity);
});
if (dirty)
registry.Flush();
Entities = entities;
return true;
}
protected override void Clear()
{
Entities = null;
ExpectedCount = 0;
}
// ── Tree walking ─────────────────────────────────────────────────────
public void WalkTreeInOrder(nint sentinel, nint root, int maxNodes, Action<nint> visitor)
{
WalkTreeInOrder(sentinel, root, maxNodes, (addr, _) => visitor(addr));
}
public void WalkTreeInOrder(nint sentinel, nint root, int maxNodes, Action<nint, EntityTreeNode> visitor)
{
if (root == 0 || root == sentinel) return;
var mem = Ctx.Memory;
var stack = new Stack<(nint Addr, EntityTreeNode Node)>();
var current = root;
var count = 0;
var visited = new HashSet<nint> { sentinel };
while ((current != sentinel && current != 0) || stack.Count > 0)
{
while (current != sentinel && current != 0)
{
if (!visited.Add(current))
{
current = sentinel;
break;
}
var node = mem.Read<EntityTreeNode>(current);
stack.Push((current, node));
current = node.Left;
}
if (stack.Count == 0) break;
var (nodeAddr, treeNode) = stack.Pop();
visitor(nodeAddr, treeNode);
count++;
if (count >= maxNodes) break;
current = treeNode.Right;
}
}
// ── Helpers ───────────────────────────────────────────────────────────
public string? TryReadEntityPath(nint entity)
{
var mem = Ctx.Memory;
var offsets = Ctx.Offsets;
var detailsPtr = mem.ReadPointer(entity + offsets.EntityDetailsOffset);
if (detailsPtr == 0) return null;
var high = (ulong)detailsPtr >> 32;
if (high == 0 || high >= 0x7FFF) return null;
return _strings.ReadMsvcWString(detailsPtr + offsets.EntityPathStringOffset);
}
public bool TryReadEntityPosition(nint entity, out float x, out float y, out float z)
{
x = y = z = 0;
var offsets = Ctx.Offsets;
var (compFirst, count) = _components.FindComponentList(entity);
if (count <= 0) return false;
if (offsets.RenderComponentIndex >= 0 && offsets.RenderComponentIndex < count)
{
var renderComp = Ctx.Memory.ReadPointer(compFirst + offsets.RenderComponentIndex * 8);
if (renderComp != 0 && _components.TryReadPositionRaw(renderComp, out x, out y, out z))
return true;
}
var scanLimit = Math.Min(count, 20);
for (var i = 0; i < scanLimit; i++)
{
var compPtr = Ctx.Memory.ReadPointer(compFirst + i * 8);
if (compPtr == 0) continue;
var high = (ulong)compPtr >> 32;
if (high == 0 || high >= 0x7FFF) continue;
if ((compPtr & 0x3) != 0) continue;
if (_components.TryReadPositionRaw(compPtr, out x, out y, out z))
return true;
}
return false;
}
private string? ReadAreaTransitionName(nint atComp)
{
var mem = Ctx.Memory;
var data = mem.ReadBytes(atComp, 0x80);
if (data is null) return null;
for (var off = 0x10; off + 8 <= data.Length; off += 8)
{
var ptr = (nint)BitConverter.ToInt64(data, off);
if (ptr == 0) continue;
if (((ulong)ptr >> 32) is 0 or >= 0x7FFF) continue;
var raw = _strings.ReadNullTermWString(ptr);
if (raw is not null && raw.Length >= 3)
return raw;
var inner = mem.ReadPointer(ptr);
if (inner != 0 && ((ulong)inner >> 32) is > 0 and < 0x7FFF)
{
raw = _strings.ReadNullTermWString(inner);
if (raw is not null && raw.Length >= 3)
return raw;
}
}
return null;
}
private void ReadEntityMods(Entity entity, nint modsComp)
{
var mem = Ctx.Memory;
var mods = mem.Read<Mods>(modsComp);
if (mods.ObjectMagicPropertiesPtr != 0 &&
((ulong)mods.ObjectMagicPropertiesPtr >> 32) is > 0 and < 0x7FFF)
{
var props = mem.Read<ObjectMagicProperties>(mods.ObjectMagicPropertiesPtr);
if (props.Rarity is >= 0 and <= 3)
entity.Rarity = props.Rarity;
}
if (mods.AllModsPtr == 0 || ((ulong)mods.AllModsPtr >> 32) is 0 or >= 0x7FFF)
return;
var allMods = mem.Read<AllModsType>(mods.AllModsPtr);
var explicitCount = (int)allMods.ExplicitMods.TotalElements(16);
if (explicitCount <= 0 || explicitCount > 20) return;
var modNames = new List<string>();
for (var i = 0; i < explicitCount; i++)
{
var modEntry = mem.Read<ModArrayStruct>(allMods.ExplicitMods.First + i * 16);
if (modEntry.ModPtr == 0) continue;
if (((ulong)modEntry.ModPtr >> 32) is 0 or >= 0x7FFF) continue;
var name = _strings.ReadNullTermWString(modEntry.ModPtr);
if (name is not null)
{
modNames.Add(name);
continue;
}
name = _strings.ReadMsvcWString(modEntry.ModPtr);
if (name is not null)
modNames.Add(name);
}
if (modNames.Count > 0)
entity.ModNames = modNames;
}
// ── Classification helpers ───────────────────────────────────────────
private static bool IsDoodadPath(string? path)
{
if (path is null) return false;
return path.Contains("Doodad", StringComparison.OrdinalIgnoreCase);
}
private static bool IsLowPriorityPath(EntityType type)
=> type is EntityType.Effect or EntityType.Terrain or EntityType.Critter;
private static EntityType ClassifyType(string? path)
{
if (path is null) return EntityType.Unknown;
var firstSlash = path.IndexOf('/');
if (firstSlash < 0) return EntityType.Unknown;
var secondSlash = path.IndexOf('/', firstSlash + 1);
var category = secondSlash > 0
? path[(firstSlash + 1)..secondSlash]
: path[(firstSlash + 1)..];
switch (category)
{
case "Characters": return EntityType.Player;
case "Monsters":
if (path.Contains("/Critters/", StringComparison.OrdinalIgnoreCase)) return EntityType.Critter;
if (path.Contains("/NPC/", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/TownNPC/", StringComparison.OrdinalIgnoreCase)) return EntityType.Npc;
return EntityType.Monster;
case "NPC": return EntityType.Npc;
case "Effects": return EntityType.Effect;
case "MiscellaneousObjects":
if (path.Contains("/Chest", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/Stash", StringComparison.OrdinalIgnoreCase)) return EntityType.Chest;
if (path.Contains("/Shrine", StringComparison.OrdinalIgnoreCase)) return EntityType.Shrine;
if (path.Contains("/Portal", StringComparison.OrdinalIgnoreCase)) return EntityType.Portal;
return EntityType.MiscellaneousObject;
case "Terrain": return EntityType.Terrain;
case "Items": return EntityType.WorldItem;
default: return EntityType.Unknown;
}
}
private static void ReclassifyFromComponents(Entity entity)
{
var components = entity.Components;
if (components is null || components.Count == 0) return;
if (components.Contains("Monster")) { entity.Type = EntityType.Monster; return; }
if (components.Contains("Chest")) { entity.Type = EntityType.Chest; return; }
if (components.Contains("Shrine")) { entity.Type = EntityType.Shrine; return; }
if (components.Contains("Waypoint")) { entity.Type = EntityType.Waypoint; return; }
if (components.Contains("AreaTransition")) { entity.Type = EntityType.AreaTransition; return; }
if (components.Contains("Portal")) { entity.Type = EntityType.Portal; return; }
if (components.Contains("TownPortal")) { entity.Type = EntityType.TownPortal; return; }
if (components.Contains("NPC")) { entity.Type = EntityType.Npc; return; }
if (components.Contains("Player")) { entity.Type = EntityType.Player; return; }
}
}

View file

@ -1,4 +1,4 @@
namespace Roboto.Memory.States; namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Game state types by slot index. Order must match the state array in the controller. /// Game state types by slot index. Order must match the state array in the controller.

View file

@ -1,4 +1,6 @@
namespace Roboto.Memory.States; using Roboto.Memory;
namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Root state orchestrator. Reads controller from GameStateBase, resolves state slot pointers, /// Root state orchestrator. Reads controller from GameStateBase, resolves state slot pointers,
@ -16,17 +18,17 @@ public sealed class GameStates
public int StatesCount { get; private set; } public int StatesCount { get; private set; }
public GameStateType CurrentState { get; private set; } = GameStateType.GameNotLoaded; public GameStateType CurrentState { get; private set; } = GameStateType.GameNotLoaded;
public IReadOnlyDictionary<nint, GameStateType> AllStates => _allStates; public IReadOnlyDictionary<nint, GameStateType> AllStates => _allStates;
public AreaLoadingState AreaLoading { get; } public AreaLoading AreaLoading { get; }
public InGameStateReader InGame { get; } public InGameState InGame { get; }
/// <summary>Raw qwords from controller 0x00-0x48 (before state slots), for UI diagnostics.</summary> /// <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 (int Offset, nint Value, string? Match, bool Changed, string? DerefInfo)[] ControllerPreSlots { get; private set; } = [];
public GameStates(MemoryContext ctx) public GameStates(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
{ {
_ctx = ctx; _ctx = ctx;
AreaLoading = new AreaLoadingState(ctx); AreaLoading = new AreaLoading(ctx);
InGame = new InGameStateReader(ctx); InGame = new InGameState(ctx, components, strings, questNames);
} }
/// <summary> /// <summary>

View file

@ -1,24 +1,25 @@
using Roboto.GameOffsets.States; using Roboto.Memory;
using IgsStruct = Roboto.GameOffsets.States.InGameState;
namespace Roboto.Memory.States; namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Reads InGameState struct (784B, 1 RPM instead of 4 individual reads). /// 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 AreaInstance and WorldData children.
/// Cascades to AreaInstanceState and WorldDataState children.
/// </summary> /// </summary>
public sealed class InGameStateReader : RemoteObject public sealed class InGameState : RemoteObject
{ {
private InGameState _data; private IgsStruct _data;
public bool IsEscapeOpen { get; private set; } public bool IsEscapeOpen { get; private set; }
public AreaInstanceState AreaInstance { get; } public AreaInstance AreaInstance { get; }
public WorldDataState WorldData { get; } public WorldData WorldData { get; }
public InGameStateReader(MemoryContext ctx) : base(ctx) public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
: base(ctx)
{ {
AreaInstance = new AreaInstanceState(ctx); AreaInstance = new AreaInstance(ctx, components, strings, questNames);
WorldData = new WorldDataState(ctx); WorldData = new WorldData(ctx, strings);
} }
protected override bool ReadData() protected override bool ReadData()
@ -26,7 +27,7 @@ public sealed class InGameStateReader : RemoteObject
var mem = Ctx.Memory; var mem = Ctx.Memory;
// Read the full InGameState struct (0x310 = 784 bytes, 1 RPM) // Read the full InGameState struct (0x310 = 784 bytes, 1 RPM)
_data = mem.Read<InGameState>(Address); _data = mem.Read<IgsStruct>(Address);
// Escape state // Escape state
IsEscapeOpen = _data.EscapeStateFlag != 0; IsEscapeOpen = _data.EscapeStateFlag != 0;

View file

@ -0,0 +1,235 @@
using Roboto.Memory;
using Roboto.GameOffsets.Components;
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads active skills from the local player's Actor component.
/// RemoteObject wrapping the previous SkillReader logic.
/// Update(localPlayerPtr) to read skills.
/// </summary>
public sealed class PlayerSkills : RemoteObject
{
private readonly ComponentReader _components;
private readonly MsvcStringReader _strings;
// Name cache — skill names are static per area, only refresh on actor change
private readonly Dictionary<nint, string?> _nameCache = new();
private nint _lastActorComp;
public List<SkillSnapshot>? Skills { get; private set; }
/// <summary>PSD pointer for skill bar ID matching. Set before calling Update().</summary>
public nint PsdPtr { get; set; }
public PlayerSkills(MemoryContext ctx, ComponentReader components, MsvcStringReader strings)
: base(ctx)
{
_components = components;
_strings = strings;
}
protected override bool ReadData()
{
if (Address == 0) { Skills = null; return false; }
var mem = Ctx.Memory;
var actorComp = _components.GetComponentAddress(Address, "Actor");
if (actorComp == 0) { Skills = null; return true; }
// Invalidate name cache if actor component address changed (area transition)
if (actorComp != _lastActorComp)
{
_nameCache.Clear();
_lastActorComp = actorComp;
}
// Read SkillBarIds from PSD if offset is configured
var skillBarIds = ReadSkillBarIds(PsdPtr);
// Read ActiveSkills vector at Actor+0xB00
var vecFirst = mem.ReadPointer(actorComp + ActorOffsets.ActiveSkillsVector);
var vecLast = mem.ReadPointer(actorComp + ActorOffsets.ActiveSkillsVector + 8);
if (vecFirst == 0 || vecLast <= vecFirst) { Skills = null; return true; }
var totalBytes = (int)(vecLast - vecFirst);
const int entrySize = 0x10;
var entryCount = totalBytes / entrySize;
if (entryCount <= 0 || entryCount > 128) { Skills = null; return true; }
var vecData = mem.ReadBytes(vecFirst, totalBytes);
if (vecData is null) { Skills = null; return true; }
var cooldowns = ReadCooldowns(actorComp);
var result = new List<SkillSnapshot>();
var seen = new HashSet<uint>();
for (var i = 0; i < entryCount; i++)
{
var activeSkillPtr = (nint)BitConverter.ToInt64(vecData, i * entrySize);
if (activeSkillPtr == 0) continue;
var high = (ulong)activeSkillPtr >> 32;
if (high == 0 || high >= 0x7FFF) continue;
var details = mem.Read<ActorSkillDetails>(activeSkillPtr - 0x10);
var name = ResolveSkillName(activeSkillPtr, details);
if (name is null) continue;
if (!seen.Add(details.UnknownIdAndEquipmentInfo)) continue;
var id = (ushort)(details.UnknownIdAndEquipmentInfo & 0xFFFF);
var id2 = (ushort)(details.UnknownIdAndEquipmentInfo >> 16);
var slot = -1;
if (skillBarIds is not null)
{
for (var s = 0; s < skillBarIds.Length; s++)
{
if (skillBarIds[s].Id == id && skillBarIds[s].Id2 == id2)
{
slot = s;
break;
}
}
}
var canBeUsed = true;
var activeCooldowns = 0;
var cdMaxUses = 0;
if (cooldowns is not null)
{
foreach (var (cd, _) in cooldowns)
{
if (cd.UnknownIdAndEquipmentInfo == details.UnknownIdAndEquipmentInfo)
{
canBeUsed = !cd.CannotBeUsed;
activeCooldowns = cd.TotalActiveCooldowns;
cdMaxUses = cd.MaxUses;
break;
}
}
}
var rawBytes = mem.ReadBytes(activeSkillPtr - 0x10, 0xC0);
result.Add(new SkillSnapshot
{
Name = name,
InternalName = name,
Address = activeSkillPtr - 0x10,
RawBytes = rawBytes,
CanBeUsed = canBeUsed,
UseStage = details.UseStage,
CastType = details.CastType,
TotalUses = details.TotalUses,
CooldownTimeMs = details.TotalCooldownTimeInMs,
ActiveCooldowns = activeCooldowns,
MaxUses = cdMaxUses,
Id = id,
Id2 = id2,
SkillBarSlot = slot,
});
}
Skills = result;
return true;
}
protected override void Clear()
{
Skills = null;
PsdPtr = 0;
_nameCache.Clear();
_lastActorComp = 0;
}
private (ushort Id, ushort Id2)[]? ReadSkillBarIds(nint psdPtr)
{
var offset = Ctx.Offsets.SkillBarIdsOffset;
if (offset <= 0 || psdPtr == 0) return null;
const int slotCount = 13;
const int bufferSize = slotCount * 4;
var data = Ctx.Memory.ReadBytes(psdPtr + offset, bufferSize);
if (data is null) return null;
var slots = new (ushort Id, ushort Id2)[slotCount];
for (var i = 0; i < slotCount; i++)
{
var off = i * 4;
slots[i] = (
BitConverter.ToUInt16(data, off),
BitConverter.ToUInt16(data, off + 2)
);
}
return slots;
}
private List<(ActorSkillCooldown Cd, nint FirstPtr)>? ReadCooldowns(nint actorComp)
{
var mem = Ctx.Memory;
var cdFirst = mem.ReadPointer(actorComp + ActorOffsets.CooldownsVector);
var cdLast = mem.ReadPointer(actorComp + ActorOffsets.CooldownsVector + 8);
if (cdFirst == 0 || cdLast <= cdFirst) return null;
var totalBytes = (int)(cdLast - cdFirst);
const int cdEntrySize = 0x48;
var count = totalBytes / cdEntrySize;
if (count <= 0 || count > 64) return null;
var result = new List<(ActorSkillCooldown, nint)>(count);
for (var i = 0; i < count; i++)
{
var cd = mem.Read<ActorSkillCooldown>(cdFirst + i * cdEntrySize);
result.Add((cd, cd.CooldownsList.First));
}
return result;
}
private string? ResolveSkillName(nint activeSkillPtr, ActorSkillDetails details)
{
if (_nameCache.TryGetValue(activeSkillPtr, out var cached))
return cached;
var mem = Ctx.Memory;
string? name = null;
var asDatDirect = details.ActiveSkillsDatPtr;
if (asDatDirect != 0 && ((ulong)asDatDirect >> 32) is > 0 and < 0x7FFF)
name = _strings.ReadNullTermWString(asDatDirect);
if (name is null)
{
var geplPtr = details.GrantedEffectsPerLevelDatRow;
if (geplPtr != 0)
{
var geFk = mem.ReadPointer(geplPtr);
if (geFk != 0 && ((ulong)geFk >> 32) is > 0 and < 0x7FFF)
{
var geData = mem.ReadBytes(geFk, 0xB0);
if (geData is not null)
{
var asDatPtr = (nint)BitConverter.ToInt64(geData, 0x00);
if (asDatPtr != 0 && ((ulong)asDatPtr >> 32) is > 0 and < 0x7FFF)
name = _strings.ReadNullTermWString(asDatPtr);
if (name is null && 0xA8 + 8 <= geData.Length)
{
var nameObjPtr = (nint)BitConverter.ToInt64(geData, 0xA8);
if (nameObjPtr != 0 && ((ulong)nameObjPtr >> 32) is > 0 and < 0x7FFF)
{
var namePtr = mem.ReadPointer(nameObjPtr);
if (namePtr != 0)
name = _strings.ReadNullTermWString(namePtr);
}
}
}
}
}
}
_nameCache[activeSkillPtr] = name;
return name;
}
}

View file

@ -0,0 +1,266 @@
using Roboto.Memory;
using Serilog;
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads quest flags from ServerData → PlayerServerData → QuestFlags.
/// RemoteObject wrapping the previous QuestReader logic.
/// Update(serverDataPtr) to read quests.
/// </summary>
public sealed class QuestFlags : RemoteObject
{
private readonly MsvcStringReader _strings;
private readonly QuestNameLookup? _nameLookup;
private readonly Dictionary<nint, string?> _nameCache = new();
private nint _lastPsd;
public List<QuestSnapshot>? Quests { get; private set; }
public QuestFlags(MemoryContext ctx, MsvcStringReader strings, QuestNameLookup? nameLookup = null)
: base(ctx)
{
_strings = strings;
_nameLookup = nameLookup;
}
protected override bool ReadData()
{
var offsets = Ctx.Offsets;
if (offsets.QuestFlagEntrySize <= 0) { Quests = null; return true; }
var mem = Ctx.Memory;
var psdVecBegin = mem.ReadPointer(Address + offsets.PlayerServerDataOffset);
if (psdVecBegin == 0) { Quests = null; return true; }
var playerServerData = mem.ReadPointer(psdVecBegin);
if (playerServerData == 0) { Quests = null; return true; }
if (playerServerData != _lastPsd)
{
_nameCache.Clear();
_lastPsd = playerServerData;
}
var questFlagsAddr = playerServerData + offsets.QuestFlagsOffset;
if (offsets.QuestFlagsContainerType == "int_vector")
Quests = ReadIntVectorQuests(questFlagsAddr, offsets);
else if (offsets.QuestFlagsContainerType == "vector")
Quests = ReadStructVectorQuests(questFlagsAddr, offsets);
else
Quests = null;
return true;
}
protected override void Clear()
{
Quests = null;
_nameCache.Clear();
_lastPsd = 0;
}
private List<QuestSnapshot>? ReadIntVectorQuests(nint questFlagsAddr, GameOffsets offsets)
{
var mem = Ctx.Memory;
var vecBegin = mem.ReadPointer(questFlagsAddr);
var vecEnd = mem.ReadPointer(questFlagsAddr + 8);
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
var totalBytes = (int)(vecEnd - vecBegin);
var entryCount = totalBytes / 4;
if (entryCount <= 0 || entryCount > offsets.QuestFlagsMaxEntries) return null;
var vecData = mem.ReadBytes(vecBegin, totalBytes);
if (vecData is null) return null;
byte[]? compData = null;
var compEntryCount = 0;
if (offsets.QuestCompanionOffset > 0 && offsets.QuestCompanionEntrySize > 0)
{
var compBegin = mem.ReadPointer(questFlagsAddr + offsets.QuestCompanionOffset);
var compEnd = mem.ReadPointer(questFlagsAddr + offsets.QuestCompanionOffset + 8);
if (compBegin != 0 && compEnd > compBegin)
{
var compBytes = (int)(compEnd - compBegin);
compEntryCount = compBytes / offsets.QuestCompanionEntrySize;
if (compEntryCount > 0 && compBytes < 0x100000)
compData = mem.ReadBytes(compBegin, compBytes);
}
}
var datTableBase = FindDatTableBase(offsets);
var result = new List<QuestSnapshot>(entryCount);
for (var i = 0; i < entryCount; i++)
{
var idx = BitConverter.ToInt32(vecData, i * 4);
string? questName = null;
string? internalId = null;
byte stateId = 0;
bool isTracked = false;
nint questObjPtr = 0;
if (compData is not null && i < compEntryCount)
{
var compOff = i * offsets.QuestCompanionEntrySize;
if (offsets.QuestCompanionTrackedOffset > 0 &&
compOff + offsets.QuestCompanionTrackedOffset + 4 <= compData.Length)
{
var trackedVal = BitConverter.ToUInt32(compData, compOff + offsets.QuestCompanionTrackedOffset);
isTracked = trackedVal == offsets.QuestTrackedMarker;
}
if (compOff + offsets.QuestCompanionObjPtrOffset + 8 <= compData.Length)
{
questObjPtr = (nint)BitConverter.ToInt64(compData, compOff + offsets.QuestCompanionObjPtrOffset);
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF
&& offsets.QuestObjEncounterStateOffset > 0)
{
var stateByte = mem.ReadBytes(questObjPtr + offsets.QuestObjEncounterStateOffset, 1);
if (stateByte is { Length: 1 })
stateId = stateByte[0];
}
}
}
if (datTableBase != 0 && offsets.QuestDatRowSize > 0)
{
var rowAddr = datTableBase + idx * offsets.QuestDatRowSize;
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
}
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
{
questName = entry?.Name;
internalId = entry?.InternalId;
}
result.Add(new QuestSnapshot
{
QuestStateIndex = idx,
QuestDatPtr = questObjPtr,
QuestName = questName,
InternalId = internalId,
StateId = stateId,
IsTracked = isTracked,
});
}
return result;
}
private nint FindDatTableBase(GameOffsets offsets)
{
if (offsets.QuestDatRowSize <= 0) return 0;
return 0;
}
private string? ResolveDatString(nint fieldAddr)
{
if (_nameCache.TryGetValue(fieldAddr, out var cached))
return cached;
var mem = Ctx.Memory;
var strPtr = mem.ReadPointer(fieldAddr);
string? result = null;
if (strPtr != 0 && ((ulong)strPtr >> 32) is > 0 and < 0x7FFF)
result = _strings.ReadNullTermWString(strPtr);
_nameCache[fieldAddr] = result;
return result;
}
private List<QuestSnapshot>? ReadStructVectorQuests(nint questFlagsAddr, GameOffsets offsets)
{
var mem = Ctx.Memory;
var vecBegin = mem.ReadPointer(questFlagsAddr);
var vecEnd = mem.ReadPointer(questFlagsAddr + 8);
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
var totalBytes = (int)(vecEnd - vecBegin);
var entrySize = offsets.QuestFlagEntrySize;
var entryCount = totalBytes / entrySize;
if (entryCount <= 0 || entryCount > offsets.QuestFlagsMaxEntries) return null;
var vecData = mem.ReadBytes(vecBegin, totalBytes);
if (vecData is null) return null;
var result = new List<QuestSnapshot>(entryCount);
for (var i = 0; i < entryCount; i++)
{
var entryOffset = i * entrySize;
nint questDatPtr = 0;
if (entryOffset + offsets.QuestEntryQuestPtrOffset + 8 <= vecData.Length)
questDatPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryQuestPtrOffset);
byte stateId = 0;
if (entryOffset + offsets.QuestEntryStateIdOffset < vecData.Length)
stateId = vecData[entryOffset + offsets.QuestEntryStateIdOffset];
var questName = ResolveQuestName(questDatPtr);
string? stateText = null;
if (offsets.QuestEntryStateTextOffset > 0 &&
entryOffset + offsets.QuestEntryStateTextOffset + 8 <= vecData.Length)
{
var stateTextPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryStateTextOffset);
if (stateTextPtr != 0 && ((ulong)stateTextPtr >> 32) is > 0 and < 0x7FFF)
stateText = _strings.ReadNullTermWString(stateTextPtr);
}
string? progressText = null;
if (offsets.QuestEntryProgressTextOffset > 0 &&
entryOffset + offsets.QuestEntryProgressTextOffset + 8 <= vecData.Length)
{
var progressTextPtr = (nint)BitConverter.ToInt64(vecData, entryOffset + offsets.QuestEntryProgressTextOffset);
if (progressTextPtr != 0 && ((ulong)progressTextPtr >> 32) is > 0 and < 0x7FFF)
progressText = _strings.ReadNullTermWString(progressTextPtr);
}
result.Add(new QuestSnapshot
{
QuestDatPtr = questDatPtr,
QuestName = questName,
StateId = stateId,
StateText = stateText,
ProgressText = progressText,
});
}
return result;
}
private string? ResolveQuestName(nint questDatPtr)
{
if (questDatPtr == 0) return null;
if (_nameCache.TryGetValue(questDatPtr, out var cached))
return cached;
var mem = Ctx.Memory;
string? name = null;
var high = (ulong)questDatPtr >> 32;
if (high is > 0 and < 0x7FFF)
{
var namePtr = mem.ReadPointer(questDatPtr);
if (namePtr != 0)
name = _strings.ReadNullTermWString(namePtr);
}
_nameCache[questDatPtr] = name;
return name;
}
}

View file

@ -0,0 +1,176 @@
using Roboto.Memory;
using Serilog;
using TerrainStruct = Roboto.GameOffsets.States.Terrain;
namespace Roboto.Memory.Objects;
/// <summary>
/// Reads terrain walkability grid from AreaInstance.
/// RemoteObject wrapping the previous TerrainReader logic.
/// Update(areaInstanceAddr) to read terrain.
/// </summary>
public sealed class Terrain : RemoteObject
{
private uint _cachedTerrainAreaHash;
private WalkabilityGrid? _cachedTerrain;
private bool _wasLoading;
public WalkabilityGrid? Grid { get; private set; }
public int TerrainWidth { get; private set; }
public int TerrainHeight { get; private set; }
public int TerrainCols { get; private set; }
public int TerrainRows { get; private set; }
public int WalkablePercent { get; private set; }
/// <summary>Set before Update() to indicate current loading/area state.</summary>
public bool IsLoading { get; set; }
public uint AreaHash { get; set; }
public Terrain(MemoryContext ctx) : base(ctx) { }
public void InvalidateCache()
{
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
}
protected override bool ReadData()
{
var mem = Ctx.Memory;
var offsets = Ctx.Offsets;
if (!offsets.TerrainInline)
{
var terrainListPtr = mem.ReadPointer(Address + offsets.TerrainListOffset);
if (terrainListPtr == 0) return true;
var terrainPtr = mem.ReadPointer(terrainListPtr);
if (terrainPtr == 0) return true;
var dimsPtr = mem.ReadPointer(terrainPtr + offsets.TerrainDimensionsOffset);
if (dimsPtr == 0) return true;
TerrainCols = mem.Read<int>(dimsPtr);
TerrainRows = mem.Read<int>(dimsPtr + 4);
if (TerrainCols > 0 && TerrainCols < 1000 &&
TerrainRows > 0 && TerrainRows < 1000)
{
TerrainWidth = TerrainCols * offsets.SubTilesPerCell;
TerrainHeight = TerrainRows * offsets.SubTilesPerCell;
}
else
{
TerrainCols = 0;
TerrainRows = 0;
}
return true;
}
// Inline mode
var terrainBase = Address + offsets.TerrainListOffset;
var t = mem.Read<TerrainStruct>(terrainBase);
var cols = (int)t.Dimensions.X;
var rows = (int)t.Dimensions.Y;
if (cols <= 0 || cols >= 1000 || rows <= 0 || rows >= 1000)
return true;
TerrainCols = cols;
TerrainRows = rows;
TerrainWidth = cols * offsets.SubTilesPerCell;
TerrainHeight = rows * offsets.SubTilesPerCell;
if (IsLoading)
{
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
Grid = null;
return true;
}
if (_wasLoading)
{
_cachedTerrain = null;
_cachedTerrainAreaHash = 0;
}
if (_cachedTerrain != null && _cachedTerrainAreaHash == AreaHash)
{
Grid = _cachedTerrain;
WalkablePercent = CalcWalkablePercent(_cachedTerrain);
return true;
}
var gridBegin = t.WalkableGrid.First;
var gridEnd = t.WalkableGrid.Last;
if (gridBegin == 0 || gridEnd <= gridBegin)
return true;
var gridDataSize = (int)(gridEnd - gridBegin);
if (gridDataSize <= 0 || gridDataSize > 16 * 1024 * 1024)
return true;
var bytesPerRow = t.BytesPerRow;
if (bytesPerRow <= 0 || bytesPerRow > 0x10000)
return true;
var gridWidth = cols * offsets.SubTilesPerCell;
var gridHeight = rows * offsets.SubTilesPerCell;
var rawData = mem.ReadBytes(gridBegin, gridDataSize);
if (rawData is null)
return true;
var data = new byte[gridWidth * gridHeight];
for (var row = 0; row < gridHeight; row++)
{
var rowStart = row * bytesPerRow;
for (var col = 0; col < gridWidth; col++)
{
var byteIndex = rowStart + col / 2;
if (byteIndex >= rawData.Length) break;
data[row * gridWidth + col] = (col % 2 == 0)
? (byte)(rawData[byteIndex] & 0x0F)
: (byte)((rawData[byteIndex] >> 4) & 0x0F);
}
}
var grid = new WalkabilityGrid(gridWidth, gridHeight, data);
Grid = grid;
WalkablePercent = CalcWalkablePercent(grid);
_cachedTerrain = grid;
_cachedTerrainAreaHash = AreaHash;
Log.Information("Terrain grid read: {W}x{H} ({Cols}x{Rows} cells), {Pct}% walkable",
gridWidth, gridHeight, cols, rows, WalkablePercent);
return true;
}
/// <summary>Update loading edge state. Call after ReadData().</summary>
public void UpdateLoadingEdge(bool isLoading)
{
_wasLoading = isLoading;
}
protected override void Clear()
{
Grid = null;
TerrainWidth = 0;
TerrainHeight = 0;
TerrainCols = 0;
TerrainRows = 0;
WalkablePercent = 0;
}
public static int CalcWalkablePercent(WalkabilityGrid grid)
{
var walkable = 0;
for (var i = 0; i < grid.Data.Length; i++)
if (grid.Data[i] != 0) walkable++;
return grid.Data.Length > 0 ? (int)(100L * walkable / grid.Data.Length) : 0;
}
}

View file

@ -1,17 +1,19 @@
using System.Numerics; using System.Numerics;
using Roboto.GameOffsets.States; using Roboto.Memory;
using WdStruct = Roboto.GameOffsets.States.WorldData;
namespace Roboto.Memory.States; namespace Roboto.Memory.Objects;
/// <summary> /// <summary>
/// Reads WorldData struct (168B, 1 RPM) and resolves the camera matrix. /// Reads WorldData struct (168B, 1 RPM) and resolves the camera matrix.
/// Primary camera source: WorldData.CameraPtr. Fallback: InGameState.CameraPtr (set via FallbackCameraPtr). /// Primary camera source: WorldData.CameraPtr. Fallback: InGameState.CameraPtr (set via FallbackCameraPtr).
/// Owns AreaTemplate child for area metadata.
/// </summary> /// </summary>
public sealed class WorldDataState : RemoteObject public sealed class WorldData : RemoteObject
{ {
private WorldData _data; private WdStruct _data;
/// <summary>Camera pointer from InGameState, set by InGameStateReader before Update() is called.</summary> /// <summary>Camera pointer from InGameState, set by InGameState before Update() is called.</summary>
public nint FallbackCameraPtr { get; set; } public nint FallbackCameraPtr { get; set; }
public Matrix4x4? CameraMatrix { get; private set; } public Matrix4x4? CameraMatrix { get; private set; }
@ -19,7 +21,12 @@ public sealed class WorldDataState : RemoteObject
/// <summary>Resolved address of the camera matrix for hot-path caching.</summary> /// <summary>Resolved address of the camera matrix for hot-path caching.</summary>
public nint CameraMatrixAddress { get; private set; } public nint CameraMatrixAddress { get; private set; }
public WorldDataState(MemoryContext ctx) : base(ctx) { } public AreaTemplate AreaTemplate { get; }
public WorldData(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
{
AreaTemplate = new AreaTemplate(ctx, strings);
}
protected override bool ReadData() protected override bool ReadData()
{ {
@ -27,7 +34,13 @@ public sealed class WorldDataState : RemoteObject
var offsets = Ctx.Offsets; var offsets = Ctx.Offsets;
// Read the full WorldData struct (0xA8 = 168 bytes, 1 RPM) // Read the full WorldData struct (0xA8 = 168 bytes, 1 RPM)
_data = mem.Read<WorldData>(Address); _data = mem.Read<WdStruct>(Address);
// Cascade to AreaTemplate
if (_data.WorldAreaDetailsPtr != 0)
AreaTemplate.Update(_data.WorldAreaDetailsPtr);
else
AreaTemplate.Reset();
// Resolve camera: primary from WorldData, fallback from InGameState // Resolve camera: primary from WorldData, fallback from InGameState
if (offsets.CameraMatrixOffset <= 0) if (offsets.CameraMatrixOffset <= 0)
@ -56,5 +69,6 @@ public sealed class WorldDataState : RemoteObject
FallbackCameraPtr = 0; FallbackCameraPtr = 0;
CameraMatrix = null; CameraMatrix = null;
CameraMatrixAddress = 0; CameraMatrixAddress = 0;
AreaTemplate.Reset();
} }
} }

View file

@ -1,4 +1,4 @@
namespace Roboto.Memory.States; namespace Roboto.Memory;
/// <summary> /// <summary>
/// Base class for state objects that read a section of game memory. /// Base class for state objects that read a section of game memory.

View file

@ -1,5 +1,5 @@
using System.Numerics; using System.Numerics;
using Roboto.Memory.States; using Roboto.Memory.Objects;
namespace Roboto.Memory; namespace Roboto.Memory;
@ -29,6 +29,15 @@ public class GameStateSnapshot
public int AreaLevel; public int AreaLevel;
public uint AreaHash; public uint AreaHash;
// Area template (from WorldData → AreaTemplate)
public string? AreaRawName;
public string? AreaName;
public int AreaAct;
public bool AreaIsTown;
public bool AreaHasWaypoint;
public int AreaMonsterLevel;
public int WorldAreaId;
// Player // Player
public string? CharacterName; public string? CharacterName;

View file

@ -0,0 +1,21 @@
namespace Roboto.Memory;
/// <summary>
/// Lightweight quest data from ServerData quest flags.
/// Stored in GameStateSnapshot; mapped to Roboto.Core.QuestProgress in the Data layer.
/// </summary>
public sealed class QuestSnapshot
{
/// <summary>QuestState.dat row index (int_vector mode) or 0 (pointer mode).</summary>
public int QuestStateIndex { get; init; }
public nint QuestDatPtr { get; init; }
public string? QuestName { get; init; }
/// <summary>Internal quest ID from dat row (e.g. "TreeOfSouls2", "IncursionQuest1_Act1").</summary>
public string? InternalId { get; init; }
/// <summary>Encounter state from quest state object: 1=locked/not encountered, 2=available/started.</summary>
public byte StateId { get; init; }
/// <summary>True if this quest is the currently tracked/active quest in the UI.</summary>
public bool IsTracked { get; init; }
public string? StateText { get; init; }
public string? ProgressText { get; init; }
}

View file

@ -0,0 +1,32 @@
namespace Roboto.Memory;
/// <summary>
/// Lightweight skill data from the Actor component's ActiveSkills vector.
/// Stored in GameStateSnapshot; mapped to Roboto.Core.SkillState in the Data layer.
/// </summary>
public sealed class SkillSnapshot
{
public string? Name { get; init; }
public string? InternalName { get; init; }
/// <summary>Address of ActiveSkillPtr in game memory (for CE inspection).</summary>
public nint Address { get; init; }
/// <summary>Raw bytes at ActiveSkillPtr for offset discovery.</summary>
public byte[]? RawBytes { get; init; }
public bool CanBeUsed { get; init; }
public int UseStage { get; init; }
public int CastType { get; init; }
public int TotalUses { get; init; }
public int CooldownTimeMs { get; init; }
/// <summary>From Cooldowns vector — number of active cooldown entries.</summary>
public int ActiveCooldowns { get; init; }
/// <summary>From Cooldowns vector — max uses (charges) for the skill.</summary>
public int MaxUses { get; init; }
/// <summary>Low 16 bits of UnknownIdAndEquipmentInfo — skill ID used for SkillBarIds matching.</summary>
public ushort Id { get; init; }
/// <summary>High 16 bits of UnknownIdAndEquipmentInfo — equipment slot / secondary ID.</summary>
public ushort Id2 { get; init; }
/// <summary>Skill bar slot index (0-12) from SkillBarIds, or -1 if not on the skill bar.</summary>
public int SkillBarSlot { get; init; } = -1;
}