lots working good, minimap / rotation / follow / entities
This commit is contained in:
parent
69a8eaea62
commit
1ba7c39c30
11 changed files with 2496 additions and 99 deletions
36
components.json
Normal file
36
components.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
[
|
||||||
|
"Actor",
|
||||||
|
"Animated",
|
||||||
|
"AreaTransition",
|
||||||
|
"BaseEvents",
|
||||||
|
"Buffs",
|
||||||
|
"Chest",
|
||||||
|
"ControlZone",
|
||||||
|
"CritterAI",
|
||||||
|
"Functions",
|
||||||
|
"HideoutDoodad",
|
||||||
|
"InteractionAction",
|
||||||
|
"Inventories",
|
||||||
|
"Life",
|
||||||
|
"MinimapIcon",
|
||||||
|
"Monster",
|
||||||
|
"NPC",
|
||||||
|
"ObjectMagicProperties",
|
||||||
|
"Pathfinding",
|
||||||
|
"PetAi",
|
||||||
|
"Player",
|
||||||
|
"PlayerClass",
|
||||||
|
"Portal",
|
||||||
|
"Positioned",
|
||||||
|
"Preload",
|
||||||
|
"Projectile",
|
||||||
|
"ProximityTrigger",
|
||||||
|
"Render",
|
||||||
|
"StateMachine",
|
||||||
|
"Stats",
|
||||||
|
"Targetable",
|
||||||
|
"Timer",
|
||||||
|
"Transitionable",
|
||||||
|
"TriggerableBlockage",
|
||||||
|
"WorldItem"
|
||||||
|
]
|
||||||
94
entities.json
Normal file
94
entities.json
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
[
|
||||||
|
"Metadata/Characters/Character_login",
|
||||||
|
"Metadata/Characters/Dex/DexFour",
|
||||||
|
"Metadata/Characters/Dex/DexFourb",
|
||||||
|
"Metadata/Characters/DexInt/DexIntFourb",
|
||||||
|
"Metadata/Characters/Int/IntFour",
|
||||||
|
"Metadata/Characters/Int/IntFourb",
|
||||||
|
"Metadata/Characters/Str/StrFourb",
|
||||||
|
"Metadata/Characters/StrDex/StrDexFourb",
|
||||||
|
"Metadata/Characters/StrInt/StrIntFourb",
|
||||||
|
"Metadata/Chests/EzomyteChest_05",
|
||||||
|
"Metadata/Chests/EzomyteChest_06",
|
||||||
|
"Metadata/Chests/MossyChest26",
|
||||||
|
"Metadata/Critters/Chicken/Chicken_kingsmarch",
|
||||||
|
"Metadata/Critters/Hedgehog/HedgehogSlow",
|
||||||
|
"Metadata/Critters/Weta/Basic",
|
||||||
|
"Metadata/Effects/Effect",
|
||||||
|
"Metadata/Effects/Microtransactions/foot_prints/delirium/footprints_delirium",
|
||||||
|
"Metadata/Effects/Microtransactions/foot_prints/harvest02/footprints_harvest",
|
||||||
|
"Metadata/Effects/PermanentEffect",
|
||||||
|
"Metadata/Effects/ServerEffect",
|
||||||
|
"Metadata/MiscellaneousObjects/AreaTransitionBlockage",
|
||||||
|
"Metadata/MiscellaneousObjects/AreaTransitionDoodad",
|
||||||
|
"Metadata/MiscellaneousObjects/AreaTransition_Animate",
|
||||||
|
"Metadata/MiscellaneousObjects/Checkpoint",
|
||||||
|
"Metadata/MiscellaneousObjects/Doodad",
|
||||||
|
"Metadata/MiscellaneousObjects/DoodadNoBlocking",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_20_1",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_6_4",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_4.75_1",
|
||||||
|
"Metadata/MiscellaneousObjects/GuildStash",
|
||||||
|
"Metadata/MiscellaneousObjects/HealingWell",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_1",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_2",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_3",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_4",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_5",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_6",
|
||||||
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalEncounter",
|
||||||
|
"Metadata/MiscellaneousObjects/MultiplexPortal",
|
||||||
|
"Metadata/MiscellaneousObjects/ServerDoodadHidden",
|
||||||
|
"Metadata/MiscellaneousObjects/Stash",
|
||||||
|
"Metadata/MiscellaneousObjects/Waypoint",
|
||||||
|
"Metadata/MiscellaneousObjects/WorldItem",
|
||||||
|
"Metadata/Monsters/Hags/UrchinHag1",
|
||||||
|
"Metadata/Monsters/Urchins/MeleeUrchin1",
|
||||||
|
"Metadata/Monsters/Urchins/SlingUrchin1",
|
||||||
|
"Metadata/Monsters/Wolves/RottenWolf1_",
|
||||||
|
"Metadata/Monsters/Wolves/RottenWolfDead",
|
||||||
|
"Metadata/Monsters/Zombies/CourtGuardZombieUnarmed",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxe",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmed",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmedPhysics",
|
||||||
|
"Metadata/NPC/Four_Act1/ClearfellPosting1",
|
||||||
|
"Metadata/NPC/Four_Act1/DogTrader_Entrance",
|
||||||
|
"Metadata/NPC/Four_Act1/ExecutionerFemaleNPCTown",
|
||||||
|
"Metadata/NPC/Four_Act1/EzomyteCivilianFemale01",
|
||||||
|
"Metadata/NPC/Four_Act1/EzomyteCivilianFemale02",
|
||||||
|
"Metadata/NPC/Four_Act1/EzomyteCivilianMale01",
|
||||||
|
"Metadata/NPC/Four_Act1/Finn",
|
||||||
|
"Metadata/NPC/Four_Act1/FinnHoodedMentorInjured",
|
||||||
|
"Metadata/NPC/Four_Act1/HoodedMentor",
|
||||||
|
"Metadata/NPC/Four_Act1/HoodedMentorAfterIronCount",
|
||||||
|
"Metadata/NPC/Four_Act1/HoodedMentorInjured",
|
||||||
|
"Metadata/NPC/Four_Act1/Renly",
|
||||||
|
"Metadata/NPC/Four_Act1/RenlyAfterIronCount",
|
||||||
|
"Metadata/NPC/Four_Act1/RenlyIntro",
|
||||||
|
"Metadata/NPC/Four_Act1/Una",
|
||||||
|
"Metadata/NPC/Four_Act1/UnaAfterHealHoodedMentor",
|
||||||
|
"Metadata/NPC/Four_Act1/UnaAfterIronCount",
|
||||||
|
"Metadata/NPC/Four_Act1/UnaHoodedOneInjured",
|
||||||
|
"Metadata/NPC/League/Incursion/AlvaIncursionWild",
|
||||||
|
"Metadata/Pet/BetaKiwis/FaridunKiwi",
|
||||||
|
"Metadata/Pet/FledglingBellcrow/FledglingBellcrow",
|
||||||
|
"Metadata/Pet/OrigamiPet/OrigamiPetBase",
|
||||||
|
"Metadata/Pet/Phoenix/PhoenixPetGreen",
|
||||||
|
"Metadata/Pet/Phoenix/PhoenixPetRed",
|
||||||
|
"Metadata/Projectiles/SlingUrchinProjectile",
|
||||||
|
"Metadata/Projectiles/Twister",
|
||||||
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1",
|
||||||
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1_CountKilled",
|
||||||
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2",
|
||||||
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2_CountKilled",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_2/Objects/RuleSet",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/Act1_finished_LightController",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBenchEzomyte",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBench_DisableRendering",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBench_EnableRendering",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/VisitedAct2_DisableRendering",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/VisitedAct2_EnableRendering",
|
||||||
|
"Metadata/Terrain/Tools/AudioTools/G1_2/RiverRapidsMedium",
|
||||||
|
"Metadata/Terrain/Tools/AudioTools/G1_Town/FurnaceFireAudio",
|
||||||
|
"Metadata/Terrain/Tools/AudioTools/G1_Town/InsideWaterMillAudio"
|
||||||
|
]
|
||||||
24
offsets.json
24
offsets.json
|
|
@ -6,9 +6,13 @@
|
||||||
"StatesBeginOffset": 72,
|
"StatesBeginOffset": 72,
|
||||||
"StateStride": 16,
|
"StateStride": 16,
|
||||||
"StatePointerOffset": 0,
|
"StatePointerOffset": 0,
|
||||||
|
"StateCount": 12,
|
||||||
"InGameStateIndex": 4,
|
"InGameStateIndex": 4,
|
||||||
|
"ActiveStatesOffset": 32,
|
||||||
"StatesInline": true,
|
"StatesInline": true,
|
||||||
"InGameStateDirectOffset": 528,
|
"InGameStateDirectOffset": 528,
|
||||||
|
"IsLoadingOffset": 832,
|
||||||
|
"EscapeStateOffset": 524,
|
||||||
"IngameDataFromStateOffset": 656,
|
"IngameDataFromStateOffset": 656,
|
||||||
"WorldDataFromStateOffset": 760,
|
"WorldDataFromStateOffset": 760,
|
||||||
"AreaLevelOffset": 196,
|
"AreaLevelOffset": 196,
|
||||||
|
|
@ -19,8 +23,22 @@
|
||||||
"LocalPlayerDirectOffset": 2576,
|
"LocalPlayerDirectOffset": 2576,
|
||||||
"EntityListOffset": 2896,
|
"EntityListOffset": 2896,
|
||||||
"EntityCountInternalOffset": 8,
|
"EntityCountInternalOffset": 8,
|
||||||
|
"EntityNodeLeftOffset": 0,
|
||||||
|
"EntityNodeParentOffset": 8,
|
||||||
|
"EntityNodeRightOffset": 16,
|
||||||
|
"EntityNodeValueOffset": 40,
|
||||||
|
"EntityIdOffset": 128,
|
||||||
|
"EntityFlagsOffset": 132,
|
||||||
|
"EntityDetailsOffset": 8,
|
||||||
|
"EntityPathStringOffset": 8,
|
||||||
"LocalPlayerOffset": 32,
|
"LocalPlayerOffset": 32,
|
||||||
"ComponentListOffset": 16,
|
"ComponentListOffset": 16,
|
||||||
|
"EntityHeaderOffset": 8,
|
||||||
|
"ComponentLookupOffset": 40,
|
||||||
|
"ComponentLookupVec2Offset": 40,
|
||||||
|
"ComponentLookupEntrySize": 16,
|
||||||
|
"ComponentLookupNameOffset": 0,
|
||||||
|
"ComponentLookupIndexOffset": 8,
|
||||||
"LifeComponentIndex": -1,
|
"LifeComponentIndex": -1,
|
||||||
"RenderComponentIndex": -1,
|
"RenderComponentIndex": -1,
|
||||||
"LifeComponentOffset1": 1056,
|
"LifeComponentOffset1": 1056,
|
||||||
|
|
@ -35,9 +53,9 @@
|
||||||
"PositionZOffset": 320,
|
"PositionZOffset": 320,
|
||||||
"TerrainListOffset": 3264,
|
"TerrainListOffset": 3264,
|
||||||
"TerrainInline": true,
|
"TerrainInline": true,
|
||||||
"TerrainDimensionsOffset": 24,
|
"TerrainDimensionsOffset": 144,
|
||||||
"TerrainWalkableGridOffset": 208,
|
"TerrainWalkableGridOffset": 328,
|
||||||
"TerrainBytesPerRowOffset": 256,
|
"TerrainBytesPerRowOffset": 424,
|
||||||
"TerrainGridPtrOffset": 8,
|
"TerrainGridPtrOffset": 8,
|
||||||
"SubTilesPerCell": 23,
|
"SubTilesPerCell": 23,
|
||||||
"InGameStateOffset": 0,
|
"InGameStateOffset": 0,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.12" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
201
src/Automata.Memory/Entity.cs
Normal file
201
src/Automata.Memory/Entity.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
namespace Automata.Memory;
|
||||||
|
|
||||||
|
public enum EntityType
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Player,
|
||||||
|
Monster,
|
||||||
|
Npc,
|
||||||
|
Effect,
|
||||||
|
WorldItem,
|
||||||
|
MiscellaneousObject,
|
||||||
|
Terrain,
|
||||||
|
Critter,
|
||||||
|
Chest,
|
||||||
|
Shrine,
|
||||||
|
Portal,
|
||||||
|
TownPortal,
|
||||||
|
Waypoint,
|
||||||
|
AreaTransition,
|
||||||
|
Door,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MonsterRarity
|
||||||
|
{
|
||||||
|
White,
|
||||||
|
Magic,
|
||||||
|
Rare,
|
||||||
|
Unique,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Entity
|
||||||
|
{
|
||||||
|
public nint Address { get; }
|
||||||
|
public uint Id { get; }
|
||||||
|
public string? Path { get; }
|
||||||
|
public string? Metadata { get; }
|
||||||
|
|
||||||
|
// Position (from Render component)
|
||||||
|
public bool HasPosition { get; internal set; }
|
||||||
|
public float X { get; internal set; }
|
||||||
|
public float Y { get; internal set; }
|
||||||
|
public float Z { get; internal set; }
|
||||||
|
|
||||||
|
// Vitals (from Life component — only populated when explicitly read)
|
||||||
|
public bool HasVitals { get; internal set; }
|
||||||
|
public int LifeCurrent { get; internal set; }
|
||||||
|
public int LifeTotal { get; internal set; }
|
||||||
|
public int ManaCurrent { get; internal set; }
|
||||||
|
public int ManaTotal { get; internal set; }
|
||||||
|
public int EsCurrent { get; internal set; }
|
||||||
|
public int EsTotal { get; internal set; }
|
||||||
|
|
||||||
|
// Component info
|
||||||
|
public int ComponentCount { get; internal set; }
|
||||||
|
public HashSet<string>? Components { get; internal set; }
|
||||||
|
|
||||||
|
// Component-based properties (populated by GameMemoryReader)
|
||||||
|
public bool IsTargetable { get; internal set; }
|
||||||
|
public bool IsOpened { get; internal set; }
|
||||||
|
public bool IsAvailable { get; internal set; }
|
||||||
|
public MonsterRarity Rarity { get; internal set; }
|
||||||
|
|
||||||
|
// Derived properties
|
||||||
|
public bool IsAlive => HasVitals && LifeCurrent > 0;
|
||||||
|
public bool IsDead => HasVitals && LifeCurrent <= 0;
|
||||||
|
public bool IsHostile => Type == EntityType.Monster;
|
||||||
|
public bool IsNpc => Type == EntityType.Npc;
|
||||||
|
public bool IsPlayer => Type == EntityType.Player;
|
||||||
|
public bool HasComponent(string name) => Components?.Contains(name) == true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grid-plane distance to another point (ignores Z).
|
||||||
|
/// </summary>
|
||||||
|
public float DistanceTo(float px, float py)
|
||||||
|
{
|
||||||
|
var dx = X - px;
|
||||||
|
var dy = Y - py;
|
||||||
|
return MathF.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grid-plane distance to another entity.
|
||||||
|
/// </summary>
|
||||||
|
public float DistanceTo(Entity other) => DistanceTo(other.X, other.Y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Short category string derived from path (e.g. "Monsters", "Effects", "NPC").
|
||||||
|
/// </summary>
|
||||||
|
public string Category
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Path is null) return "?";
|
||||||
|
var parts = Path.Split('/');
|
||||||
|
return parts.Length >= 2 ? parts[1] : "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityType Type { get; internal set; }
|
||||||
|
|
||||||
|
internal Entity(nint address, uint id, string? path)
|
||||||
|
{
|
||||||
|
Address = address;
|
||||||
|
Id = id;
|
||||||
|
Path = path;
|
||||||
|
Metadata = ExtractMetadata(path);
|
||||||
|
Type = ClassifyType(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reclassify entity type using component names (called after components are read).
|
||||||
|
/// Component-based classification is more reliable than path-based.
|
||||||
|
/// </summary>
|
||||||
|
internal void ReclassifyFromComponents()
|
||||||
|
{
|
||||||
|
if (Components is null || Components.Count == 0) return;
|
||||||
|
|
||||||
|
// Priority order matching ExileCore's ParseType logic
|
||||||
|
if (Components.Contains("Monster")) { Type = EntityType.Monster; return; }
|
||||||
|
if (Components.Contains("Chest")) { Type = EntityType.Chest; return; }
|
||||||
|
if (Components.Contains("Shrine")) { Type = EntityType.Shrine; return; }
|
||||||
|
if (Components.Contains("Waypoint")) { Type = EntityType.Waypoint; return; }
|
||||||
|
if (Components.Contains("AreaTransition")) { Type = EntityType.AreaTransition; return; }
|
||||||
|
if (Components.Contains("Portal")) { Type = EntityType.Portal; return; }
|
||||||
|
if (Components.Contains("TownPortal")) { Type = EntityType.TownPortal; return; }
|
||||||
|
if (Components.Contains("NPC")) { Type = EntityType.Npc; return; }
|
||||||
|
if (Components.Contains("Player")) { Type = EntityType.Player; return; }
|
||||||
|
// Don't override path-based classification for Effects/Terrain/etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strips the "@N" instance suffix from the path.
|
||||||
|
/// "Metadata/Monsters/Wolves/RottenWolf1_@2" → "Metadata/Monsters/Wolves/RottenWolf1_"
|
||||||
|
/// </summary>
|
||||||
|
private static string? ExtractMetadata(string? path)
|
||||||
|
{
|
||||||
|
if (path is null) return null;
|
||||||
|
var atIndex = path.LastIndexOf('@');
|
||||||
|
return atIndex > 0 ? path[..atIndex] : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EntityType ClassifyType(string? path)
|
||||||
|
{
|
||||||
|
if (path is null) return EntityType.Unknown;
|
||||||
|
|
||||||
|
// Check second path segment: "Metadata/<Category>/..."
|
||||||
|
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":
|
||||||
|
// Sub-classify: some "monsters" are actually NPCs or critters
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var pos = HasPosition ? $"({X:F0},{Y:F0})" : "no pos";
|
||||||
|
return $"[{Id}] {Type} {Path ?? "?"} {pos}";
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
134
src/Automata.Memory/ObjectRegistry.cs
Normal file
134
src/Automata.Memory/ObjectRegistry.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Automata.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persistent registry of discovered strings, organized by category.
|
||||||
|
/// Saves each category to its own JSON file (e.g. components.json, entities.json).
|
||||||
|
/// Loads on startup, saves whenever new entries are found.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ObjectRegistry
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Category> _categories = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get or create a category. Each category persists to its own JSON file.
|
||||||
|
/// </summary>
|
||||||
|
public Category this[string name]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_categories.TryGetValue(name, out var cat))
|
||||||
|
{
|
||||||
|
cat = new Category(name);
|
||||||
|
_categories[name] = cat;
|
||||||
|
}
|
||||||
|
return cat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flush all dirty categories to disk.
|
||||||
|
/// </summary>
|
||||||
|
public void Flush()
|
||||||
|
{
|
||||||
|
foreach (var cat in _categories.Values)
|
||||||
|
cat.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Category
|
||||||
|
{
|
||||||
|
private readonly string _path;
|
||||||
|
private readonly HashSet<string> _known = [];
|
||||||
|
private bool _dirty;
|
||||||
|
|
||||||
|
public IReadOnlySet<string> Known => _known;
|
||||||
|
public int Count => _known.Count;
|
||||||
|
|
||||||
|
internal Category(string name)
|
||||||
|
{
|
||||||
|
_path = $"{name}.json";
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a single entry. Returns true if it was new.
|
||||||
|
/// </summary>
|
||||||
|
public bool Register(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) return false;
|
||||||
|
if (_known.Add(value))
|
||||||
|
{
|
||||||
|
_dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register multiple entries. Returns true if any were new.
|
||||||
|
/// </summary>
|
||||||
|
public bool Register(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
var added = false;
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value) && _known.Add(value))
|
||||||
|
{
|
||||||
|
added = true;
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save to disk if there are unsaved changes.
|
||||||
|
/// </summary>
|
||||||
|
public void Flush()
|
||||||
|
{
|
||||||
|
if (!_dirty) return;
|
||||||
|
Save();
|
||||||
|
_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_path)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(_path);
|
||||||
|
var list = JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
if (list is not null)
|
||||||
|
{
|
||||||
|
foreach (var name in list)
|
||||||
|
_known.Add(name);
|
||||||
|
Log.Information("Loaded {Count} entries from '{Path}'", _known.Count, _path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error loading from '{Path}'", _path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sorted = _known.OrderBy(n => n, StringComparer.Ordinal).ToList();
|
||||||
|
var json = JsonSerializer.Serialize(sorted, JsonOptions);
|
||||||
|
File.WriteAllText(_path, json);
|
||||||
|
Log.Debug("Saved {Count} entries to '{Path}'", sorted.Count, _path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error saving to '{Path}'", _path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,15 +32,23 @@ public sealed class TerrainOffsets
|
||||||
public int StateStride { get; set; } = 0x10;
|
public int StateStride { get; set; } = 0x10;
|
||||||
/// <summary>Offset within each state entry to the actual state pointer.</summary>
|
/// <summary>Offset within each state entry to the actual state pointer.</summary>
|
||||||
public int StatePointerOffset { get; set; } = 0;
|
public int StatePointerOffset { get; set; } = 0;
|
||||||
|
/// <summary>Total number of state slots (ExileCore: 12 states, State0-State11).</summary>
|
||||||
|
public int StateCount { get; set; } = 12;
|
||||||
/// <summary>Which state index is InGameState (typically 4).</summary>
|
/// <summary>Which state index is InGameState (typically 4).</summary>
|
||||||
public int InGameStateIndex { get; set; } = 4;
|
public int InGameStateIndex { get; set; } = 4;
|
||||||
|
/// <summary>Offset from controller to active states vector begin/end pair (ExileCore: 0x20).</summary>
|
||||||
|
public int ActiveStatesOffset { get; set; } = 0x20;
|
||||||
|
/// <summary>Offset from controller to the active state pointer. When it != InGameState, we're loading. 0 = disabled, use ScanAreaLoadingState to find.</summary>
|
||||||
|
public int IsLoadingOffset { get; set; } = 0;
|
||||||
/// <summary>If true, states are inline in the controller struct. If false, StatesBeginOffset points to a begin/end vector pair.</summary>
|
/// <summary>If true, states are inline in the controller struct. If false, StatesBeginOffset points to a begin/end vector pair.</summary>
|
||||||
public bool StatesInline { get; set; } = true;
|
public bool StatesInline { get; set; } = true;
|
||||||
/// <summary>Direct offset from controller to InGameState pointer (bypasses state array). 0 = use state array instead. CE confirmed: 0x210.</summary>
|
/// <summary>Direct offset from controller to InGameState pointer (bypasses state array). 0 = use state array instead. CE confirmed: 0x210.</summary>
|
||||||
public int InGameStateDirectOffset { get; set; } = 0x210;
|
public int InGameStateDirectOffset { get; set; } = 0x210;
|
||||||
|
|
||||||
// ── InGameState → sub-structures ──
|
// ── InGameState → sub-structures ──
|
||||||
// Dump: InGameStateOffset { [0x298] AreaInstanceData, [0x2F8] WorldData, [0x648] UiRootPtr, [0xC40] IngameUi }
|
// Dump: InGameStateOffset { [0x208] EscapeState flags, [0x298] AreaInstanceData, [0x2F8] WorldData, [0x648] UiRootPtr, [0xC40] IngameUi }
|
||||||
|
/// <summary>InGameState → EscapeState int32 flag (0=closed, 1=open). Diff-scan confirmed: 0x20C. 0 = disabled.</summary>
|
||||||
|
public int EscapeStateOffset { get; set; } = 0x20C;
|
||||||
/// <summary>InGameState → AreaInstance (IngameData) pointer (dump: 0x298, CE confirmed: 0x290).</summary>
|
/// <summary>InGameState → AreaInstance (IngameData) pointer (dump: 0x298, CE confirmed: 0x290).</summary>
|
||||||
public int IngameDataFromStateOffset { get; set; } = 0x290;
|
public int IngameDataFromStateOffset { get; set; } = 0x290;
|
||||||
/// <summary>InGameState → WorldData pointer (dump: 0x2F8).</summary>
|
/// <summary>InGameState → WorldData pointer (dump: 0x2F8).</summary>
|
||||||
|
|
@ -102,8 +110,16 @@ public sealed class TerrainOffsets
|
||||||
public int ComponentListOffset { get; set; } = 0x10;
|
public int ComponentListOffset { get; set; } = 0x10;
|
||||||
/// <summary>Entity → ObjectHeader pointer (for alternative component lookup via name→index map). ExileCore: 0x08.</summary>
|
/// <summary>Entity → ObjectHeader pointer (for alternative component lookup via name→index map). ExileCore: 0x08.</summary>
|
||||||
public int EntityHeaderOffset { get; set; } = 0x08;
|
public int EntityHeaderOffset { get; set; } = 0x08;
|
||||||
/// <summary>ObjectHeader → NativePtrArray for component name→index lookup. ExileCore: 0x40.</summary>
|
/// <summary>EntityDetails → ComponentLookup object pointer. Confirmed: 0x28 (right after wstring path).</summary>
|
||||||
public int ComponentLookupOffset { get; set; } = 0x40;
|
public int ComponentLookupOffset { get; set; } = 0x28;
|
||||||
|
/// <summary>ComponentLookup object → Vec2 begin/end (name entry array). Object layout: +0x10=Vec1(ptrs), +0x28=Vec2(names).</summary>
|
||||||
|
public int ComponentLookupVec2Offset { get; set; } = 0x28;
|
||||||
|
/// <summary>Size of each entry in Vec2 (bytes). Confirmed: 16 = { char* name (8), int32 index (4), int32 flags (4) }.</summary>
|
||||||
|
public int ComponentLookupEntrySize { get; set; } = 16;
|
||||||
|
/// <summary>Offset to the char* name pointer within each lookup entry.</summary>
|
||||||
|
public int ComponentLookupNameOffset { get; set; } = 0;
|
||||||
|
/// <summary>Offset to the component index (int32) within each lookup entry.</summary>
|
||||||
|
public int ComponentLookupIndexOffset { get; set; } = 8;
|
||||||
/// <summary>Index of Life component in entity's component list. -1 = auto-discover via pattern scan.</summary>
|
/// <summary>Index of Life component in entity's component list. -1 = auto-discover via pattern scan.</summary>
|
||||||
public int LifeComponentIndex { get; set; } = -1;
|
public int LifeComponentIndex { get; set; } = -1;
|
||||||
/// <summary>Index of Render/Position component in entity's component list. -1 = unknown.</summary>
|
/// <summary>Index of Render/Position component in entity's component list. -1 = unknown.</summary>
|
||||||
|
|
@ -134,24 +150,25 @@ public sealed class TerrainOffsets
|
||||||
public int PositionZOffset { get; set; } = 0x140;
|
public int PositionZOffset { get; set; } = 0x140;
|
||||||
|
|
||||||
// ── Terrain (inline in AreaInstance) ──
|
// ── Terrain (inline in AreaInstance) ──
|
||||||
// Dump: TerrainStruct (at AreaInstance + 0xCC0) {
|
// Scan-confirmed TerrainStruct (at AreaInstance + 0xCC0):
|
||||||
// [0x18] StdTuple2D<long> TotalTiles,
|
// [0x90] StdTuple2D<long> TotalTiles (cols, rows as int64)
|
||||||
// [0x28] StdVector TileDetailsPtr,
|
// [0xA0] StdVector TileDetailsPtr (56 bytes/tile)
|
||||||
// [0xD0] StdVector GridWalkableData,
|
// [0x100] int32 cols, int32 rows (redundant compact dims)
|
||||||
// [0xE8] StdVector GridLandscapeData,
|
// [0x148] StdVector GridWalkableData (size = bytesPerRow * gridHeight)
|
||||||
// [0x100] int BytesPerRow,
|
// [0x160] StdVector GridLandscapeData
|
||||||
// [0x104] short TileHeightMultiplier
|
// [0x178] StdVector Grid3
|
||||||
// }
|
// [0x190] StdVector Grid4
|
||||||
|
// [0x1A8] int BytesPerRow = ceil(cols * 23 / 2)
|
||||||
/// <summary>Offset from AreaInstance to inline TerrainStruct (dump: 0xCC0).</summary>
|
/// <summary>Offset from AreaInstance to inline TerrainStruct (dump: 0xCC0).</summary>
|
||||||
public int TerrainListOffset { get; set; } = 0xCC0;
|
public int TerrainListOffset { get; set; } = 0xCC0;
|
||||||
/// <summary>If true, terrain is inline in AreaInstance (no pointer dereference). If false, follow pointer.</summary>
|
/// <summary>If true, terrain is inline in AreaInstance (no pointer dereference). If false, follow pointer.</summary>
|
||||||
public bool TerrainInline { get; set; } = true;
|
public bool TerrainInline { get; set; } = true;
|
||||||
/// <summary>TerrainStruct → TotalTiles offset (dump: 0x18, StdTuple2D of long).</summary>
|
/// <summary>TerrainStruct → TotalTiles offset (scan: 0x90, StdTuple2D of long).</summary>
|
||||||
public int TerrainDimensionsOffset { get; set; } = 0x18;
|
public int TerrainDimensionsOffset { get; set; } = 0x90;
|
||||||
/// <summary>TerrainStruct → GridWalkableData StdVector offset (dump: 0xD0).</summary>
|
/// <summary>TerrainStruct → GridWalkableData StdVector offset (scan: 0x148).</summary>
|
||||||
public int TerrainWalkableGridOffset { get; set; } = 0xD0;
|
public int TerrainWalkableGridOffset { get; set; } = 0x148;
|
||||||
/// <summary>TerrainStruct → BytesPerRow (dump: 0x100).</summary>
|
/// <summary>TerrainStruct → BytesPerRow (scan: 0x1A8).</summary>
|
||||||
public int TerrainBytesPerRowOffset { get; set; } = 0x100;
|
public int TerrainBytesPerRowOffset { get; set; } = 0x1A8;
|
||||||
/// <summary>Kept for pointer-based terrain mode (TerrainInline=false).</summary>
|
/// <summary>Kept for pointer-based terrain mode (TerrainInline=false).</summary>
|
||||||
public int TerrainGridPtrOffset { get; set; } = 0x08;
|
public int TerrainGridPtrOffset { get; set; } = 0x08;
|
||||||
public int SubTilesPerCell { get; set; } = 23;
|
public int SubTilesPerCell { get; set; } = 23;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
@ -32,11 +36,21 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
private GameMemoryReader? _reader;
|
private GameMemoryReader? _reader;
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
|
private const int ViewportRadius = 300; // grid pixels visible in each direction from player
|
||||||
|
|
||||||
[ObservableProperty] private bool _isEnabled;
|
[ObservableProperty] private bool _isEnabled;
|
||||||
[ObservableProperty] private string _statusText = "Not attached";
|
[ObservableProperty] private string _statusText = "Not attached";
|
||||||
|
|
||||||
public ObservableCollection<MemoryNodeViewModel> RootNodes { get; } = [];
|
public ObservableCollection<MemoryNodeViewModel> RootNodes { get; } = [];
|
||||||
|
|
||||||
|
// Minimap
|
||||||
|
[ObservableProperty] private Bitmap? _terrainImage;
|
||||||
|
private byte[]? _terrainBasePixels;
|
||||||
|
private byte[]? _minimapBuffer; // reused each frame
|
||||||
|
private int _terrainImageWidth, _terrainImageHeight;
|
||||||
|
private uint _terrainImageAreaHash;
|
||||||
|
private WalkabilityGrid? _terrainGridRef;
|
||||||
|
|
||||||
// Raw explorer
|
// Raw explorer
|
||||||
[ObservableProperty] private string _rawAddress = "";
|
[ObservableProperty] private string _rawAddress = "";
|
||||||
[ObservableProperty] private string _rawOffsets = "";
|
[ObservableProperty] private string _rawOffsets = "";
|
||||||
|
|
@ -75,10 +89,15 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
private MemoryNodeViewModel? _playerLife;
|
private MemoryNodeViewModel? _playerLife;
|
||||||
private MemoryNodeViewModel? _playerMana;
|
private MemoryNodeViewModel? _playerMana;
|
||||||
private MemoryNodeViewModel? _playerEs;
|
private MemoryNodeViewModel? _playerEs;
|
||||||
|
private MemoryNodeViewModel? _isLoadingNode;
|
||||||
|
private MemoryNodeViewModel? _escapeStateNode;
|
||||||
|
private MemoryNodeViewModel? _statesNode;
|
||||||
private MemoryNodeViewModel? _terrainCells;
|
private MemoryNodeViewModel? _terrainCells;
|
||||||
private MemoryNodeViewModel? _terrainGrid;
|
private MemoryNodeViewModel? _terrainGrid;
|
||||||
|
private MemoryNodeViewModel? _terrainWalkable;
|
||||||
private MemoryNodeViewModel? _entitySummary;
|
private MemoryNodeViewModel? _entitySummary;
|
||||||
private MemoryNodeViewModel? _entityTypesNode;
|
private MemoryNodeViewModel? _entityTypesNode;
|
||||||
|
private MemoryNodeViewModel? _entityListNode;
|
||||||
|
|
||||||
partial void OnIsEnabledChanged(bool value)
|
partial void OnIsEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
|
|
@ -115,6 +134,13 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_reader = null;
|
_reader = null;
|
||||||
RootNodes.Clear();
|
RootNodes.Clear();
|
||||||
StatusText = "Not attached";
|
StatusText = "Not attached";
|
||||||
|
|
||||||
|
_terrainBasePixels = null;
|
||||||
|
_terrainImageAreaHash = 0;
|
||||||
|
_terrainGridRef = null;
|
||||||
|
var old = TerrainImage;
|
||||||
|
TerrainImage = null;
|
||||||
|
old?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildTree()
|
private void BuildTree()
|
||||||
|
|
@ -137,11 +163,17 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_gsController = new MemoryNodeViewModel("Controller:");
|
_gsController = new MemoryNodeViewModel("Controller:");
|
||||||
_gsStates = new MemoryNodeViewModel("States:");
|
_gsStates = new MemoryNodeViewModel("States:");
|
||||||
_inGameState = new MemoryNodeViewModel("InGameState:");
|
_inGameState = new MemoryNodeViewModel("InGameState:");
|
||||||
|
_isLoadingNode = new MemoryNodeViewModel("Loading:");
|
||||||
|
_escapeStateNode = new MemoryNodeViewModel("Escape:");
|
||||||
|
_statesNode = new MemoryNodeViewModel("State Slots") { IsExpanded = true };
|
||||||
gameState.Children.Add(_gsPattern);
|
gameState.Children.Add(_gsPattern);
|
||||||
gameState.Children.Add(_gsBase);
|
gameState.Children.Add(_gsBase);
|
||||||
gameState.Children.Add(_gsController);
|
gameState.Children.Add(_gsController);
|
||||||
gameState.Children.Add(_gsStates);
|
gameState.Children.Add(_gsStates);
|
||||||
gameState.Children.Add(_inGameState);
|
gameState.Children.Add(_inGameState);
|
||||||
|
gameState.Children.Add(_isLoadingNode);
|
||||||
|
gameState.Children.Add(_escapeStateNode);
|
||||||
|
gameState.Children.Add(_statesNode);
|
||||||
|
|
||||||
// InGameState children
|
// InGameState children
|
||||||
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
|
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
|
||||||
|
|
@ -176,15 +208,19 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
var entitiesGroup = new MemoryNodeViewModel("Entities");
|
var entitiesGroup = new MemoryNodeViewModel("Entities");
|
||||||
_entitySummary = new MemoryNodeViewModel("Summary:");
|
_entitySummary = new MemoryNodeViewModel("Summary:");
|
||||||
_entityTypesNode = new MemoryNodeViewModel("Types:") { IsExpanded = false };
|
_entityTypesNode = new MemoryNodeViewModel("Types:") { IsExpanded = false };
|
||||||
|
_entityListNode = new MemoryNodeViewModel("List:") { IsExpanded = false };
|
||||||
entitiesGroup.Children.Add(_entitySummary);
|
entitiesGroup.Children.Add(_entitySummary);
|
||||||
entitiesGroup.Children.Add(_entityTypesNode);
|
entitiesGroup.Children.Add(_entityTypesNode);
|
||||||
|
entitiesGroup.Children.Add(_entityListNode);
|
||||||
|
|
||||||
// Terrain
|
// Terrain
|
||||||
var terrain = new MemoryNodeViewModel("Terrain");
|
var terrain = new MemoryNodeViewModel("Terrain");
|
||||||
_terrainCells = new MemoryNodeViewModel("Cells:");
|
_terrainCells = new MemoryNodeViewModel("Cells:");
|
||||||
_terrainGrid = new MemoryNodeViewModel("Grid:");
|
_terrainGrid = new MemoryNodeViewModel("Grid:");
|
||||||
|
_terrainWalkable = new MemoryNodeViewModel("Walkable:");
|
||||||
terrain.Children.Add(_terrainCells);
|
terrain.Children.Add(_terrainCells);
|
||||||
terrain.Children.Add(_terrainGrid);
|
terrain.Children.Add(_terrainGrid);
|
||||||
|
terrain.Children.Add(_terrainWalkable);
|
||||||
|
|
||||||
inGameStateGroup.Children.Add(areaInstanceGroup);
|
inGameStateGroup.Children.Add(areaInstanceGroup);
|
||||||
inGameStateGroup.Children.Add(player);
|
inGameStateGroup.Children.Add(player);
|
||||||
|
|
@ -198,7 +234,7 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
|
|
||||||
private async Task ReadLoop(CancellationToken ct)
|
private async Task ReadLoop(CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(500));
|
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(30));
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -240,6 +276,54 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_inGameState!.Set(
|
_inGameState!.Set(
|
||||||
snap.InGameStatePtr != 0 ? $"0x{snap.InGameStatePtr:X}" : "not found",
|
snap.InGameStatePtr != 0 ? $"0x{snap.InGameStatePtr:X}" : "not found",
|
||||||
snap.InGameStatePtr != 0);
|
snap.InGameStatePtr != 0);
|
||||||
|
_isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading);
|
||||||
|
_escapeStateNode!.Set(snap.IsEscapeOpen ? "Open" : "Closed", !snap.IsEscapeOpen);
|
||||||
|
|
||||||
|
// State Slots — show pointer + int32 at +0x08 for each state slot
|
||||||
|
if (_statesNode is not null && snap.StateSlots.Length > 0)
|
||||||
|
{
|
||||||
|
var slots = snap.StateSlots;
|
||||||
|
var needed = slots.Length;
|
||||||
|
|
||||||
|
while (_statesNode.Children.Count > needed)
|
||||||
|
_statesNode.Children.RemoveAt(_statesNode.Children.Count - 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < needed; i++)
|
||||||
|
{
|
||||||
|
var ptr = slots[i];
|
||||||
|
var stateName = i < GameMemoryReader.StateNames.Length ? GameMemoryReader.StateNames[i] : $"State{i}";
|
||||||
|
var label = $"[{i}] {stateName}:";
|
||||||
|
string val;
|
||||||
|
string color;
|
||||||
|
|
||||||
|
if (ptr == 0)
|
||||||
|
{
|
||||||
|
val = "null";
|
||||||
|
color = "#484f58";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Read int32 at state+0x08 (the value CE found)
|
||||||
|
var int32Val = snap.StateSlotValues?.Length > i ? snap.StateSlotValues[i] : 0;
|
||||||
|
val = $"0x{ptr:X} [+0x08]={int32Val}";
|
||||||
|
color = ptr == snap.InGameStatePtr ? "#3fb950" : "#8b949e";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < _statesNode.Children.Count)
|
||||||
|
{
|
||||||
|
_statesNode.Children[i].Name = label;
|
||||||
|
_statesNode.Children[i].Set(val, true);
|
||||||
|
_statesNode.Children[i].ValueColor = color;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var node = new MemoryNodeViewModel(label);
|
||||||
|
node.Set(val, true);
|
||||||
|
node.ValueColor = color;
|
||||||
|
_statesNode.Children.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Status text
|
// Status text
|
||||||
if (snap.Attached)
|
if (snap.Attached)
|
||||||
|
|
@ -292,18 +376,15 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
// Entities
|
// Entities
|
||||||
if (snap.Entities is { Count: > 0 })
|
if (snap.Entities is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var withPath = snap.Entities.Count(e => e.Path is not null);
|
|
||||||
var withPos = snap.Entities.Count(e => e.HasPosition);
|
var withPos = snap.Entities.Count(e => e.HasPosition);
|
||||||
_entitySummary!.Set($"{snap.Entities.Count} entities, {withPath} with path, {withPos} with pos");
|
var withComps = snap.Entities.Count(e => e.Components is not null);
|
||||||
|
var monsters = snap.Entities.Count(e => e.Type == Automata.Memory.EntityType.Monster);
|
||||||
|
var knownComps = _reader?.Registry["components"].Count ?? 0;
|
||||||
|
_entitySummary!.Set($"{snap.Entities.Count} total, {withComps} with comps, {knownComps} known, {monsters} monsters");
|
||||||
|
|
||||||
// Group by path category: "Metadata/Monsters/..." → "Monsters"
|
// Group by EntityType
|
||||||
var typeCounts = snap.Entities
|
var typeCounts = snap.Entities
|
||||||
.GroupBy(e =>
|
.GroupBy(e => e.Type)
|
||||||
{
|
|
||||||
if (e.Path is null) return "?";
|
|
||||||
var parts = e.Path.Split('/');
|
|
||||||
return parts.Length >= 2 ? parts[1] : "?";
|
|
||||||
})
|
|
||||||
.OrderByDescending(g => g.Count())
|
.OrderByDescending(g => g.Count())
|
||||||
.Take(20);
|
.Take(20);
|
||||||
|
|
||||||
|
|
@ -314,11 +395,15 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
node.Set(group.Count().ToString());
|
node.Set(group.Count().ToString());
|
||||||
_entityTypesNode.Children.Add(node);
|
_entityTypesNode.Children.Add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entity list grouped by type
|
||||||
|
UpdateEntityList(snap.Entities);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_entitySummary!.Set("—", false);
|
_entitySummary!.Set("—", false);
|
||||||
_entityTypesNode!.Children.Clear();
|
_entityTypesNode!.Children.Clear();
|
||||||
|
_entityListNode!.Children.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terrain
|
// Terrain
|
||||||
|
|
@ -326,11 +411,372 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
_terrainCells!.Set($"{snap.TerrainCols}x{snap.TerrainRows}");
|
_terrainCells!.Set($"{snap.TerrainCols}x{snap.TerrainRows}");
|
||||||
_terrainGrid!.Set($"{snap.TerrainWidth}x{snap.TerrainHeight}");
|
_terrainGrid!.Set($"{snap.TerrainWidth}x{snap.TerrainHeight}");
|
||||||
|
if (snap.Terrain != null)
|
||||||
|
_terrainWalkable!.Set($"{snap.TerrainWalkablePercent}%");
|
||||||
|
else
|
||||||
|
_terrainWalkable!.Set("no grid data", false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_terrainCells!.Set("?", false);
|
_terrainCells!.Set("?", false);
|
||||||
_terrainGrid!.Set("?", false);
|
_terrainGrid!.Set("?", false);
|
||||||
|
_terrainWalkable!.Set("?", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMinimap(snap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMinimap(GameStateSnapshot snap)
|
||||||
|
{
|
||||||
|
// Skip rendering entirely during loading — terrain data is stale/invalid
|
||||||
|
if (snap.IsLoading)
|
||||||
|
{
|
||||||
|
_terrainBasePixels = null;
|
||||||
|
_terrainImageAreaHash = 0;
|
||||||
|
_terrainGridRef = null;
|
||||||
|
var oldImg = TerrainImage;
|
||||||
|
TerrainImage = null;
|
||||||
|
oldImg?.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache when area changes or terrain grid object changes
|
||||||
|
var terrainChanged = snap.Terrain is not null && !ReferenceEquals(snap.Terrain, _terrainGridRef);
|
||||||
|
if (terrainChanged || (snap.AreaHash != 0 && snap.AreaHash != _terrainImageAreaHash))
|
||||||
|
{
|
||||||
|
_terrainBasePixels = null;
|
||||||
|
_terrainImageAreaHash = 0;
|
||||||
|
_terrainGridRef = null;
|
||||||
|
var old = TerrainImage;
|
||||||
|
TerrainImage = null;
|
||||||
|
old?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild base pixels from new terrain data
|
||||||
|
if (snap.Terrain is { } grid && _terrainBasePixels is null)
|
||||||
|
{
|
||||||
|
var w = grid.Width;
|
||||||
|
var h = grid.Height;
|
||||||
|
var pixels = new byte[w * h * 4];
|
||||||
|
for (var y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
var srcY = h - 1 - y; // flip Y: game Y-up → bitmap Y-down
|
||||||
|
for (var x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
var i = (y * w + x) * 4;
|
||||||
|
if (grid.Data[srcY * w + x] == 0) // walkable — transparent
|
||||||
|
{
|
||||||
|
pixels[i] = 0x00;
|
||||||
|
pixels[i + 1] = 0x00;
|
||||||
|
pixels[i + 2] = 0x00;
|
||||||
|
pixels[i + 3] = 0x00;
|
||||||
|
}
|
||||||
|
else // blocked
|
||||||
|
{
|
||||||
|
pixels[i] = 0x50; // B
|
||||||
|
pixels[i + 1] = 0x50; // G
|
||||||
|
pixels[i + 2] = 0x50; // R
|
||||||
|
pixels[i + 3] = 0xFF; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_terrainBasePixels = pixels;
|
||||||
|
_terrainImageWidth = w;
|
||||||
|
_terrainImageHeight = h;
|
||||||
|
_terrainImageAreaHash = snap.AreaHash;
|
||||||
|
_terrainGridRef = grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var basePixels = _terrainBasePixels;
|
||||||
|
if (basePixels is null) return;
|
||||||
|
|
||||||
|
var tw = _terrainImageWidth;
|
||||||
|
var th = _terrainImageHeight;
|
||||||
|
if (tw < 2 || th < 2) return;
|
||||||
|
|
||||||
|
// World-to-grid conversion: Render component gives world coords,
|
||||||
|
// terrain bitmap is in grid/subtile coords (NumCols*23 x NumRows*23).
|
||||||
|
// Each tile = 250 world units = 23 subtiles, so grid = world * 23/250.
|
||||||
|
const float worldToGrid = 23.0f / 250.0f;
|
||||||
|
const float cos45 = 0.70710678f;
|
||||||
|
const float sin45 = 0.70710678f;
|
||||||
|
|
||||||
|
var viewSize = ViewportRadius * 2;
|
||||||
|
|
||||||
|
// Player grid position — center of the output
|
||||||
|
float pgx, pgy;
|
||||||
|
if (snap.HasPosition)
|
||||||
|
{
|
||||||
|
pgx = snap.PlayerX * worldToGrid;
|
||||||
|
pgy = th - 1 - snap.PlayerY * worldToGrid;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pgx = tw * 0.5f;
|
||||||
|
pgy = th * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufSize = viewSize * viewSize * 4;
|
||||||
|
if (_minimapBuffer is null || _minimapBuffer.Length != bufSize)
|
||||||
|
_minimapBuffer = new byte[bufSize];
|
||||||
|
var buf = _minimapBuffer;
|
||||||
|
Array.Clear(buf, 0, bufSize);
|
||||||
|
|
||||||
|
var outStride = viewSize * 4;
|
||||||
|
var cx = viewSize * 0.5f;
|
||||||
|
var cy = viewSize * 0.5f;
|
||||||
|
|
||||||
|
// Sample terrain with -45° rotation baked in (nearest-neighbor, unsafe).
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = basePixels, dstPtr = buf)
|
||||||
|
{
|
||||||
|
var srcInt = (int*)srcPtr;
|
||||||
|
var dstInt = (int*)dstPtr;
|
||||||
|
for (var ry = 0; ry < viewSize; ry++)
|
||||||
|
{
|
||||||
|
var dy = ry - cy;
|
||||||
|
var baseX = -dy * sin45 + pgx;
|
||||||
|
var baseY = dy * cos45 + pgy;
|
||||||
|
for (var rx = 0; rx < viewSize; rx++)
|
||||||
|
{
|
||||||
|
var dx = rx - cx;
|
||||||
|
var sx = (int)(dx * cos45 + baseX);
|
||||||
|
var sy = (int)(dx * sin45 + baseY);
|
||||||
|
if ((uint)sx >= (uint)tw || (uint)sy >= (uint)th) continue;
|
||||||
|
|
||||||
|
dstInt[ry * viewSize + rx] = srcInt[sy * tw + sx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw entity dots — transform grid coords into rotated output space
|
||||||
|
if (snap.Entities is { Count: > 0 })
|
||||||
|
{
|
||||||
|
foreach (var e in snap.Entities)
|
||||||
|
{
|
||||||
|
if (!e.HasPosition) continue;
|
||||||
|
if (e.Type is not (EntityType.Player or EntityType.Monster or EntityType.Npc)) continue;
|
||||||
|
if (e.Address == snap.LocalPlayerPtr) continue;
|
||||||
|
if (e.Type == EntityType.Monster && e.IsDead) continue;
|
||||||
|
|
||||||
|
// Entity position relative to player in grid coords
|
||||||
|
var dx = e.X * worldToGrid - pgx;
|
||||||
|
var dy = (th - 1 - e.Y * worldToGrid) - pgy;
|
||||||
|
// Apply -45° rotation into output space
|
||||||
|
var ex = (int)(dx * cos45 + dy * sin45 + cx);
|
||||||
|
var ey = (int)(-dx * sin45 + dy * cos45 + cy);
|
||||||
|
|
||||||
|
if (ex < 0 || ex >= viewSize || ey < 0 || ey >= viewSize) continue;
|
||||||
|
|
||||||
|
byte b, g, r;
|
||||||
|
switch (e.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Player: // other players — green #3FB950
|
||||||
|
b = 0x50; g = 0xB9; r = 0x3F; break;
|
||||||
|
case EntityType.Npc: // orange #FF8C00
|
||||||
|
b = 0x00; g = 0x8C; r = 0xFF; break;
|
||||||
|
case EntityType.Monster: // red #FF4444
|
||||||
|
b = 0x44; g = 0x44; r = 0xFF; break;
|
||||||
|
default: continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawDot(buf, outStride, viewSize, viewSize, ex, ey, 4, b, g, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw player dot at center (white, on top)
|
||||||
|
if (snap.HasPosition)
|
||||||
|
DrawDot(buf, outStride, viewSize, viewSize, (int)cx, (int)cy, 5, 0xFF, 0xFF, 0xFF);
|
||||||
|
|
||||||
|
// Create WriteableBitmap
|
||||||
|
var bmp = new WriteableBitmap(
|
||||||
|
new PixelSize(viewSize, viewSize),
|
||||||
|
new Vector(96, 96),
|
||||||
|
Avalonia.Platform.PixelFormat.Bgra8888,
|
||||||
|
Avalonia.Platform.AlphaFormat.Premul);
|
||||||
|
|
||||||
|
using (var fb = bmp.Lock())
|
||||||
|
{
|
||||||
|
Marshal.Copy(buf, 0, fb.Address, buf.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var old2 = TerrainImage;
|
||||||
|
TerrainImage = bmp;
|
||||||
|
old2?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawDot(byte[] buf, int stride, int bw, int bh, int cx, int cy, int radius, byte b, byte g, byte r)
|
||||||
|
{
|
||||||
|
var outer = radius + 1;
|
||||||
|
for (var dy = -outer; dy <= outer; dy++)
|
||||||
|
{
|
||||||
|
for (var dx = -outer; dx <= outer; dx++)
|
||||||
|
{
|
||||||
|
var sx = cx + dx;
|
||||||
|
var sy = cy + dy;
|
||||||
|
if (sx < 0 || sx >= bw || sy < 0 || sy >= bh) continue;
|
||||||
|
|
||||||
|
var dist = MathF.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist > radius + 0.5f) continue;
|
||||||
|
|
||||||
|
var alpha = Math.Clamp(radius + 0.5f - dist, 0f, 1f);
|
||||||
|
var i = sy * stride + sx * 4;
|
||||||
|
buf[i] = (byte)(b * alpha + buf[i] * (1 - alpha));
|
||||||
|
buf[i + 1] = (byte)(g * alpha + buf[i + 1] * (1 - alpha));
|
||||||
|
buf[i + 2] = (byte)(r * alpha + buf[i + 2] * (1 - alpha));
|
||||||
|
buf[i + 3] = (byte)(Math.Max(alpha, buf[i + 3] / 255f) * 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateEntityList(List<Entity> entities)
|
||||||
|
{
|
||||||
|
if (_entityListNode is null) return;
|
||||||
|
|
||||||
|
// Group by type, sorted by type name
|
||||||
|
var groups = entities
|
||||||
|
.GroupBy(e => e.Type)
|
||||||
|
.OrderBy(g => g.Key.ToString());
|
||||||
|
|
||||||
|
// Build a lookup of existing type group nodes by name for reuse
|
||||||
|
var existingGroups = new Dictionary<string, MemoryNodeViewModel>();
|
||||||
|
foreach (var child in _entityListNode.Children)
|
||||||
|
existingGroups[child.Name] = child;
|
||||||
|
|
||||||
|
var usedGroups = new HashSet<string>();
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var groupName = $"{group.Key} ({group.Count()})";
|
||||||
|
usedGroups.Add(groupName);
|
||||||
|
|
||||||
|
if (!existingGroups.TryGetValue(groupName, out var groupNode))
|
||||||
|
{
|
||||||
|
groupNode = new MemoryNodeViewModel(groupName) { IsExpanded = false };
|
||||||
|
_entityListNode.Children.Add(groupNode);
|
||||||
|
existingGroups[groupName] = groupNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: players first, then by distance to 0,0 (or by id)
|
||||||
|
var sorted = group.OrderBy(e => e.Id).ToList();
|
||||||
|
|
||||||
|
// Rebuild children — reuse by index to reduce churn
|
||||||
|
while (groupNode.Children.Count > sorted.Count)
|
||||||
|
groupNode.Children.RemoveAt(groupNode.Children.Count - 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < sorted.Count; i++)
|
||||||
|
{
|
||||||
|
var e = sorted[i];
|
||||||
|
var label = FormatEntityName(e);
|
||||||
|
var value = FormatEntityValue(e);
|
||||||
|
|
||||||
|
if (i < groupNode.Children.Count)
|
||||||
|
{
|
||||||
|
var existing = groupNode.Children[i];
|
||||||
|
existing.Name = label;
|
||||||
|
existing.Set(value, e.HasPosition);
|
||||||
|
UpdateEntityChildren(existing, e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var node = new MemoryNodeViewModel(label) { IsExpanded = false };
|
||||||
|
node.Set(value, e.HasPosition);
|
||||||
|
UpdateEntityChildren(node, e);
|
||||||
|
groupNode.Children.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale type groups
|
||||||
|
for (var i = _entityListNode.Children.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (!usedGroups.Contains(_entityListNode.Children[i].Name))
|
||||||
|
_entityListNode.Children.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatEntityName(Entity e)
|
||||||
|
{
|
||||||
|
// Short name: last path segment without @instance
|
||||||
|
if (e.Path is not null)
|
||||||
|
{
|
||||||
|
var lastSlash = e.Path.LastIndexOf('/');
|
||||||
|
var name = lastSlash >= 0 ? e.Path[(lastSlash + 1)..] : e.Path;
|
||||||
|
var at = name.IndexOf('@');
|
||||||
|
if (at > 0) name = name[..at];
|
||||||
|
return $"[{e.Id}] {name}";
|
||||||
|
}
|
||||||
|
return $"[{e.Id}] ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatEntityValue(Entity e)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
if (e.HasVitals)
|
||||||
|
parts.Add(e.IsAlive ? "Alive" : "Dead");
|
||||||
|
|
||||||
|
if (e.HasPosition)
|
||||||
|
parts.Add($"({e.X:F0},{e.Y:F0})");
|
||||||
|
|
||||||
|
if (e.HasVitals)
|
||||||
|
parts.Add($"HP:{e.LifeCurrent}/{e.LifeTotal}");
|
||||||
|
|
||||||
|
if (e.Components is { Count: > 0 })
|
||||||
|
parts.Add($"{e.Components.Count} comps");
|
||||||
|
|
||||||
|
return parts.Count > 0 ? string.Join(" ", parts) : "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateEntityChildren(MemoryNodeViewModel node, Entity e)
|
||||||
|
{
|
||||||
|
// Build children: address, position, vitals, components
|
||||||
|
var needed = new List<(string name, string value, bool valid)>();
|
||||||
|
|
||||||
|
needed.Add(("Addr:", $"0x{e.Address:X}", true));
|
||||||
|
|
||||||
|
if (e.Path is not null)
|
||||||
|
needed.Add(("Path:", e.Path, true));
|
||||||
|
|
||||||
|
if (e.HasPosition)
|
||||||
|
needed.Add(("Pos:", $"({e.X:F1}, {e.Y:F1}, {e.Z:F1})", true));
|
||||||
|
|
||||||
|
if (e.HasVitals)
|
||||||
|
{
|
||||||
|
needed.Add(("Life:", $"{e.LifeCurrent} / {e.LifeTotal}", e.LifeCurrent > 0));
|
||||||
|
if (e.ManaTotal > 0)
|
||||||
|
needed.Add(("Mana:", $"{e.ManaCurrent} / {e.ManaTotal}", true));
|
||||||
|
if (e.EsTotal > 0)
|
||||||
|
needed.Add(("ES:", $"{e.EsCurrent} / {e.EsTotal}", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Components is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var compList = string.Join(", ", e.Components.OrderBy(c => c));
|
||||||
|
needed.Add(("Components:", compList, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse existing children by index
|
||||||
|
while (node.Children.Count > needed.Count)
|
||||||
|
node.Children.RemoveAt(node.Children.Count - 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < needed.Count; i++)
|
||||||
|
{
|
||||||
|
var (name, value, valid) = needed[i];
|
||||||
|
if (i < node.Children.Count)
|
||||||
|
{
|
||||||
|
node.Children[i].Name = name;
|
||||||
|
node.Children[i].Set(value, valid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var child = new MemoryNodeViewModel(name);
|
||||||
|
child.Set(value, valid);
|
||||||
|
node.Children.Add(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,6 +911,66 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
ScanResult = _reader.ScanEntities();
|
ScanResult = _reader.ScanEntities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanCompLookupExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.ScanComponentLookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanAreaLoadingExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.ScanAreaLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanDiffExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.ScanMemoryDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanActiveVecExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.ScanActiveStatesVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanTerrainExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.ScanTerrain();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ScanStructureExecute()
|
private void ScanStructureExecute()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,16 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image Source="{Binding MinimapImage}" Stretch="Uniform"
|
<Image Source="{Binding MinimapImage}" Stretch="Uniform"
|
||||||
RenderOptions.BitmapInterpolationMode="None" />
|
RenderOptions.BitmapInterpolationMode="None"
|
||||||
|
IsVisible="{Binding MemoryVm.TerrainImage, Converter={x:Static ObjectConverters.IsNull}}" />
|
||||||
<TextBlock Text="Idle"
|
<TextBlock Text="Idle"
|
||||||
IsVisible="{Binding MinimapImage, Converter={x:Static ObjectConverters.IsNull}}"
|
IsVisible="{Binding MinimapImage, Converter={x:Static ObjectConverters.IsNull}}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
FontSize="12" Foreground="#484f58" />
|
FontSize="12" Foreground="#484f58" />
|
||||||
|
<!-- Memory terrain overlay (when memory reader is enabled) -->
|
||||||
|
<Image Source="{Binding MemoryVm.TerrainImage}" Stretch="Uniform"
|
||||||
|
RenderOptions.BitmapInterpolationMode="None"
|
||||||
|
IsVisible="{Binding MemoryVm.TerrainImage, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
@ -730,28 +735,38 @@
|
||||||
<Button Content="Probe IGS" Command="{Binding ProbeExecuteCommand}"
|
<Button Content="Probe IGS" Command="{Binding ProbeExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
|
<WrapPanel Orientation="Horizontal" Margin="0,4,0,0">
|
||||||
<TextBlock Text="Vitals:" Foreground="#8b949e" FontSize="11"
|
<TextBlock Text="Vitals:" Foreground="#8b949e" FontSize="11"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" Margin="0,0,6,4" />
|
||||||
<TextBox Text="{Binding VitalHp}" Watermark="HP"
|
<TextBox Text="{Binding VitalHp}" Watermark="HP"
|
||||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
Width="60" FontFamily="Consolas" FontSize="11" Margin="0,0,6,4" />
|
||||||
<TextBox Text="{Binding VitalMana}" Watermark="Mana"
|
<TextBox Text="{Binding VitalMana}" Watermark="Mana"
|
||||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
Width="60" FontFamily="Consolas" FontSize="11" Margin="0,0,6,4" />
|
||||||
<TextBox Text="{Binding VitalEs}" Watermark="ES"
|
<TextBox Text="{Binding VitalEs}" Watermark="ES"
|
||||||
Width="60" FontFamily="Consolas" FontSize="11" />
|
Width="60" FontFamily="Consolas" FontSize="11" Margin="0,0,6,4" />
|
||||||
<Button Content="Scan Components" Command="{Binding ScanComponentsExecuteCommand}"
|
<Button Content="Scan Components" Command="{Binding ScanComponentsExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Deep Scan" Command="{Binding DeepScanExecuteCommand}"
|
<Button Content="Deep Scan" Command="{Binding DeepScanExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Diagnose Vitals" Command="{Binding DiagnoseVitalsExecuteCommand}"
|
<Button Content="Diagnose Vitals" Command="{Binding DiagnoseVitalsExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Diagnose Entity" Command="{Binding DiagnoseEntityExecuteCommand}"
|
<Button Content="Diagnose Entity" Command="{Binding DiagnoseEntityExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Scan Position" Command="{Binding ScanPositionExecuteCommand}"
|
<Button Content="Scan Position" Command="{Binding ScanPositionExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Scan Entities" Command="{Binding ScanEntitiesExecuteCommand}"
|
<Button Content="Scan Entities" Command="{Binding ScanEntitiesExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
</StackPanel>
|
<Button Content="Scan Comp Lookup" Command="{Binding ScanCompLookupExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Scan Terrain" Command="{Binding ScanTerrainExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Scan Loading" Command="{Binding ScanAreaLoadingExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Diff Scan" Command="{Binding ScanDiffExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Scan ActiveVec" Command="{Binding ScanActiveVecExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
</WrapPanel>
|
||||||
<TextBox Text="{Binding ScanResult, Mode=OneWay}" FontFamily="Consolas"
|
<TextBox Text="{Binding ScanResult, Mode=OneWay}" FontFamily="Consolas"
|
||||||
FontSize="10" Foreground="#e6edf3" Background="#0d1117"
|
FontSize="10" Foreground="#e6edf3" Background="#0d1117"
|
||||||
BorderBrush="#30363d" BorderThickness="1" CornerRadius="4"
|
BorderBrush="#30363d" BorderThickness="1" CornerRadius="4"
|
||||||
|
|
|
||||||
BIN
terrain.png
Normal file
BIN
terrain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue