From a8341e8232dbff292c7ea34120098aedf83e86f4 Mon Sep 17 00:00:00 2001 From: Boki Date: Mon, 2 Mar 2026 23:45:12 -0500 Subject: [PATCH] huge refactor --- data/poe2/areas.json | 96 ++ entities.json | 27 + src/Automata.Ui/ViewModels/MemoryViewModel.cs | 202 ++- src/Automata.Ui/ViewModels/RobotoViewModel.cs | 4 +- src/Automata.Ui/Views/MainWindow.axaml | 6 + src/Roboto.Core/EntitySnapshot.cs | 25 + src/Roboto.Core/IMemoryProvider.cs | 9 - src/Roboto.Core/SkillState.cs | 5 +- src/Roboto.Data/AreaNameLookup.cs | 42 + src/Roboto.Data/EntityClassifier.cs | 90 ++ src/Roboto.Data/EntityMapper.cs | 54 + src/Roboto.Data/MemoryPoller.cs | 52 +- src/Roboto.GameOffsets/Components/Actor.cs | 97 +- src/Roboto.Memory/Entity.cs | 112 +- src/Roboto.Memory/EntityReader.cs | 226 ++- src/Roboto.Memory/GameMemoryReader.cs | 118 +- src/Roboto.Memory/GameStateReader.cs | 42 +- src/Roboto.Memory/GameStateSnapshot.cs | 6 + src/Roboto.Memory/MemoryDiagnostics.cs | 1338 ++++++++++++++++- src/Roboto.Memory/MsvcStringReader.cs | 32 + src/Roboto.Memory/Roboto.Memory.csproj | 2 +- src/Roboto.Memory/SkillReader.cs | 226 +++ src/Roboto.Memory/States/AreaInstanceState.cs | 63 + src/Roboto.Memory/States/AreaLoadingState.cs | 36 + src/Roboto.Memory/States/GameStateType.cs | 22 + src/Roboto.Memory/States/GameStates.cs | 437 ++++++ src/Roboto.Memory/States/InGameStateReader.cs | 51 + src/Roboto.Memory/States/RemoteObject.cs | 44 + src/Roboto.Memory/States/WorldDataState.cs | 60 + 29 files changed, 3184 insertions(+), 340 deletions(-) create mode 100644 data/poe2/areas.json delete mode 100644 src/Roboto.Core/IMemoryProvider.cs create mode 100644 src/Roboto.Data/AreaNameLookup.cs create mode 100644 src/Roboto.Data/EntityClassifier.cs create mode 100644 src/Roboto.Data/EntityMapper.cs create mode 100644 src/Roboto.Memory/SkillReader.cs create mode 100644 src/Roboto.Memory/States/AreaInstanceState.cs create mode 100644 src/Roboto.Memory/States/AreaLoadingState.cs create mode 100644 src/Roboto.Memory/States/GameStateType.cs create mode 100644 src/Roboto.Memory/States/GameStates.cs create mode 100644 src/Roboto.Memory/States/InGameStateReader.cs create mode 100644 src/Roboto.Memory/States/RemoteObject.cs create mode 100644 src/Roboto.Memory/States/WorldDataState.cs diff --git a/data/poe2/areas.json b/data/poe2/areas.json new file mode 100644 index 0000000..762c7a1 --- /dev/null +++ b/data/poe2/areas.json @@ -0,0 +1,96 @@ +[ + { + "act": 1, + "areas": [ + { "id": "G1_1", "name": "The Riverbank", "level": 1, "order": 1, "wp": false, "connects": ["G1_town"] }, + { "id": "G1_town", "name": "Clearfell Encampment", "level": 15, "order": 2, "town": true, "wp": true, "connects": ["G1_1", "G1_2"] }, + { "id": "G1_2", "name": "Clearfell", "level": 2, "order": 3, "wp": true, "connects": ["G1_town", "G1_3", "G1_4"] }, + { "id": "G1_3", "name": "Mud Burrow", "level": 3, "order": 4, "wp": false, "connects": ["G1_2"] }, + { "id": "G1_4", "name": "The Grelwood", "level": 4, "order": 5, "wp": true, "connects": ["G1_2", "G1_5", "G1_6"] }, + { "id": "G1_5", "name": "The Red Vale", "level": 5, "order": 6, "wp": true, "connects": ["G1_4"] }, + { "id": "G1_6", "name": "The Grim Tangle", "level": 6, "order": 7, "wp": true, "connects": ["G1_4", "G1_7"] }, + { "id": "G1_7", "name": "Cemetery of the Eternals", "level": 7, "order": 8, "wp": true, "connects": ["G1_6", "G1_8", "G1_9", "G1_11"] }, + { "id": "G1_8", "name": "Mausoleum of the Praetor", "level": 8, "order": 9, "wp": true, "connects": ["G1_7"] }, + { "id": "G1_9", "name": "Tomb of the Consort", "level": 8, "order": 10, "wp": true, "connects": ["G1_7"] }, + { "id": "G1_10", "name": "Root Hollow", "level": 15, "order": 11, "wp": false, "connects": [] }, + { "id": "G1_11", "name": "Hunting Grounds", "level": 10, "order": 12, "wp": true, "connects": ["G1_7", "G1_12", "G1_13_1"] }, + { "id": "G1_12", "name": "Freythorn", "level": 11, "order": 13, "wp": true, "connects": ["G1_11"] }, + { "id": "G1_13_1", "name": "Ogham Farmlands", "level": 12, "order": 14, "wp": true, "connects": ["G1_11", "G1_13_2"] }, + { "id": "G1_13_2", "name": "Ogham Village", "level": 13, "order": 15, "wp": true, "connects": ["G1_13_1", "G1_14"] }, + { "id": "G1_14", "name": "The Manor Ramparts", "level": 14, "order": 16, "wp": true, "connects": ["G1_13_2", "G1_15"] }, + { "id": "G1_15", "name": "Ogham Manor", "level": 15, "order": 17, "wp": true, "connects": ["G1_14"] } + ] + }, + { + "act": 2, + "areas": [ + { "id": "G2_1", "name": "Vastiri Outskirts", "level": 16, "order": 1, "wp": true, "connects": ["G2_10_2"] }, + { "id": "G2_town", "name": "The Ardura Caravan", "level": 32, "order": 2, "town": true, "wp": true, "connects": ["G2_10_1", "G2_13"] }, + { "id": "G2_2", "name": "Traitor's Passage", "level": 19, "order": 3, "wp": true, "connects": ["G2_3", "G2_12_1"] }, + { "id": "G2_3", "name": "The Halani Gates", "level": 20, "order": 4, "wp": true, "connects": ["G2_2"] }, + { "id": "G2_4_1", "name": "Keth", "level": 21, "order": 5, "wp": true, "connects": ["G2_8", "G2_4_2"] }, + { "id": "G2_4_2", "name": "The Lost City", "level": 22, "order": 6, "wp": true, "connects": ["G2_4_1", "G2_5_2", "G2_4_3"] }, + { "id": "G2_4_3", "name": "Buried Shrines", "level": 23, "order": 7, "wp": true, "connects": ["G2_4_2"] }, + { "id": "G2_5_1", "name": "Mastodon Badlands", "level": 21, "order": 8, "wp": true, "connects": ["G2_12_1", "G2_5_2"] }, + { "id": "G2_5_2", "name": "The Bone Pits", "level": 22, "order": 9, "wp": true, "connects": ["G2_5_1", "G2_4_2"] }, + { "id": "Abyss_Intro", "name": "Lightless Passage", "level": 22, "order": 10, "wp": false, "connects": [] }, + { "id": "Abyss_Hub", "name": "The Well of Souls", "level": 22, "order": 11, "wp": false, "connects": [] }, + { "id": "G2_6", "name": "Valley of the Titans", "level": 21, "order": 12, "wp": true, "connects": ["G2_12_1", "G2_7"] }, + { "id": "G2_7", "name": "The Titan Grotto", "level": 22, "order": 13, "wp": true, "connects": ["G2_6"] }, + { "id": "G2_8", "name": "Deshar", "level": 28, "order": 14, "wp": true, "connects": ["G2_4_1", "G2_9_1"] }, + { "id": "G2_9_1", "name": "Path of Mourning", "level": 29, "order": 15, "wp": true, "connects": ["G2_8", "G2_9_2"] }, + { "id": "G2_9_2", "name": "The Spires of Deshar", "level": 30, "order": 16, "wp": true, "connects": ["G2_9_1"] }, + { "id": "G2_10_1", "name": "Mawdun Quarry", "level": 17, "order": 17, "wp": true, "connects": ["G2_10_2", "G2_town"] }, + { "id": "G2_10_2", "name": "Mawdun Mine", "level": 18, "order": 18, "wp": true, "connects": ["G2_1", "G2_10_1"] }, + { "id": "G2_12_1", "name": "The Dreadnought", "level": 31, "order": 19, "wp": true, "connects": ["G2_2", "G2_12_2", "G2_5_1", "G2_6"] }, + { "id": "G2_12_2", "name": "Dreadnought Vanguard", "level": 32, "order": 20, "wp": true, "connects": ["G2_12_1"] }, + { "id": "G2_13", "name": "Trial of the Sekhemas", "level": 22, "order": 21, "wp": true, "connects": ["G2_town"] } + ] + }, + { + "act": 3, + "areas": [ + { "id": "G3_1", "name": "Sandswept Marsh", "level": 33, "order": 1, "wp": true, "connects": ["G3_4", "G3_3"] }, + { "id": "G3_town", "name": "Ziggurat Encampment", "level": 44, "order": 2, "town": true, "wp": true, "connects": ["G3_3", "G3_2_1", "G3_8"] }, + { "id": "G3_2_1", "name": "Infested Barrens", "level": 35, "order": 3, "wp": true, "connects": ["G3_town", "G3_7", "G3_5"] }, + { "id": "G3_2_2", "name": "The Matlan Waterways", "level": 39, "order": 4, "wp": false, "connects": ["G3_3", "G3_5"] }, + { "id": "G3_3", "name": "Jungle Ruins", "level": 34, "order": 5, "wp": true, "connects": ["G3_1", "G3_4", "G3_town", "G3_2_2"] }, + { "id": "G3_4", "name": "The Venom Crypts", "level": 35, "order": 6, "wp": false, "connects": ["G3_1", "G3_3"] }, + { "id": "G3_5", "name": "Chimeral Wetlands", "level": 36, "order": 7, "wp": true, "connects": ["G3_2_2", "G3_2_1", "G3_6_1", "G3_10"] }, + { "id": "G3_6_1", "name": "Jiquani's Machinarium", "level": 37, "order": 8, "wp": true, "connects": ["G3_5", "G3_6_2"] }, + { "id": "G3_6_2", "name": "Jiquani's Sanctum", "level": 38, "order": 9, "wp": true, "connects": ["G3_6_1"] }, + { "id": "G3_7", "name": "The Azak Bog", "level": 36, "order": 10, "wp": true, "connects": ["G3_2_1"] }, + { "id": "G3_8", "name": "The Drowned City", "level": 40, "order": 11, "wp": true, "connects": ["G3_town", "G3_11", "G3_9"] }, + { "id": "G3_9", "name": "The Molten Vault", "level": 41, "order": 12, "wp": true, "connects": ["G3_8"] }, + { "id": "G3_10", "name": "The Trial of Chaos", "level": 38, "order": 13, "wp": true, "connects": ["G3_5", "G3_14"] }, + { "id": "G3_11", "name": "Apex of Filth", "level": 41, "order": 14, "wp": true, "connects": ["G3_8", "G3_12"] }, + { "id": "G3_12", "name": "Temple of Kopec", "level": 42, "order": 15, "wp": false, "connects": ["G3_11", "G3_14"] }, + { "id": "G3_14", "name": "Utzaal", "level": 43, "order": 16, "wp": true, "connects": ["G3_12", "G3_16", "G3_10"] }, + { "id": "G3_16", "name": "Aggorat", "level": 44, "order": 17, "wp": true, "connects": ["G3_14", "G3_17"] }, + { "id": "G3_17", "name": "The Black Chambers", "level": 45, "order": 18, "wp": true, "connects": ["G3_16"] } + ] + }, + { + "act": 4, + "areas": [ + { "id": "G4_town", "name": "Kingsmarch", "level": 53, "order": 1, "town": true, "wp": true, "connects": ["G4_1_1", "G4_5_1"] }, + { "id": "G4_1_1", "name": "Isle of Kin", "level": 46, "order": 2, "wp": true, "connects": ["G4_town", "G4_1_2"] }, + { "id": "G4_1_2", "name": "Volcanic Warrens", "level": 47, "order": 3, "wp": true, "connects": ["G4_1_1", "G4_7"] }, + { "id": "G4_2_1", "name": "Kedge Bay", "level": 46, "order": 4, "wp": true, "connects": ["G4_7", "G4_2_2"] }, + { "id": "G4_2_2", "name": "Journey's End", "level": 47, "order": 5, "wp": true, "connects": ["G4_2_1", "G4_3_1"] }, + { "id": "G4_3_1", "name": "Whakapanu Island", "level": 46, "order": 6, "wp": true, "connects": ["G4_2_2", "G4_3_2"] }, + { "id": "G4_3_2", "name": "Singing Caverns", "level": 47, "order": 7, "wp": true, "connects": ["G4_3_1", "G4_4_1"] }, + { "id": "G4_4_1", "name": "Eye of Hinekora", "level": 46, "order": 8, "wp": true, "connects": ["G4_3_2", "G4_4_2"] }, + { "id": "G4_4_2", "name": "Halls of the Dead", "level": 47, "order": 9, "wp": true, "connects": ["G4_4_1", "G4_4_3", "G4_8a"] }, + { "id": "G4_4_3", "name": "Trial of the Ancestors", "level": 51, "order": 10, "wp": true, "connects": ["G4_4_2", "G4_11_1a"] }, + { "id": "G4_5_1", "name": "Abandoned Prison", "level": 46, "order": 11, "wp": true, "connects": ["G4_town", "G4_5_2"] }, + { "id": "G4_5_2", "name": "Solitary Confinement", "level": 47, "order": 12, "wp": true, "connects": ["G4_5_1"] }, + { "id": "G4_7", "name": "Shrike Island", "level": 46, "order": 13, "wp": true, "connects": ["G4_1_2", "G4_2_1"] }, + { "id": "G4_8a", "name": "Arastas", "level": 52, "order": 14, "wp": true, "connects": ["G4_4_2", "G4_10"] }, + { "id": "G4_10", "name": "The Excavation", "level": 52, "order": 15, "wp": true, "connects": ["G4_8a", "G4_11_1a"] }, + { "id": "G4_11_1a", "name": "Ngakanu", "level": 53, "order": 16, "wp": true, "connects": ["G4_4_3", "G4_10", "G4_11_2"] }, + { "id": "G4_11_2", "name": "Heart of the Tribe", "level": 53, "order": 17, "wp": true, "connects": ["G4_11_1a"] }, + { "id": "G4_13", "name": "Plunder's Point", "level": 53, "order": 18, "wp": true, "connects": [] } + ] + } +] diff --git a/entities.json b/entities.json index 961d3fd..7383399 100644 --- a/entities.json +++ b/entities.json @@ -13,18 +13,24 @@ "Metadata/Chests/EzomyteChest_06", "Metadata/Chests/LeagueIncursion/EncounterChest", "Metadata/Chests/MossyChest11", + "Metadata/Chests/MossyChest13", "Metadata/Chests/MossyChest20", "Metadata/Chests/MossyChest21", "Metadata/Chests/MossyChest26", "Metadata/Critters/Chicken/Chicken_kingsmarch", + "Metadata/Critters/Crow/Crow", + "Metadata/Critters/Ferret/Ferret", "Metadata/Critters/Hedgehog/HedgehogSlow", "Metadata/Critters/Weta/Basic", "Metadata/Effects/Effect", + "Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummy", + "Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummyMarble", "Metadata/Effects/Microtransactions/foot_prints/delirium/footprints_delirium", "Metadata/Effects/Microtransactions/foot_prints/harvest02/footprints_harvest", "Metadata/Effects/PermanentEffect", "Metadata/Effects/ServerEffect", "Metadata/Effects/Spells/monsters_effects/Act1_FOUR/CarrionCrone/IceSpike", + "Metadata/Effects/Spells/sandstorm_swipe/sandstorm_swipe_storm", "Metadata/MiscellaneousObjects/AreaTransitionBlockage", "Metadata/MiscellaneousObjects/AreaTransitionDoodad", "Metadata/MiscellaneousObjects/AreaTransition_Animate", @@ -46,19 +52,28 @@ "Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_6", "Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalEncounter", "Metadata/MiscellaneousObjects/MultiplexPortal", + "Metadata/MiscellaneousObjects/ReviveIcon", "Metadata/MiscellaneousObjects/ServerDoodadHidden", "Metadata/MiscellaneousObjects/Stash", "Metadata/MiscellaneousObjects/Waypoint", "Metadata/MiscellaneousObjects/WorldItem", + "Metadata/Monsters/BansheeRemake/WitchHut/Objects/AmbushLocation", + "Metadata/Monsters/BansheeRemake/WitchHutBanshee", + "Metadata/Monsters/FungusZombie/FungusZombieLarge", + "Metadata/Monsters/FungusZombie/FungusZombieMedium", "Metadata/Monsters/Hags/Objects/BossRoomMinimapIcon", "Metadata/Monsters/Hags/UrchinHag1", "Metadata/Monsters/Hags/UrchinHagBoss", "Metadata/Monsters/InvisibleFire/MDCarrionCroneWave", + "Metadata/Monsters/MonsterMods/OnDeathColdExplosionParent", "Metadata/Monsters/Urchins/MeleeUrchin1", "Metadata/Monsters/Urchins/SlingUrchin1", + "Metadata/Monsters/Werewolves/WerewolfPack1", + "Metadata/Monsters/Werewolves/WerewolfProwler1", "Metadata/Monsters/Wolves/RottenWolf1_", "Metadata/Monsters/Wolves/RottenWolfDead", "Metadata/Monsters/Wolves/RottenWolfHagSummonedDead", + "Metadata/Monsters/Zombies/CourtGuardZombieAxe", "Metadata/Monsters/Zombies/CourtGuardZombieUnarmed", "Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxe", "Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxePhysics__", @@ -89,10 +104,16 @@ "Metadata/Pet/BabyChimera/BabyChimera", "Metadata/Pet/BetaKiwis/BaronKiwi", "Metadata/Pet/BetaKiwis/FaridunKiwi", + "Metadata/Pet/BetaKiwis/KaruiKiwi", + "Metadata/Pet/BetaKiwis/VaalKiwi", + "Metadata/Pet/BookAndQuillPet/BookAndQuillPet", "Metadata/Pet/BookAndQuillPet/BookAndQuillPet_Abyss", "Metadata/Pet/Cat/Sphynx/GiantSphynx/GiantSphynxBlack", + "Metadata/Pet/EtchedBeetlePet/EtchedBeetlePetAsala", "Metadata/Pet/FledglingBellcrow/FledglingBellcrow", + "Metadata/Pet/HeritagePeacock/HeritagePeacock", "Metadata/Pet/LandSharkPet/LandSharkPet", + "Metadata/Pet/LightBringerCat/LightbringerCat", "Metadata/Pet/OctopusParasite/OctopusParasiteCelestial", "Metadata/Pet/OrigamiPet/OrigamiPetBase", "Metadata/Pet/Phoenix/PhoenixPetBlue", @@ -100,10 +121,12 @@ "Metadata/Pet/Phoenix/PhoenixPetRed", "Metadata/Pet/QuadrillaPet/QuadrillaArmoured", "Metadata/Pet/ScavengerBat/ScavengerBat", + "Metadata/Pet/WayfinderWolf/WayfinderWolf", "Metadata/Projectiles/CarrionCroneIceSpear", "Metadata/Projectiles/HagBossIceShard", "Metadata/Projectiles/IceSpear", "Metadata/Projectiles/SlingUrchinProjectile", + "Metadata/Projectiles/Spark", "Metadata/Projectiles/Twister", "Metadata/Terrain/Doodads/Gallows/ClearfellBull1", "Metadata/Terrain/Doodads/Gallows/ClearfellBull1_CountKilled", @@ -112,6 +135,9 @@ "Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteChest", "Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteController", "Metadata/Terrain/Gallows/Act1/1_2/Objects/RuleSet", + "Metadata/Terrain/Gallows/Act1/1_4/Objects/HagCauldron", + "Metadata/Terrain/Gallows/Act1/1_4/Objects/SecretRoomMinimapIcon", + "Metadata/Terrain/Gallows/Act1/1_4/Objects/WitchHutTitle", "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", @@ -122,6 +148,7 @@ "Metadata/Terrain/Tools/AudioTools/G1_2/ForestEntrance", "Metadata/Terrain/Tools/AudioTools/G1_2/HagArena", "Metadata/Terrain/Tools/AudioTools/G1_2/RiverRapidsMedium", + "Metadata/Terrain/Tools/AudioTools/G1_4/WitchHutIndoorAudio", "Metadata/Terrain/Tools/AudioTools/G1_Town/FurnaceFireAudio", "Metadata/Terrain/Tools/AudioTools/G1_Town/InsideWaterMillAudio" ] \ No newline at end of file diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Automata.Ui/ViewModels/MemoryViewModel.cs index 3dec42c..fc5d58c 100644 --- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs +++ b/src/Automata.Ui/ViewModels/MemoryViewModel.cs @@ -7,6 +7,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Roboto.Memory; +using Roboto.Memory.States; namespace Automata.Ui.ViewModels; @@ -89,8 +90,10 @@ public partial class MemoryViewModel : ObservableObject private MemoryNodeViewModel? _playerLife; private MemoryNodeViewModel? _playerMana; private MemoryNodeViewModel? _playerEs; + private MemoryNodeViewModel? _currentStateNode; private MemoryNodeViewModel? _isLoadingNode; private MemoryNodeViewModel? _escapeStateNode; + private MemoryNodeViewModel? _activeStatesNode; private MemoryNodeViewModel? _statesNode; private MemoryNodeViewModel? _terrainCells; private MemoryNodeViewModel? _terrainGrid; @@ -98,6 +101,7 @@ public partial class MemoryViewModel : ObservableObject private MemoryNodeViewModel? _entitySummary; private MemoryNodeViewModel? _entityTypesNode; private MemoryNodeViewModel? _entityListNode; + private MemoryNodeViewModel? _skillsNode; partial void OnIsEnabledChanged(bool value) { @@ -163,16 +167,20 @@ public partial class MemoryViewModel : ObservableObject _gsController = new MemoryNodeViewModel("Controller:"); _gsStates = new MemoryNodeViewModel("States:"); _inGameState = new MemoryNodeViewModel("InGameState:"); + _currentStateNode = new MemoryNodeViewModel("Current State:"); _isLoadingNode = new MemoryNodeViewModel("Loading:"); _escapeStateNode = new MemoryNodeViewModel("Escape:"); + _activeStatesNode = new MemoryNodeViewModel("Controller") { IsExpanded = true }; _statesNode = new MemoryNodeViewModel("State Slots") { IsExpanded = true }; gameState.Children.Add(_gsPattern); gameState.Children.Add(_gsBase); gameState.Children.Add(_gsController); gameState.Children.Add(_gsStates); gameState.Children.Add(_inGameState); + gameState.Children.Add(_currentStateNode); gameState.Children.Add(_isLoadingNode); gameState.Children.Add(_escapeStateNode); + gameState.Children.Add(_activeStatesNode); gameState.Children.Add(_statesNode); // InGameState children @@ -199,10 +207,12 @@ public partial class MemoryViewModel : ObservableObject _playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" }; _playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" }; _playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" }; + _skillsNode = new MemoryNodeViewModel("Skills") { IsExpanded = false }; player.Children.Add(_playerPos); player.Children.Add(_playerLife); player.Children.Add(_playerMana); player.Children.Add(_playerEs); + player.Children.Add(_skillsNode); // Entities var entitiesGroup = new MemoryNodeViewModel("Entities"); @@ -276,9 +286,69 @@ public partial class MemoryViewModel : ObservableObject _inGameState!.Set( snap.InGameStatePtr != 0 ? $"0x{snap.InGameStatePtr:X}" : "not found", snap.InGameStatePtr != 0); + // Current game state + var currentState = snap.CurrentGameState; + var stateColor = currentState switch + { + GameStateType.InGameState => "#3fb950", // green + GameStateType.AreaLoadingState or + GameStateType.LoadingState => "#d29922", // yellow + GameStateType.EscapeState => "#f85149", // red + GameStateType.LoginState or + GameStateType.SelectCharacterState or + GameStateType.PreGameState => "#58a6ff", // blue + GameStateType.GameNotLoaded => "#484f58", // dim + _ => "#8b949e", // gray + }; + _currentStateNode!.Value = currentState.ToString(); + _currentStateNode.ValueColor = stateColor; + _isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading); _escapeStateNode!.Set(snap.IsEscapeOpen ? "Open" : "Closed", !snap.IsEscapeOpen); + // Controller dump — show qwords before state slots to find active state offset + if (_activeStatesNode is not null) + { + var pre = snap.ControllerPreSlots; + _activeStatesNode.Value = pre.Length > 0 + ? $"controller+0x00..0x{pre.Length * 8:X} ({pre.Length} qwords)" + : "no data"; + _activeStatesNode.ValueColor = "#8b949e"; + + while (_activeStatesNode.Children.Count > pre.Length) + _activeStatesNode.Children.RemoveAt(_activeStatesNode.Children.Count - 1); + + for (var i = 0; i < pre.Length; i++) + { + var (off, val, match, changed, derefInfo) = pre[i]; + var label = $"+0x{off:X2}:"; + var changeTag = changed ? " [CHANGED]" : ""; + var derefTag = derefInfo != null ? $" ({derefInfo})" : ""; + var display = val == 0 + ? "0" + : match != null + ? $"0x{val:X} ← {match}{changeTag}" + : $"0x{val:X}{derefTag}{changeTag}"; + var color = changed ? "#f85149" + : match != null ? "#3fb950" + : derefInfo != null && derefInfo.Contains('→') && derefInfo.Contains("State") ? "#d29922" // yellow for indirect state match + : val == 0 ? "#484f58" + : "#8b949e"; + + if (i < _activeStatesNode.Children.Count) + { + _activeStatesNode.Children[i].Name = label; + _activeStatesNode.Children[i].Value = display; + _activeStatesNode.Children[i].ValueColor = color; + } + else + { + var node = new MemoryNodeViewModel(label) { Value = display, ValueColor = color }; + _activeStatesNode.Children.Add(node); + } + } + } + // State Slots — show pointer + int32 at +0x08 for each state slot if (_statesNode is not null && snap.StateSlots.Length > 0) { @@ -303,10 +373,18 @@ public partial class MemoryViewModel : ObservableObject } 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"; + var isActive = snap.ActiveStates.Contains(ptr); + var activeTag = isActive ? " [ACTIVE]" : ""; + val = $"0x{ptr:X} [+0x08]={int32Val}{activeTag}"; + + // Green if current state, cyan if active, default gray + if (i < (int)GameStateType.GameNotLoaded && (GameStateType)i == snap.CurrentGameState) + color = "#3fb950"; // green — current state + else if (isActive) + color = "#58a6ff"; // blue — active but not current + else + color = "#8b949e"; // gray — inactive } if (i < _statesNode.Children.Count) @@ -325,11 +403,14 @@ public partial class MemoryViewModel : ObservableObject } } - // Status text + // Status text — show resolved current state if (snap.Attached) - StatusText = snap.InGameStatePtr != 0 - ? $"Attached (PID {snap.ProcessId}) — InGame" - : $"Attached (PID {snap.ProcessId})"; + { + var stateLabel = snap.CurrentGameState != GameStateType.GameNotLoaded + ? snap.CurrentGameState.ToString() + : snap.InGameStatePtr != 0 ? "InGame" : "unknown"; + StatusText = $"Attached (PID {snap.ProcessId}) — {stateLabel}"; + } else if (snap.Error is not null) StatusText = $"Error: {snap.Error}"; @@ -373,6 +454,57 @@ public partial class MemoryViewModel : ObservableObject _playerEs!.Set("? (set LifeComponentIndex)", false); } + // Player skills + if (_skillsNode is not null) + { + if (snap.PlayerSkills is { Count: > 0 }) + { + _skillsNode.Value = $"{snap.PlayerSkills.Count} skills"; + _skillsNode.ValueColor = "#3fb950"; + + while (_skillsNode.Children.Count > snap.PlayerSkills.Count) + _skillsNode.Children.RemoveAt(_skillsNode.Children.Count - 1); + + for (var i = 0; i < snap.PlayerSkills.Count; i++) + { + var skill = snap.PlayerSkills[i]; + var name = skill.Name ?? $"Skill#{i}"; + var label = $"[{i}] {name}:"; + + var parts = new List(); + parts.Add(skill.CanBeUsed ? "Ready" : "Cooldown"); + if (skill.UseStage != 0) + parts.Add($"stage:{skill.UseStage}"); + if (skill.CooldownTimeMs > 0) + parts.Add($"cd:{skill.CooldownTimeMs}ms"); + if (skill.MaxUses > 1) + parts.Add($"charges:{skill.MaxUses - skill.ActiveCooldowns}/{skill.MaxUses}"); + parts.Add($"cast:{skill.CastType}"); + + var value = string.Join(" ", parts); + var color = skill.CanBeUsed ? "#3fb950" : "#d29922"; + + if (i < _skillsNode.Children.Count) + { + _skillsNode.Children[i].Name = label; + _skillsNode.Children[i].Value = value; + _skillsNode.Children[i].ValueColor = color; + } + else + { + var node = new MemoryNodeViewModel(label) { Value = value, ValueColor = color }; + _skillsNode.Children.Add(node); + } + } + } + else + { + _skillsNode.Value = "—"; + _skillsNode.ValueColor = "#484f58"; + _skillsNode.Children.Clear(); + } + } + // Entities if (snap.Entities is { Count: > 0 }) { @@ -707,6 +839,8 @@ public partial class MemoryViewModel : ObservableObject var name = lastSlash >= 0 ? e.Path[(lastSlash + 1)..] : e.Path; var at = name.IndexOf('@'); if (at > 0) name = name[..at]; + if (e.TransitionName is not null) + return $"[{e.Id}] {name} → {e.TransitionName}"; return $"[{e.Id}] {name}"; } return $"[{e.Id}] ?"; @@ -716,6 +850,9 @@ public partial class MemoryViewModel : ObservableObject { var parts = new List(); + if (e.Rarity != 0) + parts.Add(e.Rarity.ToString()); + if (e.HasVitals) parts.Add(e.IsAlive ? "Alive" : "Dead"); @@ -725,6 +862,9 @@ public partial class MemoryViewModel : ObservableObject if (e.HasVitals) parts.Add($"HP:{e.LifeCurrent}/{e.LifeTotal}"); + if (e.ActionId != 0) + parts.Add($"act:0x{e.ActionId:X}"); + if (e.Components is { Count: > 0 }) parts.Add($"{e.Components.Count} comps"); @@ -741,6 +881,9 @@ public partial class MemoryViewModel : ObservableObject if (e.Path is not null) needed.Add(("Path:", e.Path, true)); + if (e.TransitionName is not null) + needed.Add(("Destination:", e.TransitionName, true)); + if (e.HasPosition) needed.Add(("Pos:", $"({e.X:F1}, {e.Y:F1}, {e.Z:F1})", true)); @@ -753,6 +896,15 @@ public partial class MemoryViewModel : ObservableObject needed.Add(("ES:", $"{e.EsCurrent} / {e.EsTotal}", true)); } + if (e.Rarity != 0) + needed.Add(("Rarity:", e.Rarity.ToString(), true)); + + if (e.ModNames is { Count: > 0 }) + { + foreach (var mod in e.ModNames) + needed.Add(("Mod:", mod, true)); + } + if (e.Components is { Count: > 0 }) { var compList = string.Join(", ", e.Components.OrderBy(c => c)); @@ -959,6 +1111,18 @@ public partial class MemoryViewModel : ObservableObject ScanResult = _reader.Diagnostics!.ScanActiveStatesVector(); } + [RelayCommand] + private void ScanStateDiffExecute() + { + if (_reader is null || !_reader.IsAttached) + { + ScanResult = "Error: not attached"; + return; + } + + ScanResult = _reader.Diagnostics!.ScanStateDiff(); + } + [RelayCommand] private void ScanTerrainExecute() { @@ -1009,4 +1173,28 @@ public partial class MemoryViewModel : ObservableObject ScanResult = _reader.Diagnostics!.CameraDiff(); } + + [RelayCommand] + private void ScanActorSkillsExecute() + { + if (_reader is null || !_reader.IsAttached) + { + ScanResult = "Error: not attached"; + return; + } + + ScanResult = _reader.Diagnostics!.ScanActorSkills(); + } + + [RelayCommand] + private void ScanActorDiffExecute() + { + if (_reader is null || !_reader.IsAttached) + { + ScanResult = "Error: not attached"; + return; + } + + ScanResult = _reader.Diagnostics!.ScanActorDiff(); + } } diff --git a/src/Automata.Ui/ViewModels/RobotoViewModel.cs b/src/Automata.Ui/ViewModels/RobotoViewModel.cs index d310c12..1aa4310 100644 --- a/src/Automata.Ui/ViewModels/RobotoViewModel.cs +++ b/src/Automata.Ui/ViewModels/RobotoViewModel.cs @@ -222,7 +222,9 @@ public partial class RobotoViewModel : ObservableObject, IDisposable Entities.Clear(); foreach (var e in state.Entities) { - var shortLabel = GetShortLabel(e.Path); + var shortLabel = e.Category == EntityCategory.AreaTransition && e.TransitionName is not null + ? $"AreaTransition — {e.TransitionName}" + : GetShortLabel(e.Path); var item = new EntityListItem(e.Id, shortLabel, e.Category.ToString(), e.DistanceToPlayer, e.Position.X, e.Position.Y); if (checkedIds.Contains(e.Id)) item.IsChecked = true; diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml index 7f1b152..0a4a15c 100644 --- a/src/Automata.Ui/Views/MainWindow.axaml +++ b/src/Automata.Ui/Views/MainWindow.axaml @@ -766,10 +766,16 @@ Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />