test
This commit is contained in:
parent
0df70abad7
commit
0c14d78d8a
25 changed files with 1487 additions and 179 deletions
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
14
src/Roboto.GameOffsets/Components/ActorDeployedEntity.cs
Normal file
14
src/Roboto.GameOffsets/Components/ActorDeployedEntity.cs
Normal 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;
|
||||||
|
}
|
||||||
33
src/Roboto.GameOffsets/Components/ActorSkill.cs
Normal file
33
src/Roboto.GameOffsets/Components/ActorSkill.cs
Normal 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;
|
||||||
|
}
|
||||||
24
src/Roboto.GameOffsets/Components/ActorSkillCooldown.cs
Normal file
24
src/Roboto.GameOffsets/Components/ActorSkillCooldown.cs
Normal 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;
|
||||||
|
}
|
||||||
15
src/Roboto.GameOffsets/Components/ActorVaalSkill.cs
Normal file
15
src/Roboto.GameOffsets/Components/ActorVaalSkill.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
57
src/Roboto.Memory/Objects/AreaTemplate.cs
Normal file
57
src/Roboto.Memory/Objects/AreaTemplate.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
391
src/Roboto.Memory/Objects/EntityList.cs
Normal file
391
src/Roboto.Memory/Objects/EntityList.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
235
src/Roboto.Memory/Objects/PlayerSkills.cs
Normal file
235
src/Roboto.Memory/Objects/PlayerSkills.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/Roboto.Memory/Objects/QuestFlags.cs
Normal file
266
src/Roboto.Memory/Objects/QuestFlags.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
176
src/Roboto.Memory/Objects/Terrain.cs
Normal file
176
src/Roboto.Memory/Objects/Terrain.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
21
src/Roboto.Memory/Snapshots/QuestSnapshot.cs
Normal file
21
src/Roboto.Memory/Snapshots/QuestSnapshot.cs
Normal 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; }
|
||||||
|
}
|
||||||
32
src/Roboto.Memory/Snapshots/SkillSnapshot.cs
Normal file
32
src/Roboto.Memory/Snapshots/SkillSnapshot.cs
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue