diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Automata.Ui/ViewModels/MemoryViewModel.cs
index af8a748..417ea0b 100644
--- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs
+++ b/src/Automata.Ui/ViewModels/MemoryViewModel.cs
@@ -103,6 +103,13 @@ public partial class MemoryViewModel : ObservableObject
private MemoryNodeViewModel? _entityListNode;
private MemoryNodeViewModel? _skillsNode;
private MemoryNodeViewModel? _questsNode;
+ private MemoryNodeViewModel? _areaRawName;
+ private MemoryNodeViewModel? _areaDisplayName;
+ private MemoryNodeViewModel? _areaAct;
+ private MemoryNodeViewModel? _areaIsTown;
+ private MemoryNodeViewModel? _areaHasWaypoint;
+ private MemoryNodeViewModel? _areaMonsterLevel;
+ private MemoryNodeViewModel? _worldAreaId;
partial void OnIsEnabledChanged(bool value)
{
@@ -235,7 +242,25 @@ public partial class MemoryViewModel : ObservableObject
terrain.Children.Add(_terrainGrid);
terrain.Children.Add(_terrainWalkable);
+ // AreaTemplate (from WorldData — zone metadata)
+ var areaTemplateGroup = new MemoryNodeViewModel("AreaTemplate");
+ _areaRawName = new MemoryNodeViewModel("RawName:");
+ _areaDisplayName = new MemoryNodeViewModel("Name:");
+ _areaAct = new MemoryNodeViewModel("Act:");
+ _areaIsTown = new MemoryNodeViewModel("IsTown:");
+ _areaHasWaypoint = new MemoryNodeViewModel("HasWaypoint:");
+ _areaMonsterLevel = new MemoryNodeViewModel("MonsterLevel:");
+ _worldAreaId = new MemoryNodeViewModel("WorldAreaId:");
+ areaTemplateGroup.Children.Add(_areaRawName);
+ areaTemplateGroup.Children.Add(_areaDisplayName);
+ areaTemplateGroup.Children.Add(_areaAct);
+ areaTemplateGroup.Children.Add(_areaIsTown);
+ areaTemplateGroup.Children.Add(_areaHasWaypoint);
+ areaTemplateGroup.Children.Add(_areaMonsterLevel);
+ areaTemplateGroup.Children.Add(_worldAreaId);
+
inGameStateGroup.Children.Add(areaInstanceGroup);
+ inGameStateGroup.Children.Add(areaTemplateGroup);
inGameStateGroup.Children.Add(player);
inGameStateGroup.Children.Add(entitiesGroup);
inGameStateGroup.Children.Add(terrain);
@@ -437,6 +462,15 @@ public partial class MemoryViewModel : ObservableObject
snap.EntityCount > 0 ? snap.EntityCount.ToString() : "—",
snap.EntityCount > 0);
+ // AreaTemplate
+ _areaRawName!.Set(snap.AreaRawName ?? "—", snap.AreaRawName is not null);
+ _areaDisplayName!.Set(snap.AreaName ?? "—", snap.AreaName is not null);
+ _areaAct!.Set(snap.AreaAct > 0 ? snap.AreaAct.ToString() : "—", snap.AreaAct > 0);
+ _areaIsTown!.Set(snap.AreaIsTown ? "Yes" : "No", snap.AreaIsTown);
+ _areaHasWaypoint!.Set(snap.AreaHasWaypoint ? "Yes" : "No", snap.AreaHasWaypoint);
+ _areaMonsterLevel!.Set(snap.AreaMonsterLevel > 0 ? snap.AreaMonsterLevel.ToString() : "—", snap.AreaMonsterLevel > 0);
+ _worldAreaId!.Set(snap.WorldAreaId > 0 ? snap.WorldAreaId.ToString() : "—", snap.WorldAreaId > 0);
+
// Player position
if (snap.HasPosition)
_playerPos!.Set($"({snap.PlayerX:F1}, {snap.PlayerY:F1}, {snap.PlayerZ:F1})");
@@ -1355,4 +1389,16 @@ public partial class MemoryViewModel : ObservableObject
ScanResult = _reader.Diagnostics!.ScanQuestStateObjects();
}
+
+ [RelayCommand]
+ private void ScanAreaTemplateExecute()
+ {
+ if (_reader is null || !_reader.IsAttached)
+ {
+ ScanResult = "Error: not attached";
+ return;
+ }
+
+ ScanResult = _reader.Diagnostics!.ScanAreaTemplate();
+ }
}
diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml
index 5106860..8239e31 100644
--- a/src/Automata.Ui/Views/MainWindow.axaml
+++ b/src/Automata.Ui/Views/MainWindow.axaml
@@ -784,6 +784,8 @@
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
+
+ /// Diagnostic: dump AreaInstance → AreaTemplate pointer chain.
+ ///
+ public string ScanAreaTemplate()
+ {
+ var sb = new StringBuilder();
+ var mem = _ctx.Memory;
+ var offsets = _ctx.Offsets;
+
+ // Resolve InGameState
+ var controller = mem.ReadPointer(_ctx.GameStateBase);
+ if (controller == 0) { sb.AppendLine("ERROR: controller is null"); return sb.ToString(); }
+
+ nint inGameState;
+ if (offsets.InGameStateDirectOffset > 0)
+ inGameState = mem.ReadPointer(controller + offsets.InGameStateDirectOffset);
+ else
+ inGameState = 0;
+ if (inGameState == 0) { sb.AppendLine("ERROR: InGameState is null"); return sb.ToString(); }
+
+ sb.AppendLine($"InGameState: 0x{inGameState:X}");
+
+ // AreaInstance ptr
+ var aiPtr = mem.ReadPointer(inGameState + offsets.IngameDataFromStateOffset);
+ sb.AppendLine($"AreaInstance (+0x{offsets.IngameDataFromStateOffset:X}): 0x{aiPtr:X}");
+ if (aiPtr == 0) { sb.AppendLine("ERROR: AreaInstance is null"); return sb.ToString(); }
+
+ // AreaTemplate ptr
+ var atPtr = mem.ReadPointer(aiPtr + offsets.AreaTemplateOffset);
+ sb.AppendLine($"AreaTemplate (+0x{offsets.AreaTemplateOffset:X}): 0x{atPtr:X}");
+ if (atPtr == 0) { sb.AppendLine("ERROR: AreaTemplate pointer is null"); return sb.ToString(); }
+
+ sb.AppendLine();
+ DumpAreaTemplateAt(sb, mem, atPtr, offsets);
+
+ return sb.ToString();
+ }
+
+ private void DumpAreaTemplateAt(StringBuilder sb, ProcessMemory mem, nint addr, GameOffsets offsets)
+ {
+ var rawNamePtr = mem.ReadPointer(addr + offsets.AreaTemplateRawNameOffset);
+ var namePtr = mem.ReadPointer(addr + offsets.AreaTemplateNameOffset);
+ var act = mem.Read(addr + offsets.AreaTemplateActOffset);
+ var isTown = mem.Read(addr + offsets.AreaTemplateIsTownOffset);
+ var hasWaypoint = mem.Read(addr + offsets.AreaTemplateHasWaypointOffset);
+ var monsterLevel = mem.Read(addr + offsets.AreaTemplateMonsterLevelOffset);
+ var worldAreaId = mem.Read(addr + offsets.AreaTemplateWorldAreaIdOffset);
+
+ var rawName = _strings.ReadNullTermWString(rawNamePtr);
+ var name = _strings.ReadNullTermWString(namePtr);
+
+ sb.AppendLine($" RawNamePtr (+0x{offsets.AreaTemplateRawNameOffset:X2}): 0x{rawNamePtr:X} → \"{rawName}\"");
+ sb.AppendLine($" NamePtr (+0x{offsets.AreaTemplateNameOffset:X2}): 0x{namePtr:X} → \"{name}\"");
+ sb.AppendLine($" Act (+0x{offsets.AreaTemplateActOffset:X2}): {act}");
+ sb.AppendLine($" IsTown (+0x{offsets.AreaTemplateIsTownOffset:X2}): {isTown}");
+ sb.AppendLine($" HasWaypoint(+0x{offsets.AreaTemplateHasWaypointOffset:X2}): {hasWaypoint}");
+ sb.AppendLine($" MonsterLvl (+0x{offsets.AreaTemplateMonsterLevelOffset:X2}): {monsterLevel}");
+ sb.AppendLine($" WorldAreaId(+0x{offsets.AreaTemplateWorldAreaIdOffset:X2}): {worldAreaId}");
+ }
}
diff --git a/src/Roboto.Memory/GameMemoryReader.cs b/src/Roboto.Memory/GameMemoryReader.cs
index 24f1982..bccab1f 100644
--- a/src/Roboto.Memory/GameMemoryReader.cs
+++ b/src/Roboto.Memory/GameMemoryReader.cs
@@ -192,8 +192,8 @@ public class GameMemoryReader : IDisposable
snap.AreaLevel = areaLevel;
snap.AreaHash = ai.AreaHash;
- // Area template from WorldData
- var at = gs.InGame.WorldData.AreaTemplate;
+ // Area template from AreaInstance
+ var at = ai.AreaTemplate;
if (at.IsValid)
{
snap.AreaRawName = at.RawName;
diff --git a/src/Roboto.Memory/GameOffsets.cs b/src/Roboto.Memory/GameOffsets.cs
index 784a577..bfe6dc5 100644
--- a/src/Roboto.Memory/GameOffsets.cs
+++ b/src/Roboto.Memory/GameOffsets.cs
@@ -235,9 +235,9 @@ public sealed class GameOffsets
/// Offset within Camera struct to the Matrix4x4 (64 bytes). 0 = disabled.
public int CameraMatrixOffset { get; set; } = 0x1A0;
- // ── AreaTemplate (WorldData → WorldAreaDetailsPtr → AreaTemplate) ──
- /// WorldData struct → WorldAreaDetailsPtr offset. Already in struct at 0x98.
- public int WorldAreaDetailsOffset { get; set; } = 0x98;
+ // ── AreaTemplate (AreaInstance +0xA0 → AreaTemplate) ──
+ /// AreaInstance → AreaTemplate pointer. Confirmed: 0xA0.
+ public int AreaTemplateOffset { get; set; } = 0xA0;
/// AreaTemplate → RawName wchar_t* pointer.
public int AreaTemplateRawNameOffset { get; set; } = 0x00;
/// AreaTemplate → Name wchar_t* pointer (display name).
diff --git a/src/Roboto.Memory/Objects/AreaInstance.cs b/src/Roboto.Memory/Objects/AreaInstance.cs
index f971679..b7b3197 100644
--- a/src/Roboto.Memory/Objects/AreaInstance.cs
+++ b/src/Roboto.Memory/Objects/AreaInstance.cs
@@ -20,6 +20,7 @@ public sealed class AreaInstance : RemoteObject
public PlayerSkills PlayerSkills { get; }
public QuestFlags QuestFlags { get; }
public Terrain Terrain { get; }
+ public AreaTemplate AreaTemplate { get; }
public AreaInstance(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
: base(ctx)
@@ -28,6 +29,7 @@ public sealed class AreaInstance : RemoteObject
PlayerSkills = new PlayerSkills(ctx, components, strings);
QuestFlags = new QuestFlags(ctx, strings, questNames);
Terrain = new Terrain(ctx);
+ AreaTemplate = new AreaTemplate(ctx, strings);
}
protected override bool ReadData()
@@ -99,6 +101,13 @@ public sealed class AreaInstance : RemoteObject
else
QuestFlags.Reset();
+ // AreaTemplate — pointer at AreaInstance + AreaTemplateOffset
+ var areaTemplatePtr = mem.ReadPointer(Address + offsets.AreaTemplateOffset);
+ if (areaTemplatePtr != 0)
+ AreaTemplate.Update(areaTemplatePtr);
+ else
+ AreaTemplate.Reset();
+
// Terrain — pass loading/area state before update
Terrain.AreaHash = AreaHash;
Terrain.Update(Address);
@@ -133,5 +142,6 @@ public sealed class AreaInstance : RemoteObject
PlayerSkills.Reset();
QuestFlags.Reset();
Terrain.Reset();
+ AreaTemplate.Reset();
}
}
diff --git a/src/Roboto.Memory/Objects/InGameState.cs b/src/Roboto.Memory/Objects/InGameState.cs
index 32438d5..0298cd5 100644
--- a/src/Roboto.Memory/Objects/InGameState.cs
+++ b/src/Roboto.Memory/Objects/InGameState.cs
@@ -19,7 +19,7 @@ public sealed class InGameState : RemoteObject
: base(ctx)
{
AreaInstance = new AreaInstance(ctx, components, strings, questNames);
- WorldData = new WorldData(ctx, strings);
+ WorldData = new WorldData(ctx);
}
protected override bool ReadData()
diff --git a/src/Roboto.Memory/Objects/WorldData.cs b/src/Roboto.Memory/Objects/WorldData.cs
index a5f724c..3f4ad12 100644
--- a/src/Roboto.Memory/Objects/WorldData.cs
+++ b/src/Roboto.Memory/Objects/WorldData.cs
@@ -7,7 +7,6 @@ namespace Roboto.Memory.Objects;
///
/// Reads WorldData struct (168B, 1 RPM) and resolves the camera matrix.
/// Primary camera source: WorldData.CameraPtr. Fallback: InGameState.CameraPtr (set via FallbackCameraPtr).
-/// Owns AreaTemplate child for area metadata.
///
public sealed class WorldData : RemoteObject
{
@@ -21,12 +20,7 @@ public sealed class WorldData : RemoteObject
/// Resolved address of the camera matrix for hot-path caching.
public nint CameraMatrixAddress { get; private set; }
- public AreaTemplate AreaTemplate { get; }
-
- public WorldData(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
- {
- AreaTemplate = new AreaTemplate(ctx, strings);
- }
+ public WorldData(MemoryContext ctx) : base(ctx) { }
protected override bool ReadData()
{
@@ -36,12 +30,6 @@ public sealed class WorldData : RemoteObject
// Read the full WorldData struct (0xA8 = 168 bytes, 1 RPM)
_data = mem.Read(Address);
- // Cascade to AreaTemplate
- if (_data.WorldAreaDetailsPtr != 0)
- AreaTemplate.Update(_data.WorldAreaDetailsPtr);
- else
- AreaTemplate.Reset();
-
// Resolve camera: primary from WorldData, fallback from InGameState
if (offsets.CameraMatrixOffset <= 0)
return true;
@@ -69,6 +57,5 @@ public sealed class WorldData : RemoteObject
FallbackCameraPtr = 0;
CameraMatrix = null;
CameraMatrixAddress = 0;
- AreaTemplate.Reset();
}
}