quests and queststate work
This commit is contained in:
parent
94b460bbc8
commit
445ae1387c
27 changed files with 3815 additions and 179 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -33,4 +33,5 @@ tools/python-detect/models/
|
||||||
nul
|
nul
|
||||||
|
|
||||||
# Extras
|
# Extras
|
||||||
lib/extras
|
lib/extras
|
||||||
|
lib/ExileCore-master
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"ControlZone",
|
"ControlZone",
|
||||||
"CritterAI",
|
"CritterAI",
|
||||||
"DiesAfterTime",
|
"DiesAfterTime",
|
||||||
|
"F",
|
||||||
"Functions",
|
"Functions",
|
||||||
"GlobalAudioParamEvents",
|
"GlobalAudioParamEvents",
|
||||||
"HideoutDoodad",
|
"HideoutDoodad",
|
||||||
|
|
|
||||||
103
entities.json
103
entities.json
|
|
@ -11,20 +11,35 @@
|
||||||
"Metadata/Chests/EzomyteChest_02",
|
"Metadata/Chests/EzomyteChest_02",
|
||||||
"Metadata/Chests/EzomyteChest_05",
|
"Metadata/Chests/EzomyteChest_05",
|
||||||
"Metadata/Chests/EzomyteChest_06",
|
"Metadata/Chests/EzomyteChest_06",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen1",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen2",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen3",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen4",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen5",
|
||||||
|
"Metadata/Chests/EzomyteStatueGreen7",
|
||||||
|
"Metadata/Chests/GallowsTutorialChest2",
|
||||||
"Metadata/Chests/LeagueIncursion/EncounterChest",
|
"Metadata/Chests/LeagueIncursion/EncounterChest",
|
||||||
|
"Metadata/Chests/MossyBoulder1",
|
||||||
|
"Metadata/Chests/MossyBoulder2",
|
||||||
"Metadata/Chests/MossyChest11",
|
"Metadata/Chests/MossyChest11",
|
||||||
|
"Metadata/Chests/MossyChest11MagicAndRare",
|
||||||
"Metadata/Chests/MossyChest13",
|
"Metadata/Chests/MossyChest13",
|
||||||
"Metadata/Chests/MossyChest14",
|
"Metadata/Chests/MossyChest14",
|
||||||
|
"Metadata/Chests/MossyChest14MagicAndRare",
|
||||||
|
"Metadata/Chests/MossyChest17",
|
||||||
"Metadata/Chests/MossyChest20",
|
"Metadata/Chests/MossyChest20",
|
||||||
"Metadata/Chests/MossyChest21",
|
"Metadata/Chests/MossyChest21",
|
||||||
"Metadata/Chests/MossyChest26",
|
"Metadata/Chests/MossyChest26",
|
||||||
"Metadata/Chests/MuddyChest1",
|
"Metadata/Chests/MuddyChest1",
|
||||||
|
"Metadata/Chests/SirenEggs/SirenEgg_02",
|
||||||
"Metadata/Critters/BloodWorm/BloodWormBrown",
|
"Metadata/Critters/BloodWorm/BloodWormBrown",
|
||||||
"Metadata/Critters/Chicken/Chicken_kingsmarch",
|
"Metadata/Critters/Chicken/Chicken_kingsmarch",
|
||||||
"Metadata/Critters/Crow/Crow",
|
"Metadata/Critters/Crow/Crow",
|
||||||
"Metadata/Critters/Ferret/Ferret",
|
"Metadata/Critters/Ferret/Ferret",
|
||||||
"Metadata/Critters/Hedgehog/HedgehogSlow",
|
"Metadata/Critters/Hedgehog/HedgehogSlow",
|
||||||
|
"Metadata/Critters/Spider/NurseryWebSpider",
|
||||||
"Metadata/Critters/Weta/Basic",
|
"Metadata/Critters/Weta/Basic",
|
||||||
|
"Metadata/Effects/BeamEffect",
|
||||||
"Metadata/Effects/Effect",
|
"Metadata/Effects/Effect",
|
||||||
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummy",
|
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummy",
|
||||||
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummyMarble",
|
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummyMarble",
|
||||||
|
|
@ -32,19 +47,37 @@
|
||||||
"Metadata/Effects/Microtransactions/foot_prints/harvest02/footprints_harvest",
|
"Metadata/Effects/Microtransactions/foot_prints/harvest02/footprints_harvest",
|
||||||
"Metadata/Effects/PermanentEffect",
|
"Metadata/Effects/PermanentEffect",
|
||||||
"Metadata/Effects/ServerEffect",
|
"Metadata/Effects/ServerEffect",
|
||||||
|
"Metadata/Effects/SleepableBeamEffect",
|
||||||
|
"Metadata/Effects/Spells/ground_effects/VisibleServerGroundEffect",
|
||||||
"Metadata/Effects/Spells/monsters_effects/Act1_FOUR/CarrionCrone/IceSpike",
|
"Metadata/Effects/Spells/monsters_effects/Act1_FOUR/CarrionCrone/IceSpike",
|
||||||
|
"Metadata/Effects/Spells/monsters_effects/Act1_FOUR/MudBurrower/mudburrower_chasm",
|
||||||
|
"Metadata/Effects/Spells/monsters_effects/Act1_FOUR/MudBurrower/mudburrower_chasm_body",
|
||||||
"Metadata/Effects/Spells/sandstorm_swipe/sandstorm_swipe_storm",
|
"Metadata/Effects/Spells/sandstorm_swipe/sandstorm_swipe_storm",
|
||||||
"Metadata/MiscellaneousObjects/AreaTransitionBlockage",
|
"Metadata/MiscellaneousObjects/AreaTransitionBlockage",
|
||||||
"Metadata/MiscellaneousObjects/AreaTransitionDoodad",
|
"Metadata/MiscellaneousObjects/AreaTransitionDoodad",
|
||||||
"Metadata/MiscellaneousObjects/AreaTransition_Animate",
|
"Metadata/MiscellaneousObjects/AreaTransition_Animate",
|
||||||
|
"Metadata/MiscellaneousObjects/BossTargetMarkerSerialized",
|
||||||
|
"Metadata/MiscellaneousObjects/BossTargetMarkerSerialized2",
|
||||||
|
"Metadata/MiscellaneousObjects/CameraZoom/MinorZoomIn",
|
||||||
|
"Metadata/MiscellaneousObjects/CameraZoom/TreeOfSouls",
|
||||||
"Metadata/MiscellaneousObjects/Checkpoint",
|
"Metadata/MiscellaneousObjects/Checkpoint",
|
||||||
|
"Metadata/MiscellaneousObjects/CheckpointTutorial",
|
||||||
"Metadata/MiscellaneousObjects/Doodad",
|
"Metadata/MiscellaneousObjects/Doodad",
|
||||||
"Metadata/MiscellaneousObjects/DoodadInvisible",
|
"Metadata/MiscellaneousObjects/DoodadInvisible",
|
||||||
"Metadata/MiscellaneousObjects/DoodadNoBlocking",
|
"Metadata/MiscellaneousObjects/DoodadNoBlocking",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/EnvLineEnd",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/EnvLineStart",
|
||||||
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_20_1",
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_20_1",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_5_1",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_6_1",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowBlocking_8_1",
|
||||||
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_4.75_1",
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_4.75_1",
|
||||||
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_6_4",
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_6_4",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_8_8",
|
||||||
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_4.75_1",
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_4.75_1",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_6_4",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_6_6",
|
||||||
|
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSource_8_8",
|
||||||
"Metadata/MiscellaneousObjects/GuildStash",
|
"Metadata/MiscellaneousObjects/GuildStash",
|
||||||
"Metadata/MiscellaneousObjects/HealingWell",
|
"Metadata/MiscellaneousObjects/HealingWell",
|
||||||
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_1",
|
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_1",
|
||||||
|
|
@ -58,19 +91,46 @@
|
||||||
"Metadata/MiscellaneousObjects/ReviveIcon",
|
"Metadata/MiscellaneousObjects/ReviveIcon",
|
||||||
"Metadata/MiscellaneousObjects/ServerDoodadHidden",
|
"Metadata/MiscellaneousObjects/ServerDoodadHidden",
|
||||||
"Metadata/MiscellaneousObjects/Stash",
|
"Metadata/MiscellaneousObjects/Stash",
|
||||||
|
"Metadata/MiscellaneousObjects/TargetMarker1",
|
||||||
|
"Metadata/MiscellaneousObjects/TutorialArrow",
|
||||||
"Metadata/MiscellaneousObjects/Waypoint",
|
"Metadata/MiscellaneousObjects/Waypoint",
|
||||||
"Metadata/MiscellaneousObjects/WorldItem",
|
"Metadata/MiscellaneousObjects/WorldItem",
|
||||||
"Metadata/Monsters/BansheeRemake/WitchHut/Objects/AmbushLocation",
|
"Metadata/Monsters/BansheeRemake/WitchHut/Objects/AmbushLocation",
|
||||||
"Metadata/Monsters/BansheeRemake/WitchHutBanshee",
|
"Metadata/Monsters/BansheeRemake/WitchHutBanshee",
|
||||||
|
"Metadata/Monsters/CarnivorousPlantEater/OldForest/BossRoomMinimapIcon",
|
||||||
|
"Metadata/Monsters/CarnivorousPlantEater/OldForest/CarnivorousPlantEaterOldForest_",
|
||||||
|
"Metadata/Monsters/Daemon/FungalBurstDaemon",
|
||||||
|
"Metadata/Monsters/FungusZombie/FungalBurstMushrooms/FungalBurstSpawner",
|
||||||
"Metadata/Monsters/FungusZombie/FungusZombieLarge",
|
"Metadata/Monsters/FungusZombie/FungusZombieLarge",
|
||||||
"Metadata/Monsters/FungusZombie/FungusZombieMedium",
|
"Metadata/Monsters/FungusZombie/FungusZombieMedium",
|
||||||
"Metadata/Monsters/Hags/Objects/BossRoomMinimapIcon",
|
"Metadata/Monsters/Hags/Objects/BossRoomMinimapIcon",
|
||||||
"Metadata/Monsters/Hags/UrchinHag1",
|
"Metadata/Monsters/Hags/UrchinHag1",
|
||||||
"Metadata/Monsters/Hags/UrchinHagBoss",
|
"Metadata/Monsters/Hags/UrchinHagBoss",
|
||||||
|
"Metadata/Monsters/HuhuGrub/CinematicHuhuGrub",
|
||||||
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeEmerge1",
|
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeEmerge1",
|
||||||
|
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeEmergeSummoned1_",
|
||||||
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeRanged1",
|
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeRanged1",
|
||||||
"Metadata/Monsters/InvisibleFire/MDCarrionCroneWave",
|
"Metadata/Monsters/InvisibleFire/MDCarrionCroneWave",
|
||||||
|
"Metadata/Monsters/MonsterMods/GroundOnDeath/ShockedGroundDaemonParent",
|
||||||
"Metadata/Monsters/MonsterMods/OnDeathColdExplosionParent",
|
"Metadata/Monsters/MonsterMods/OnDeathColdExplosionParent",
|
||||||
|
"Metadata/Monsters/MudBurrower/Arena_Blocker",
|
||||||
|
"Metadata/Monsters/MudBurrower/Arena_Blocker_Visual",
|
||||||
|
"Metadata/Monsters/MudBurrower/MudBurrowerBodyBoss",
|
||||||
|
"Metadata/Monsters/MudBurrower/MudBurrowerBodyDaemon",
|
||||||
|
"Metadata/Monsters/MudBurrower/MudBurrowerHeadBoss",
|
||||||
|
"Metadata/Monsters/MudBurrower/MudBurrowerTailBoss_",
|
||||||
|
"Metadata/Monsters/MudBurrower/Objects/BossRoomMinimapIcon",
|
||||||
|
"Metadata/Monsters/MudGolem/MudGolemWallAnimated",
|
||||||
|
"Metadata/Monsters/MudGolem/MudGolemWet1",
|
||||||
|
"Metadata/Monsters/MudGolem/MudGolemWetEncased1",
|
||||||
|
"Metadata/Monsters/NPC/DogTrader_",
|
||||||
|
"Metadata/Monsters/QuillCrab/QuillCrab",
|
||||||
|
"Metadata/Monsters/QuillCrab/QuillCrabBig",
|
||||||
|
"Metadata/Monsters/Skeletons/RetchSkeletonOneHandSword",
|
||||||
|
"Metadata/Monsters/Skeletons/RetchSkeletonOneHandSwordShield",
|
||||||
|
"Metadata/Monsters/SnakeFlowerMan/BloomSerpentEmerge1",
|
||||||
|
"Metadata/Monsters/SwollenMiller/Objects/BossRoomMinimapIcon",
|
||||||
|
"Metadata/Monsters/SwollenMiller/SwollenMillerBoss",
|
||||||
"Metadata/Monsters/Urchins/MeleeUrchin1",
|
"Metadata/Monsters/Urchins/MeleeUrchin1",
|
||||||
"Metadata/Monsters/Urchins/SlingUrchin1",
|
"Metadata/Monsters/Urchins/SlingUrchin1",
|
||||||
"Metadata/Monsters/Werewolves/WerewolfPack1",
|
"Metadata/Monsters/Werewolves/WerewolfPack1",
|
||||||
|
|
@ -84,6 +144,14 @@
|
||||||
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxePhysics__",
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxePhysics__",
|
||||||
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmed",
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmed",
|
||||||
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmedPhysics",
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmedPhysics",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedOneHandAxe",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedOneHandAxeHighAggroRangeMiller",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedOneHandAxePhysics",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedOneHandAxePhysicsHighAggroRangeMiller",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedUnarmed",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedUnarmedHighAggroRangeMiller",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedUnarmedPhysics",
|
||||||
|
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedUnarmedPhysicsHighAggroRangeMiller",
|
||||||
"Metadata/NPC/Four_Act1/ClearfellPosting1",
|
"Metadata/NPC/Four_Act1/ClearfellPosting1",
|
||||||
"Metadata/NPC/Four_Act1/ClearfellPosting3",
|
"Metadata/NPC/Four_Act1/ClearfellPosting3",
|
||||||
"Metadata/NPC/Four_Act1/DogTrader_Entrance",
|
"Metadata/NPC/Four_Act1/DogTrader_Entrance",
|
||||||
|
|
@ -96,6 +164,9 @@
|
||||||
"Metadata/NPC/Four_Act1/HoodedMentor",
|
"Metadata/NPC/Four_Act1/HoodedMentor",
|
||||||
"Metadata/NPC/Four_Act1/HoodedMentorAfterIronCount",
|
"Metadata/NPC/Four_Act1/HoodedMentorAfterIronCount",
|
||||||
"Metadata/NPC/Four_Act1/HoodedMentorInjured",
|
"Metadata/NPC/Four_Act1/HoodedMentorInjured",
|
||||||
|
"Metadata/NPC/Four_Act1/HoodedMentorOldForest",
|
||||||
|
"Metadata/NPC/Four_Act1/HoodedMentorOldForestInTree",
|
||||||
|
"Metadata/NPC/Four_Act1/OldForestGlyph1",
|
||||||
"Metadata/NPC/Four_Act1/Renly",
|
"Metadata/NPC/Four_Act1/Renly",
|
||||||
"Metadata/NPC/Four_Act1/RenlyAfterIronCount",
|
"Metadata/NPC/Four_Act1/RenlyAfterIronCount",
|
||||||
"Metadata/NPC/Four_Act1/RenlyIntro",
|
"Metadata/NPC/Four_Act1/RenlyIntro",
|
||||||
|
|
@ -103,6 +174,8 @@
|
||||||
"Metadata/NPC/Four_Act1/UnaAfterHealHoodedMentor",
|
"Metadata/NPC/Four_Act1/UnaAfterHealHoodedMentor",
|
||||||
"Metadata/NPC/Four_Act1/UnaAfterIronCount",
|
"Metadata/NPC/Four_Act1/UnaAfterIronCount",
|
||||||
"Metadata/NPC/Four_Act1/UnaHoodedOneInjured",
|
"Metadata/NPC/Four_Act1/UnaHoodedOneInjured",
|
||||||
|
"Metadata/NPC/Four_Act1/UnaTreeSummon",
|
||||||
|
"Metadata/NPC/Four_Act1/UnaTreeSummonKneeling",
|
||||||
"Metadata/NPC/League/Incursion/AlvaIncursionWild",
|
"Metadata/NPC/League/Incursion/AlvaIncursionWild",
|
||||||
"Metadata/Pet/AzmeriStag/AzmeriStag",
|
"Metadata/Pet/AzmeriStag/AzmeriStag",
|
||||||
"Metadata/Pet/BabyBossesHumans/BabyBrutus/BabyBrutus",
|
"Metadata/Pet/BabyBossesHumans/BabyBrutus/BabyBrutus",
|
||||||
|
|
@ -128,20 +201,48 @@
|
||||||
"Metadata/Pet/ScavengerBat/ScavengerBat",
|
"Metadata/Pet/ScavengerBat/ScavengerBat",
|
||||||
"Metadata/Pet/WayfinderWolf/WayfinderWolf",
|
"Metadata/Pet/WayfinderWolf/WayfinderWolf",
|
||||||
"Metadata/Projectiles/CarrionCroneIceSpear",
|
"Metadata/Projectiles/CarrionCroneIceSpear",
|
||||||
|
"Metadata/Projectiles/Fireball",
|
||||||
"Metadata/Projectiles/HagBossIceShard",
|
"Metadata/Projectiles/HagBossIceShard",
|
||||||
|
"Metadata/Projectiles/HuhuGrubLarvaeMortar",
|
||||||
"Metadata/Projectiles/IceSpear",
|
"Metadata/Projectiles/IceSpear",
|
||||||
|
"Metadata/Projectiles/MudBurrowerAcidMortarSmall",
|
||||||
|
"Metadata/Projectiles/MudBurrowerBloodProj",
|
||||||
|
"Metadata/Projectiles/MudBurrowerGoopMortar",
|
||||||
|
"Metadata/Projectiles/MudBurrowerGoopProjectile",
|
||||||
|
"Metadata/Projectiles/QuillCrabShrapnel",
|
||||||
|
"Metadata/Projectiles/QuillCrabSpike",
|
||||||
"Metadata/Projectiles/SlingUrchinProjectile",
|
"Metadata/Projectiles/SlingUrchinProjectile",
|
||||||
"Metadata/Projectiles/Spark",
|
"Metadata/Projectiles/Spark",
|
||||||
"Metadata/Projectiles/Twister",
|
"Metadata/Projectiles/Twister",
|
||||||
|
"Metadata/QuestObjects/Four_Act1/TreeOfSoulsRoots",
|
||||||
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1",
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1",
|
||||||
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1_CountKilled",
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1_CountKilled",
|
||||||
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2",
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2",
|
||||||
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2_CountKilled",
|
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2_CountKilled",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/EncampmentSpikeLadder",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/TutorialBlocker1",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/TutorialBlocker2",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/TutorialBlocker3",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/TutorialBlocker4",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_1/Objects/TutorialNPCZombie",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteChest",
|
"Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteChest",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteController",
|
"Metadata/Terrain/Gallows/Act1/1_2/Objects/CampsiteController",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_2/Objects/RuleSet",
|
"Metadata/Terrain/Gallows/Act1/1_2/Objects/RuleSet",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_3/Objects/MudGolemSpawners/MudGolemSpawnerBase01_02",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_3/Objects/MudGolemSpawners/MudGolemSpawnerBase01_06",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_3/Objects/MudGolemSpawners/MudGolemSpawnerBase02_011",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_3/Objects/MudGolemSpawners/MudGolemSpawnerBase02_03",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_3/Objects/SecretRoomMinimapIcon",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/AreaTransition_4a",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/BossRootDoor",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_4/Objects/HagCauldron",
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/HagCauldron",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/HoodedMentorController",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_4/Objects/SecretRoomMinimapIcon",
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/SecretRoomMinimapIcon",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/SummonAlly",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/TreeOfSoulsBodies",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/TreeOfSoulsNailStake1",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/TreeOfSoulsNailStake2",
|
||||||
|
"Metadata/Terrain/Gallows/Act1/1_4/Objects/TreeOfSoulsNailStake3",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_4/Objects/WitchHutTitle",
|
"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/Act1_finished_LightController",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBenchEzomyte",
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBenchEzomyte",
|
||||||
|
|
@ -149,12 +250,14 @@
|
||||||
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/CraftingBench_EnableRendering",
|
"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_DisableRendering",
|
||||||
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/VisitedAct2_EnableRendering",
|
"Metadata/Terrain/Gallows/Act1/1_town_ExileEncampment/Objects/VisitedAct2_EnableRendering",
|
||||||
|
"Metadata/Terrain/Tools/AudioTools/G1_1/TownEntrance",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_2/BurrowEntrance",
|
"Metadata/Terrain/Tools/AudioTools/G1_2/BurrowEntrance",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_2/ForestEntrance",
|
"Metadata/Terrain/Tools/AudioTools/G1_2/ForestEntrance",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_2/HagArena",
|
"Metadata/Terrain/Tools/AudioTools/G1_2/HagArena",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_2/RiverRapidsMedium",
|
"Metadata/Terrain/Tools/AudioTools/G1_2/RiverRapidsMedium",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_3/TunnelA",
|
"Metadata/Terrain/Tools/AudioTools/G1_3/TunnelA",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_4/WitchHutIndoorAudio",
|
"Metadata/Terrain/Tools/AudioTools/G1_4/WitchHutIndoorAudio",
|
||||||
|
"Metadata/Terrain/Tools/AudioTools/G1_5/OldForestEntrance",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_Town/FurnaceFireAudio",
|
"Metadata/Terrain/Tools/AudioTools/G1_Town/FurnaceFireAudio",
|
||||||
"Metadata/Terrain/Tools/AudioTools/G1_Town/InsideWaterMillAudio"
|
"Metadata/Terrain/Tools/AudioTools/G1_Town/InsideWaterMillAudio"
|
||||||
]
|
]
|
||||||
21
offsets.json
21
offsets.json
|
|
@ -50,10 +50,17 @@
|
||||||
"QuestCompanionObjPtrOffset": "0x10",
|
"QuestCompanionObjPtrOffset": "0x10",
|
||||||
"QuestTrackedMarker": "0x43020000",
|
"QuestTrackedMarker": "0x43020000",
|
||||||
"QuestObjEncounterStateOffset": 8,
|
"QuestObjEncounterStateOffset": 8,
|
||||||
"QuestDatRowSize": "0x77",
|
"QuestDatRowSize": "0x68",
|
||||||
"QuestDatNameOffset": 0,
|
"QuestDatNameOffset": 0,
|
||||||
|
"QuestDatOrderOffset": "0x10",
|
||||||
|
"QuestDatTextOffset": "0x34",
|
||||||
|
"QuestDatMessageOffset": "0x3D",
|
||||||
"QuestDatInternalIdOffset": "0x6B",
|
"QuestDatInternalIdOffset": "0x6B",
|
||||||
"QuestDatActOffset": "0x73",
|
"QuestDatActOffset": "0x73",
|
||||||
|
"QuestStateObjectOffset": "0x900",
|
||||||
|
"QuestStateVectorOffset": "0x240",
|
||||||
|
"QuestStateEntrySize": 12,
|
||||||
|
"QuestStateMaxEntries": "0x100",
|
||||||
"ComponentListOffset": "0x10",
|
"ComponentListOffset": "0x10",
|
||||||
"EntityHeaderOffset": 8,
|
"EntityHeaderOffset": 8,
|
||||||
"ComponentLookupOffset": "0x28",
|
"ComponentLookupOffset": "0x28",
|
||||||
|
|
@ -94,5 +101,15 @@
|
||||||
"UiElementVisibleBit": 11,
|
"UiElementVisibleBit": 11,
|
||||||
"UiElementSizeOffset": "0x288",
|
"UiElementSizeOffset": "0x288",
|
||||||
"UiElementTextOffset": "0x448",
|
"UiElementTextOffset": "0x448",
|
||||||
"UiElementScanRange": "0x1000"
|
"UiElementScanRange": "0x1000",
|
||||||
|
"QuestLinkedListOffset": "0x358",
|
||||||
|
"QuestLinkedListNodeSize": "0x28",
|
||||||
|
"QuestNodeQuestPtrOffset": "0x10",
|
||||||
|
"QuestNodeStateIdOffset": "0x20",
|
||||||
|
"QuestObjNamePtrOffset": 0,
|
||||||
|
"QuestLinkedListMaxNodes": "0x100",
|
||||||
|
"TrackedQuestPanelChildIndex": 6,
|
||||||
|
"TrackedQuestPanelSubChildIndex": 1,
|
||||||
|
"TrackedQuestLinkedListOffset": "0x318",
|
||||||
|
"QuestStateObjTextOffset": "0x34"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
{
|
{
|
||||||
"GooGoGaaGa": "GooGoGaaGa_Default_Copy"
|
"GooGoGaaGa": "GooGoGaaGa_Default_Copy",
|
||||||
|
"terdsare": "terdsare_Default",
|
||||||
|
"dudemoko": "dudemoko_Default"
|
||||||
}
|
}
|
||||||
166
profiles/dudemoko_Default.json
Normal file
166
profiles/dudemoko_Default.json
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
{
|
||||||
|
"Name": "dudemoko_Default",
|
||||||
|
"CreatedAt": "2026-03-05T05:44:54.8014147Z",
|
||||||
|
"LastModified": "2026-03-05T05:44:54.8014154Z",
|
||||||
|
"Flasks": {
|
||||||
|
"LifeFlaskThreshold": 50,
|
||||||
|
"ManaFlaskThreshold": 50,
|
||||||
|
"FlaskCooldownMs": 4000,
|
||||||
|
"LifeFlaskScanCode": 2,
|
||||||
|
"ManaFlaskScanCode": 3
|
||||||
|
},
|
||||||
|
"Combat": {
|
||||||
|
"GlobalCooldownMs": 500,
|
||||||
|
"AttackRange": 600,
|
||||||
|
"SafeRange": 400,
|
||||||
|
"KiteEnabled": false,
|
||||||
|
"KiteRange": 300,
|
||||||
|
"KiteDelayMs": 200
|
||||||
|
},
|
||||||
|
"Skills": [
|
||||||
|
{
|
||||||
|
"SlotIndex": 0,
|
||||||
|
"Label": "LMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "LeftClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 0,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 1,
|
||||||
|
"Label": "RMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "RightClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 1,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 2,
|
||||||
|
"Label": "MMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "MiddleClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 2,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 3,
|
||||||
|
"Label": "Q",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 16,
|
||||||
|
"Priority": 3,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 4,
|
||||||
|
"Label": "E",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 18,
|
||||||
|
"Priority": 4,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 5,
|
||||||
|
"Label": "R",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 19,
|
||||||
|
"Priority": 5,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 6,
|
||||||
|
"Label": "T",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 20,
|
||||||
|
"Priority": 6,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 7,
|
||||||
|
"Label": "F",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 33,
|
||||||
|
"Priority": 7,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
166
profiles/terdsare_Default.json
Normal file
166
profiles/terdsare_Default.json
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
{
|
||||||
|
"Name": "terdsare_Default",
|
||||||
|
"CreatedAt": "2026-03-05T04:11:41.4102481Z",
|
||||||
|
"LastModified": "2026-03-05T04:11:41.4102483Z",
|
||||||
|
"Flasks": {
|
||||||
|
"LifeFlaskThreshold": 50,
|
||||||
|
"ManaFlaskThreshold": 50,
|
||||||
|
"FlaskCooldownMs": 4000,
|
||||||
|
"LifeFlaskScanCode": 2,
|
||||||
|
"ManaFlaskScanCode": 3
|
||||||
|
},
|
||||||
|
"Combat": {
|
||||||
|
"GlobalCooldownMs": 500,
|
||||||
|
"AttackRange": 600,
|
||||||
|
"SafeRange": 400,
|
||||||
|
"KiteEnabled": false,
|
||||||
|
"KiteRange": 300,
|
||||||
|
"KiteDelayMs": 200
|
||||||
|
},
|
||||||
|
"Skills": [
|
||||||
|
{
|
||||||
|
"SlotIndex": 0,
|
||||||
|
"Label": "LMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "LeftClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 0,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 1,
|
||||||
|
"Label": "RMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "RightClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 1,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 2,
|
||||||
|
"Label": "MMB",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "MiddleClick",
|
||||||
|
"ScanCode": 0,
|
||||||
|
"Priority": 2,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 3,
|
||||||
|
"Label": "Q",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 16,
|
||||||
|
"Priority": 3,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 4,
|
||||||
|
"Label": "E",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 18,
|
||||||
|
"Priority": 4,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 5,
|
||||||
|
"Label": "R",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 19,
|
||||||
|
"Priority": 5,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 6,
|
||||||
|
"Label": "T",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 20,
|
||||||
|
"Priority": 6,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SlotIndex": 7,
|
||||||
|
"Label": "F",
|
||||||
|
"SkillName": null,
|
||||||
|
"InputType": "KeyPress",
|
||||||
|
"ScanCode": 33,
|
||||||
|
"Priority": 7,
|
||||||
|
"IsEnabled": true,
|
||||||
|
"CooldownMs": 300,
|
||||||
|
"RangeMin": 0,
|
||||||
|
"RangeMax": 600,
|
||||||
|
"TargetSelection": "Nearest",
|
||||||
|
"RequiresTarget": true,
|
||||||
|
"IsAura": false,
|
||||||
|
"IsMovementSkill": false,
|
||||||
|
"MinMonstersInRange": 1,
|
||||||
|
"MaintainPressed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,10 @@ public partial class MemoryNodeViewModel : ObservableObject
|
||||||
[ObservableProperty] private string _name;
|
[ObservableProperty] private string _name;
|
||||||
[ObservableProperty] private string _value = "";
|
[ObservableProperty] private string _value = "";
|
||||||
[ObservableProperty] private string _valueColor = "#484f58";
|
[ObservableProperty] private string _valueColor = "#484f58";
|
||||||
[ObservableProperty] private bool _isExpanded = true;
|
[ObservableProperty] private bool _isExpanded;
|
||||||
|
|
||||||
|
/// <summary>Optional back-reference to a UIElementNode for lazy child population.</summary>
|
||||||
|
public UIElementNode? UiElement { get; set; }
|
||||||
|
|
||||||
public ObservableCollection<MemoryNodeViewModel> Children { get; } = [];
|
public ObservableCollection<MemoryNodeViewModel> Children { get; } = [];
|
||||||
|
|
||||||
|
|
@ -93,8 +96,6 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
private MemoryNodeViewModel? _currentStateNode;
|
private MemoryNodeViewModel? _currentStateNode;
|
||||||
private MemoryNodeViewModel? _isLoadingNode;
|
private MemoryNodeViewModel? _isLoadingNode;
|
||||||
private MemoryNodeViewModel? _escapeStateNode;
|
private MemoryNodeViewModel? _escapeStateNode;
|
||||||
private MemoryNodeViewModel? _activeStatesNode;
|
|
||||||
private MemoryNodeViewModel? _statesNode;
|
|
||||||
private MemoryNodeViewModel? _terrainCells;
|
private MemoryNodeViewModel? _terrainCells;
|
||||||
private MemoryNodeViewModel? _terrainGrid;
|
private MemoryNodeViewModel? _terrainGrid;
|
||||||
private MemoryNodeViewModel? _terrainWalkable;
|
private MemoryNodeViewModel? _terrainWalkable;
|
||||||
|
|
@ -102,7 +103,6 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
private MemoryNodeViewModel? _entityTypesNode;
|
private MemoryNodeViewModel? _entityTypesNode;
|
||||||
private MemoryNodeViewModel? _entityListNode;
|
private MemoryNodeViewModel? _entityListNode;
|
||||||
private MemoryNodeViewModel? _skillsNode;
|
private MemoryNodeViewModel? _skillsNode;
|
||||||
private MemoryNodeViewModel? _questsNode;
|
|
||||||
private MemoryNodeViewModel? _areaRawName;
|
private MemoryNodeViewModel? _areaRawName;
|
||||||
private MemoryNodeViewModel? _areaDisplayName;
|
private MemoryNodeViewModel? _areaDisplayName;
|
||||||
private MemoryNodeViewModel? _areaAct;
|
private MemoryNodeViewModel? _areaAct;
|
||||||
|
|
@ -110,6 +110,8 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
private MemoryNodeViewModel? _areaHasWaypoint;
|
private MemoryNodeViewModel? _areaHasWaypoint;
|
||||||
private MemoryNodeViewModel? _areaMonsterLevel;
|
private MemoryNodeViewModel? _areaMonsterLevel;
|
||||||
private MemoryNodeViewModel? _worldAreaId;
|
private MemoryNodeViewModel? _worldAreaId;
|
||||||
|
private MemoryNodeViewModel? _uiElementsNode;
|
||||||
|
private MemoryNodeViewModel? _questLinkedListNode;
|
||||||
|
|
||||||
partial void OnIsEnabledChanged(bool value)
|
partial void OnIsEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
|
|
@ -178,8 +180,6 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_currentStateNode = new MemoryNodeViewModel("Current State:");
|
_currentStateNode = new MemoryNodeViewModel("Current State:");
|
||||||
_isLoadingNode = new MemoryNodeViewModel("Loading:");
|
_isLoadingNode = new MemoryNodeViewModel("Loading:");
|
||||||
_escapeStateNode = new MemoryNodeViewModel("Escape:");
|
_escapeStateNode = new MemoryNodeViewModel("Escape:");
|
||||||
_activeStatesNode = new MemoryNodeViewModel("Controller") { IsExpanded = true };
|
|
||||||
_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);
|
||||||
|
|
@ -188,8 +188,6 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
gameState.Children.Add(_currentStateNode);
|
gameState.Children.Add(_currentStateNode);
|
||||||
gameState.Children.Add(_isLoadingNode);
|
gameState.Children.Add(_isLoadingNode);
|
||||||
gameState.Children.Add(_escapeStateNode);
|
gameState.Children.Add(_escapeStateNode);
|
||||||
gameState.Children.Add(_activeStatesNode);
|
|
||||||
gameState.Children.Add(_statesNode);
|
|
||||||
|
|
||||||
// InGameState children
|
// InGameState children
|
||||||
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
|
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
|
||||||
|
|
@ -215,14 +213,12 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" };
|
_playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" };
|
||||||
_playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" };
|
_playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" };
|
||||||
_playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" };
|
_playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" };
|
||||||
_skillsNode = new MemoryNodeViewModel("Skills") { IsExpanded = false };
|
_skillsNode = new MemoryNodeViewModel("Skills");
|
||||||
_questsNode = new MemoryNodeViewModel("Quests") { IsExpanded = false };
|
|
||||||
player.Children.Add(_playerPos);
|
player.Children.Add(_playerPos);
|
||||||
player.Children.Add(_playerLife);
|
player.Children.Add(_playerLife);
|
||||||
player.Children.Add(_playerMana);
|
player.Children.Add(_playerMana);
|
||||||
player.Children.Add(_playerEs);
|
player.Children.Add(_playerEs);
|
||||||
player.Children.Add(_skillsNode);
|
player.Children.Add(_skillsNode);
|
||||||
player.Children.Add(_questsNode);
|
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
var entitiesGroup = new MemoryNodeViewModel("Entities");
|
var entitiesGroup = new MemoryNodeViewModel("Entities");
|
||||||
|
|
@ -259,11 +255,19 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
areaTemplateGroup.Children.Add(_areaMonsterLevel);
|
areaTemplateGroup.Children.Add(_areaMonsterLevel);
|
||||||
areaTemplateGroup.Children.Add(_worldAreaId);
|
areaTemplateGroup.Children.Add(_worldAreaId);
|
||||||
|
|
||||||
|
// Quest Linked Lists (all quests + tracked merged from GameUi)
|
||||||
|
_questLinkedListNode = new MemoryNodeViewModel("Quests");
|
||||||
|
|
||||||
|
// UIElements tree
|
||||||
|
_uiElementsNode = new MemoryNodeViewModel("UIElements") { IsExpanded = false };
|
||||||
|
|
||||||
inGameStateGroup.Children.Add(areaInstanceGroup);
|
inGameStateGroup.Children.Add(areaInstanceGroup);
|
||||||
inGameStateGroup.Children.Add(areaTemplateGroup);
|
inGameStateGroup.Children.Add(areaTemplateGroup);
|
||||||
inGameStateGroup.Children.Add(player);
|
inGameStateGroup.Children.Add(player);
|
||||||
inGameStateGroup.Children.Add(entitiesGroup);
|
inGameStateGroup.Children.Add(entitiesGroup);
|
||||||
inGameStateGroup.Children.Add(terrain);
|
inGameStateGroup.Children.Add(terrain);
|
||||||
|
inGameStateGroup.Children.Add(_questLinkedListNode);
|
||||||
|
inGameStateGroup.Children.Add(_uiElementsNode);
|
||||||
|
|
||||||
RootNodes.Add(process);
|
RootNodes.Add(process);
|
||||||
RootNodes.Add(gameState);
|
RootNodes.Add(gameState);
|
||||||
|
|
@ -334,103 +338,6 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading);
|
_isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading);
|
||||||
_escapeStateNode!.Set(snap.IsEscapeOpen ? "Open" : "Closed", !snap.IsEscapeOpen);
|
_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)
|
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
|
||||||
var int32Val = snap.StateSlotValues?.Length > i ? snap.StateSlotValues[i] : 0;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
_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 — show resolved current state
|
// Status text — show resolved current state
|
||||||
if (snap.Attached)
|
if (snap.Attached)
|
||||||
{
|
{
|
||||||
|
|
@ -593,48 +500,106 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quest states with rich info from companion vector
|
// Quest Linked Lists (all quests + tracked merged from GameUi)
|
||||||
if (_questsNode is not null)
|
if (_questLinkedListNode is not null)
|
||||||
{
|
{
|
||||||
if (snap.QuestFlags is { Count: > 0 })
|
if (snap.QuestLinkedList is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var named = snap.QuestFlags.Count(q => q.QuestName is not null);
|
var active = snap.QuestLinkedList.Count(q => q.StateId > 0);
|
||||||
_questsNode.Value = $"{snap.QuestFlags.Count} quest states ({named} named)";
|
var tracked = snap.QuestLinkedList.Count(q => q.IsTracked);
|
||||||
_questsNode.ValueColor = "#3fb950";
|
_questLinkedListNode.Value = $"{snap.QuestLinkedList.Count} total, {active} active, {tracked} tracked";
|
||||||
|
_questLinkedListNode.ValueColor = "#3fb950";
|
||||||
|
|
||||||
while (_questsNode.Children.Count > snap.QuestFlags.Count)
|
// Only update children if expanded
|
||||||
_questsNode.Children.RemoveAt(_questsNode.Children.Count - 1);
|
if (_questLinkedListNode.IsExpanded)
|
||||||
|
|
||||||
for (var i = 0; i < snap.QuestFlags.Count; i++)
|
|
||||||
{
|
{
|
||||||
var q = snap.QuestFlags[i];
|
var sorted = snap.QuestLinkedList
|
||||||
var trackedPrefix = q.IsTracked ? "[T] " : "";
|
.Where(q => q.StateId > 0)
|
||||||
var stateLabel = q.StateId switch { 1 => "locked", 2 => "started", _ => $"s{q.StateId}" };
|
.OrderByDescending(q => q.IsTracked)
|
||||||
var label = $"{trackedPrefix}{q.QuestName ?? (q.QuestStateIndex > 0 ? $"#{q.QuestStateIndex}" : $"[{i}]")}";
|
.ThenByDescending(q => q.StateId > 0)
|
||||||
var value = q.InternalId is not null
|
.ThenBy(q => q.Act)
|
||||||
? $"idx={q.QuestStateIndex} {stateLabel} id={q.InternalId}"
|
.ThenBy(q => q.DisplayName)
|
||||||
: $"idx={q.QuestStateIndex} {stateLabel}";
|
.ToList();
|
||||||
|
|
||||||
var color = q.IsTracked ? "#58a6ff" : q.StateId == 2 ? "#8b949e" : "#484f58";
|
while (_questLinkedListNode.Children.Count > sorted.Count)
|
||||||
|
_questLinkedListNode.Children.RemoveAt(_questLinkedListNode.Children.Count - 1);
|
||||||
|
|
||||||
if (i < _questsNode.Children.Count)
|
for (var i = 0; i < sorted.Count; i++)
|
||||||
{
|
{
|
||||||
_questsNode.Children[i].Name = label;
|
var q = sorted[i];
|
||||||
_questsNode.Children[i].Value = value;
|
var prefix = q.IsTracked ? "[T] " : "";
|
||||||
_questsNode.Children[i].ValueColor = color;
|
var stateLabel = $"step {q.StateId}";
|
||||||
}
|
var label = $"{prefix}{q.DisplayName ?? q.InternalId ?? $"[{i}]"}";
|
||||||
else
|
var value = $"Act{q.Act} {stateLabel}";
|
||||||
{
|
|
||||||
var node = new MemoryNodeViewModel(label) { Value = value, ValueColor = color };
|
var color = q.IsTracked ? "#58a6ff" : "#d29922";
|
||||||
_questsNode.Children.Add(node);
|
|
||||||
|
MemoryNodeViewModel node;
|
||||||
|
if (i < _questLinkedListNode.Children.Count)
|
||||||
|
{
|
||||||
|
node = _questLinkedListNode.Children[i];
|
||||||
|
node.Name = label;
|
||||||
|
node.Value = value;
|
||||||
|
node.ValueColor = color;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node = new MemoryNodeViewModel(label) { Value = value, ValueColor = color };
|
||||||
|
_questLinkedListNode.Children.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate detail children when quest node is expanded
|
||||||
|
if (node.IsExpanded)
|
||||||
|
{
|
||||||
|
var details = new List<(string key, string val)>
|
||||||
|
{
|
||||||
|
("InternalId:", q.InternalId ?? "—"),
|
||||||
|
("DisplayName:", q.DisplayName ?? "—"),
|
||||||
|
("Act:", q.Act.ToString()),
|
||||||
|
("StateId:", q.StateId.ToString()),
|
||||||
|
("IsTracked:", q.IsTracked.ToString()),
|
||||||
|
};
|
||||||
|
if (q.ObjectiveText is not null)
|
||||||
|
details.Add(("Objective:", q.ObjectiveText));
|
||||||
|
if (q.QuestDatPtr != 0)
|
||||||
|
details.Add(("QuestDatPtr:", $"0x{q.QuestDatPtr:X}"));
|
||||||
|
|
||||||
|
while (node.Children.Count > details.Count)
|
||||||
|
node.Children.RemoveAt(node.Children.Count - 1);
|
||||||
|
|
||||||
|
for (var j = 0; j < details.Count; j++)
|
||||||
|
{
|
||||||
|
var (key, val) = details[j];
|
||||||
|
if (j < node.Children.Count)
|
||||||
|
{
|
||||||
|
node.Children[j].Name = key;
|
||||||
|
node.Children[j].Value = val;
|
||||||
|
node.Children[j].ValueColor = "#8b949e";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
node.Children.Add(new MemoryNodeViewModel(key) { Value = val, ValueColor = "#8b949e" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Seed placeholder so expand arrow shows
|
||||||
|
if (node.Children.Count == 0)
|
||||||
|
node.Children.Add(new MemoryNodeViewModel("...") { ValueColor = "#484f58" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Seed placeholder so expand arrow shows
|
||||||
|
if (_questLinkedListNode.Children.Count == 0)
|
||||||
|
_questLinkedListNode.Children.Add(new MemoryNodeViewModel("...") { ValueColor = "#484f58" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_questsNode.Value = "—";
|
_questLinkedListNode.Value = "—";
|
||||||
_questsNode.ValueColor = "#484f58";
|
_questLinkedListNode.ValueColor = "#484f58";
|
||||||
_questsNode.Children.Clear();
|
_questLinkedListNode.Children.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -688,9 +653,101 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
_terrainWalkable!.Set("?", false);
|
_terrainWalkable!.Set("?", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UIElements tree
|
||||||
|
UpdateUiElementsTree(snap.GameUiPtr);
|
||||||
|
|
||||||
UpdateMinimap(snap);
|
UpdateMinimap(snap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateUiElementsTree(nint gameUiPtr)
|
||||||
|
{
|
||||||
|
if (_uiElementsNode is null) return;
|
||||||
|
|
||||||
|
var uiElements = _reader?.UIElements;
|
||||||
|
if (gameUiPtr == 0 || uiElements is null)
|
||||||
|
{
|
||||||
|
_uiElementsNode.Value = "—";
|
||||||
|
_uiElementsNode.ValueColor = "#484f58";
|
||||||
|
_uiElementsNode.Children.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiElementsNode.Value = $"GameUi 0x{gameUiPtr:X}";
|
||||||
|
_uiElementsNode.ValueColor = "#3fb950";
|
||||||
|
_uiElementsNode.UiElement = new UIElementNode { Address = gameUiPtr, ChildCount = 1 };
|
||||||
|
|
||||||
|
// Read and sync children for all expanded nodes
|
||||||
|
SyncUiNodeLazy(_uiElementsNode, uiElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively syncs UI element tree nodes. For expanded nodes, reads children
|
||||||
|
/// from live memory and recurses. For collapsed nodes with children, ensures
|
||||||
|
/// a placeholder exists so the expand arrow is visible.
|
||||||
|
/// </summary>
|
||||||
|
private static void SyncUiNodeLazy(MemoryNodeViewModel vm, Roboto.Memory.Objects.UIElements uiElements)
|
||||||
|
{
|
||||||
|
var uiEl = vm.UiElement;
|
||||||
|
if (uiEl is null || uiEl.Address == 0) return;
|
||||||
|
|
||||||
|
if (!vm.IsExpanded)
|
||||||
|
{
|
||||||
|
// Collapsed — just ensure placeholder for expand arrow
|
||||||
|
if (uiEl.ChildCount > 0 && vm.Children.Count == 0)
|
||||||
|
vm.Children.Add(new MemoryNodeViewModel("...") { Value = "", ValueColor = "#484f58", IsExpanded = false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expanded — read children from live memory
|
||||||
|
var children = uiElements.ReadChildren(uiEl.Address);
|
||||||
|
if (children is null || children.Count == 0)
|
||||||
|
{
|
||||||
|
// Read failed or no children — show error hint
|
||||||
|
vm.Children.Clear();
|
||||||
|
if (uiEl.ChildCount > 0)
|
||||||
|
vm.Children.Add(new MemoryNodeViewModel("(read failed)") { Value = $"ChildCount={uiEl.ChildCount}", ValueColor = "#f85149", IsExpanded = false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove placeholder
|
||||||
|
if (vm.Children.Count == 1 && vm.Children[0].UiElement is null)
|
||||||
|
vm.Children.Clear();
|
||||||
|
|
||||||
|
// Trim excess
|
||||||
|
while (vm.Children.Count > children.Count)
|
||||||
|
vm.Children.RemoveAt(vm.Children.Count - 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < children.Count; i++)
|
||||||
|
{
|
||||||
|
var child = children[i];
|
||||||
|
var label = child.StringId ?? $"[{i}]";
|
||||||
|
var visTag = child.IsVisible ? "" : " [hidden]";
|
||||||
|
var childTag = child.ChildCount > 0 ? $" ({child.ChildCount} ch)" : "";
|
||||||
|
var textTag = child.Text is not null ? $" \"{child.Text}\"" : "";
|
||||||
|
var value = $"0x{child.Address:X}{visTag}{childTag}{textTag}";
|
||||||
|
var color = child.IsVisible ? "#3fb950" : "#484f58";
|
||||||
|
|
||||||
|
MemoryNodeViewModel childVm;
|
||||||
|
if (i < vm.Children.Count)
|
||||||
|
{
|
||||||
|
childVm = vm.Children[i];
|
||||||
|
childVm.Name = label;
|
||||||
|
childVm.Value = value;
|
||||||
|
childVm.ValueColor = color;
|
||||||
|
childVm.UiElement = child;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
childVm = new MemoryNodeViewModel(label)
|
||||||
|
{ Value = value, ValueColor = color, IsExpanded = false, UiElement = child };
|
||||||
|
vm.Children.Add(childVm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse — will read grandchildren if this child is expanded, or add placeholder if collapsed
|
||||||
|
SyncUiNodeLazy(childVm, uiElements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateMinimap(GameStateSnapshot snap)
|
private void UpdateMinimap(GameStateSnapshot snap)
|
||||||
{
|
{
|
||||||
// Skip rendering entirely during loading — terrain data is stale/invalid
|
// Skip rendering entirely during loading — terrain data is stale/invalid
|
||||||
|
|
@ -1390,6 +1447,90 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
ScanResult = _reader.Diagnostics!.ScanQuestStateObjects();
|
ScanResult = _reader.Diagnostics!.ScanQuestStateObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanQuestOffsetsExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ScanQuestStateOffsets();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanActiveQuestsExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ScanActiveQuests();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanQuestContainersExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ScanQuestStateContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanWorldDataVectorsExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ScanWorldDataVectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ProbeCompanionQuestObjectsExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ProbeCompanionQuestObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ScanQuestLinkedListExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ScanQuestLinkedList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ReadQuestLinkedListsExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ReadQuestLinkedLists();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ScanAreaTemplateExecute()
|
private void ScanAreaTemplateExecute()
|
||||||
{
|
{
|
||||||
|
|
@ -1401,4 +1542,18 @@ public partial class MemoryViewModel : ObservableObject
|
||||||
|
|
||||||
ScanResult = _reader.Diagnostics!.ScanAreaTemplate();
|
ScanResult = _reader.Diagnostics!.ScanAreaTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ProbeQuestAddressesExecute()
|
||||||
|
{
|
||||||
|
if (_reader is null || !_reader.IsAttached)
|
||||||
|
{
|
||||||
|
ScanResult = "Error: not attached";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses = "3E129519BB0";
|
||||||
|
|
||||||
|
ScanResult = _reader.Diagnostics!.ProbeQuestAddresses(addresses, 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,19 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
|
||||||
var config = new BotConfig();
|
var config = new BotConfig();
|
||||||
var reader = new GameMemoryReader();
|
var reader = new GameMemoryReader();
|
||||||
var humanizer = new Humanizer(config);
|
var humanizer = new Humanizer(config);
|
||||||
var input = new InterceptionInputController(humanizer);
|
// Try Interception driver first, fall back to SendInput
|
||||||
|
var interception = new InterceptionInputController(humanizer);
|
||||||
|
IInputController input;
|
||||||
|
if (interception.Initialize())
|
||||||
|
{
|
||||||
|
input = interception;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sendInput = new SendInputController(humanizer);
|
||||||
|
sendInput.Initialize();
|
||||||
|
input = sendInput;
|
||||||
|
}
|
||||||
|
|
||||||
_engine = new BotEngine(config, reader, input, humanizer);
|
_engine = new BotEngine(config, reader, input, humanizer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -784,8 +784,24 @@
|
||||||
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Quest Objects" Command="{Binding ScanQuestObjectsExecuteCommand}"
|
<Button Content="Quest Objects" Command="{Binding ScanQuestObjectsExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Quest Offsets" Command="{Binding ScanQuestOffsetsExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Active Quests" Command="{Binding ScanActiveQuestsExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Quest Containers" Command="{Binding ScanQuestContainersExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="WorldData Vectors" Command="{Binding ScanWorldDataVectorsExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Probe Quest Ptrs" Command="{Binding ProbeCompanionQuestObjectsExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Quest LinkedList" Command="{Binding ScanQuestLinkedListExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Read Quests" Command="{Binding ReadQuestLinkedListsExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
<Button Content="Area Template" Command="{Binding ScanAreaTemplateExecuteCommand}"
|
<Button Content="Area Template" Command="{Binding ScanAreaTemplateExecuteCommand}"
|
||||||
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
|
<Button Content="Probe Quest Addr" Command="{Binding ProbeQuestAddressesExecuteCommand}"
|
||||||
|
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
|
||||||
</WrapPanel>
|
</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"
|
||||||
|
|
@ -803,6 +819,11 @@
|
||||||
BorderThickness="1" CornerRadius="8" Padding="8">
|
BorderThickness="1" CornerRadius="8" Padding="8">
|
||||||
<TreeView ItemsSource="{Binding RootNodes}"
|
<TreeView ItemsSource="{Binding RootNodes}"
|
||||||
Background="Transparent">
|
Background="Transparent">
|
||||||
|
<TreeView.Styles>
|
||||||
|
<Style Selector="TreeViewItem">
|
||||||
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
|
</Style>
|
||||||
|
</TreeView.Styles>
|
||||||
<TreeView.ItemTemplate>
|
<TreeView.ItemTemplate>
|
||||||
<TreeDataTemplate ItemsSource="{Binding Children}"
|
<TreeDataTemplate ItemsSource="{Binding Children}"
|
||||||
x:DataType="vm:MemoryNodeViewModel">
|
x:DataType="vm:MemoryNodeViewModel">
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ public class GameState
|
||||||
public DangerLevel Danger { get; set; }
|
public DangerLevel Danger { get; set; }
|
||||||
public Matrix4x4? CameraMatrix { get; set; }
|
public Matrix4x4? CameraMatrix { get; set; }
|
||||||
public IReadOnlyList<QuestProgress> ActiveQuests { get; set; } = [];
|
public IReadOnlyList<QuestProgress> ActiveQuests { get; set; } = [];
|
||||||
|
/// <summary>Active quests as shown in the game UI (title + objectives).</summary>
|
||||||
|
public IReadOnlyList<UiQuestInfo> UiQuests { get; set; } = [];
|
||||||
|
|
||||||
// Derived (computed once per tick by GameStateEnricher)
|
// Derived (computed once per tick by GameStateEnricher)
|
||||||
public ThreatMap Threats { get; set; } = new();
|
public ThreatMap Threats { get; set; } = new();
|
||||||
|
|
|
||||||
12
src/Roboto.Core/UiQuestInfo.cs
Normal file
12
src/Roboto.Core/UiQuestInfo.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Roboto.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Active quest info as displayed in the game UI.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UiQuestInfo
|
||||||
|
{
|
||||||
|
/// <summary>Quest title (e.g. "Treacherous Ground").</summary>
|
||||||
|
public string? Title { get; init; }
|
||||||
|
/// <summary>Current quest objective texts.</summary>
|
||||||
|
public IReadOnlyList<string> Objectives { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Roboto.Core;
|
using Roboto.Core;
|
||||||
|
using Roboto.Memory;
|
||||||
|
|
||||||
namespace Roboto.Data;
|
namespace Roboto.Data;
|
||||||
|
|
||||||
|
|
@ -89,6 +90,18 @@ public sealed class GameDataCache
|
||||||
public volatile string? CurrentAreaName;
|
public volatile string? CurrentAreaName;
|
||||||
public volatile string? CharacterName;
|
public volatile string? CharacterName;
|
||||||
|
|
||||||
|
// ── UI tree root pointer (updated at 10Hz) — tree is read on-demand ──
|
||||||
|
public volatile nint GameUiPtr;
|
||||||
|
|
||||||
|
// ── Quest groups from UI element tree (updated at 10Hz) ──
|
||||||
|
public volatile IReadOnlyList<UiQuestGroup>? UiQuestGroups;
|
||||||
|
|
||||||
|
// ── Quest linked lists from GameUi (updated at 10Hz) ──
|
||||||
|
public volatile IReadOnlyList<QuestLinkedEntry>? QuestLinkedList;
|
||||||
|
|
||||||
|
// ── Quest states from AreaInstance sub-object (updated at 10Hz) ──
|
||||||
|
public volatile IReadOnlyList<QuestStateEntry>? QuestStates;
|
||||||
|
|
||||||
// ── Full GameState (updated at 10Hz) — for systems that need the complete object ──
|
// ── Full GameState (updated at 10Hz) — for systems that need the complete object ──
|
||||||
public volatile GameState? LatestState;
|
public volatile GameState? LatestState;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,10 @@ public sealed class MemoryPoller : IDisposable
|
||||||
_cache.AreaHash = state.AreaHash;
|
_cache.AreaHash = state.AreaHash;
|
||||||
_cache.AreaLevel = state.AreaLevel;
|
_cache.AreaLevel = state.AreaLevel;
|
||||||
_cache.CharacterName = state.Player.CharacterName;
|
_cache.CharacterName = state.Player.CharacterName;
|
||||||
|
_cache.GameUiPtr = snap.GameUiPtr;
|
||||||
|
_cache.UiQuestGroups = snap.UiQuestGroups;
|
||||||
|
_cache.QuestLinkedList = snap.QuestLinkedList;
|
||||||
|
_cache.QuestStates = snap.QuestStates;
|
||||||
_cache.LatestState = state;
|
_cache.LatestState = state;
|
||||||
_cache.ColdTickTimestamp = Environment.TickCount64;
|
_cache.ColdTickTimestamp = Environment.TickCount64;
|
||||||
|
|
||||||
|
|
@ -331,25 +335,42 @@ public sealed class MemoryPoller : IDisposable
|
||||||
|
|
||||||
if (snap.QuestFlags is { Count: > 0 })
|
if (snap.QuestFlags is { Count: > 0 })
|
||||||
{
|
{
|
||||||
state.ActiveQuests = snap.QuestFlags.Select(q => new QuestProgress
|
// StateId: 1=available/in-progress, 2=completed, 3+=special
|
||||||
{
|
// Filter to non-completed quests for ActiveQuests
|
||||||
QuestStateIndex = q.QuestStateIndex,
|
state.ActiveQuests = snap.QuestFlags
|
||||||
QuestName = q.QuestName,
|
.Where(q => q.StateId != 2) // exclude completed
|
||||||
InternalId = q.InternalId,
|
.Select(q => new QuestProgress
|
||||||
StateId = q.StateId,
|
{
|
||||||
IsTracked = q.IsTracked,
|
QuestStateIndex = q.QuestStateIndex,
|
||||||
StateText = q.StateText,
|
QuestName = q.QuestName,
|
||||||
ProgressText = q.ProgressText,
|
InternalId = q.InternalId,
|
||||||
}).ToList();
|
StateId = q.StateId,
|
||||||
|
IsTracked = q.IsTracked,
|
||||||
|
StateText = q.StateText,
|
||||||
|
ProgressText = q.ProgressText,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
if (_lastQuestCount != snap.QuestFlags.Count)
|
var activeCount = state.ActiveQuests.Count;
|
||||||
|
if (_lastQuestCount != activeCount)
|
||||||
{
|
{
|
||||||
var indices = string.Join(", ", snap.QuestFlags.Select(q => q.QuestStateIndex));
|
Log.Debug("Active quests: {Active}/{Total} (filtered ES!=2)",
|
||||||
Log.Debug("Quest state indices ({Count}): [{Indices}]", snap.QuestFlags.Count, indices);
|
activeCount, snap.QuestFlags.Count);
|
||||||
_lastQuestCount = snap.QuestFlags.Count;
|
_lastQuestCount = activeCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snap.UiQuestGroups is { Count: > 0 })
|
||||||
|
{
|
||||||
|
state.UiQuests = snap.UiQuestGroups.Select(g => new UiQuestInfo
|
||||||
|
{
|
||||||
|
Title = g.Title,
|
||||||
|
Objectives = g.Steps
|
||||||
|
.Where(s => s.Text is not null)
|
||||||
|
.Select(s => s.Text!)
|
||||||
|
.ToList(),
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (snap.Terrain is not null)
|
if (snap.Terrain is not null)
|
||||||
{
|
{
|
||||||
state.Terrain = new WalkabilitySnapshot
|
state.Terrain = new WalkabilitySnapshot
|
||||||
|
|
|
||||||
300
src/Roboto.Input/SendInputController.cs
Normal file
300
src/Roboto.Input/SendInputController.cs
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Roboto.Core;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Roboto.Input;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback input controller using Win32 SendInput with KEYEVENTF_SCANCODE.
|
||||||
|
/// Games read scan codes, so this works for POE2 without the Interception driver.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class SendInputController : IInputController
|
||||||
|
{
|
||||||
|
private static readonly Random Rng = new();
|
||||||
|
private readonly Humanizer? _humanizer;
|
||||||
|
|
||||||
|
public bool IsInitialized { get; private set; }
|
||||||
|
|
||||||
|
public SendInputController(Humanizer? humanizer = null)
|
||||||
|
{
|
||||||
|
_humanizer = humanizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Initialize()
|
||||||
|
{
|
||||||
|
IsInitialized = true;
|
||||||
|
Log.Information("SendInput controller initialized (fallback)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard ──
|
||||||
|
|
||||||
|
public void KeyDown(ushort scanCode)
|
||||||
|
{
|
||||||
|
var input = MakeKeyScanInput(scanCode, keyUp: false);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void KeyUp(ushort scanCode)
|
||||||
|
{
|
||||||
|
var input = MakeKeyScanInput(scanCode, keyUp: true);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void KeyPress(ushort scanCode, int holdMs = 50)
|
||||||
|
{
|
||||||
|
if (_humanizer is not null)
|
||||||
|
{
|
||||||
|
if (_humanizer.ShouldThrottle()) return;
|
||||||
|
holdMs = _humanizer.GaussianDelay(holdMs);
|
||||||
|
_humanizer.RecordAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDown(scanCode);
|
||||||
|
Thread.Sleep(holdMs);
|
||||||
|
KeyUp(scanCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mouse movement ──
|
||||||
|
|
||||||
|
public void MouseMoveTo(int x, int y)
|
||||||
|
{
|
||||||
|
SetCursorPos(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MouseMoveBy(int dx, int dy)
|
||||||
|
{
|
||||||
|
if (!GetCursorPos(out var pt)) return;
|
||||||
|
SetCursorPos(pt.X + dx, pt.Y + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SmoothMoveTo(int x, int y)
|
||||||
|
{
|
||||||
|
if (!GetCursorPos(out var pt)) { MouseMoveTo(x, y); return; }
|
||||||
|
|
||||||
|
var dx = (double)(x - pt.X);
|
||||||
|
var dy = (double)(y - pt.Y);
|
||||||
|
var distance = Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 15) { MouseMoveTo(x, y); return; }
|
||||||
|
|
||||||
|
var perpX = -dy / distance;
|
||||||
|
var perpY = dx / distance;
|
||||||
|
var spread = distance * 0.15;
|
||||||
|
|
||||||
|
var cp1X = pt.X + dx * 0.3 + perpX * (Rng.NextDouble() - 0.5) * spread;
|
||||||
|
var cp1Y = pt.Y + dy * 0.3 + perpY * (Rng.NextDouble() - 0.5) * spread;
|
||||||
|
var cp2X = pt.X + dx * 0.7 + perpX * (Rng.NextDouble() - 0.5) * spread;
|
||||||
|
var cp2Y = pt.Y + dy * 0.7 + perpY * (Rng.NextDouble() - 0.5) * spread;
|
||||||
|
|
||||||
|
var steps = Math.Clamp((int)Math.Round(distance / 15), 10, 40);
|
||||||
|
|
||||||
|
for (var i = 1; i <= steps; i++)
|
||||||
|
{
|
||||||
|
var t = EaseInOutQuad((double)i / steps);
|
||||||
|
var (bx, by) = CubicBezier(t, pt.X, pt.Y, cp1X, cp1Y, cp2X, cp2Y, x, y);
|
||||||
|
MouseMoveTo((int)Math.Round(bx), (int)Math.Round(by));
|
||||||
|
Thread.Sleep(2 + Rng.Next(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseMoveTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mouse clicks ──
|
||||||
|
|
||||||
|
public void LeftClick(int x, int y)
|
||||||
|
{
|
||||||
|
if (_humanizer is not null)
|
||||||
|
{
|
||||||
|
if (_humanizer.ShouldThrottle()) return;
|
||||||
|
(x, y) = _humanizer.JitterPosition(x, y);
|
||||||
|
Thread.Sleep(_humanizer.GaussianDelay(10));
|
||||||
|
_humanizer.RecordAction();
|
||||||
|
}
|
||||||
|
SmoothMoveTo(x, y);
|
||||||
|
Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10);
|
||||||
|
MouseClick(MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, _humanizer?.GaussianDelay(50) ?? 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RightClick(int x, int y)
|
||||||
|
{
|
||||||
|
if (_humanizer is not null)
|
||||||
|
{
|
||||||
|
if (_humanizer.ShouldThrottle()) return;
|
||||||
|
(x, y) = _humanizer.JitterPosition(x, y);
|
||||||
|
Thread.Sleep(_humanizer.GaussianDelay(10));
|
||||||
|
_humanizer.RecordAction();
|
||||||
|
}
|
||||||
|
SmoothMoveTo(x, y);
|
||||||
|
Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10);
|
||||||
|
MouseClick(MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, _humanizer?.GaussianDelay(50) ?? 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MiddleClick(int x, int y)
|
||||||
|
{
|
||||||
|
if (_humanizer is not null)
|
||||||
|
{
|
||||||
|
if (_humanizer.ShouldThrottle()) return;
|
||||||
|
(x, y) = _humanizer.JitterPosition(x, y);
|
||||||
|
Thread.Sleep(_humanizer.GaussianDelay(10));
|
||||||
|
_humanizer.RecordAction();
|
||||||
|
}
|
||||||
|
SmoothMoveTo(x, y);
|
||||||
|
Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10);
|
||||||
|
MouseClick(MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, _humanizer?.GaussianDelay(50) ?? 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LeftDown()
|
||||||
|
{
|
||||||
|
var input = MakeMouseInput(MOUSEEVENTF_LEFTDOWN);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LeftUp()
|
||||||
|
{
|
||||||
|
var input = MakeMouseInput(MOUSEEVENTF_LEFTUP);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RightDown()
|
||||||
|
{
|
||||||
|
var input = MakeMouseInput(MOUSEEVENTF_RIGHTDOWN);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RightUp()
|
||||||
|
{
|
||||||
|
var input = MakeMouseInput(MOUSEEVENTF_RIGHTUP);
|
||||||
|
SendInput(1, [input], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private helpers ──
|
||||||
|
|
||||||
|
private void MouseClick(uint downFlag, uint upFlag, int holdMs)
|
||||||
|
{
|
||||||
|
var down = MakeMouseInput(downFlag);
|
||||||
|
var up = MakeMouseInput(upFlag);
|
||||||
|
SendInput(1, [down], INPUT_SIZE);
|
||||||
|
Thread.Sleep(holdMs);
|
||||||
|
SendInput(1, [up], INPUT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double EaseInOutQuad(double t) =>
|
||||||
|
t < 0.5 ? 2 * t * t : 1 - Math.Pow(-2 * t + 2, 2) / 2;
|
||||||
|
|
||||||
|
private static (double X, double Y) CubicBezier(double t,
|
||||||
|
double p0x, double p0y, double p1x, double p1y,
|
||||||
|
double p2x, double p2y, double p3x, double p3y)
|
||||||
|
{
|
||||||
|
var u = 1 - t;
|
||||||
|
var u2 = u * u;
|
||||||
|
var u3 = u2 * u;
|
||||||
|
var t2 = t * t;
|
||||||
|
var t3 = t2 * t;
|
||||||
|
return (
|
||||||
|
u3 * p0x + 3 * u2 * t * p1x + 3 * u * t2 * p2x + t3 * p3x,
|
||||||
|
u3 * p0y + 3 * u2 * t * p1y + 3 * u * t2 * p2y + t3 * p3y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Win32 SendInput P/Invoke ──
|
||||||
|
|
||||||
|
private const int INPUT_KEYBOARD = 1;
|
||||||
|
private const int INPUT_MOUSE = 0;
|
||||||
|
private const uint KEYEVENTF_SCANCODE = 0x0008;
|
||||||
|
private const uint KEYEVENTF_KEYUP = 0x0002;
|
||||||
|
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
|
||||||
|
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
|
||||||
|
private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
|
||||||
|
private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
|
||||||
|
private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
|
||||||
|
private const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
|
||||||
|
|
||||||
|
private static readonly int INPUT_SIZE = Marshal.SizeOf<INPUT>();
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT { public int X; public int Y; }
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct INPUT
|
||||||
|
{
|
||||||
|
public int type;
|
||||||
|
public INPUT_UNION union;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
private struct INPUT_UNION
|
||||||
|
{
|
||||||
|
[FieldOffset(0)] public KEYBDINPUT ki;
|
||||||
|
[FieldOffset(0)] public MOUSEINPUT mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct KEYBDINPUT
|
||||||
|
{
|
||||||
|
public ushort wVk;
|
||||||
|
public ushort wScan;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint time;
|
||||||
|
public nint dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MOUSEINPUT
|
||||||
|
{
|
||||||
|
public int dx;
|
||||||
|
public int dy;
|
||||||
|
public uint mouseData;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint time;
|
||||||
|
public nint dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INPUT MakeKeyScanInput(ushort scanCode, bool keyUp)
|
||||||
|
{
|
||||||
|
var flags = KEYEVENTF_SCANCODE;
|
||||||
|
if (keyUp) flags |= KEYEVENTF_KEYUP;
|
||||||
|
|
||||||
|
return new INPUT
|
||||||
|
{
|
||||||
|
type = INPUT_KEYBOARD,
|
||||||
|
union = new INPUT_UNION
|
||||||
|
{
|
||||||
|
ki = new KEYBDINPUT
|
||||||
|
{
|
||||||
|
wVk = 0,
|
||||||
|
wScan = scanCode,
|
||||||
|
dwFlags = flags,
|
||||||
|
time = 0,
|
||||||
|
dwExtraInfo = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INPUT MakeMouseInput(uint flags)
|
||||||
|
{
|
||||||
|
return new INPUT
|
||||||
|
{
|
||||||
|
type = INPUT_MOUSE,
|
||||||
|
union = new INPUT_UNION
|
||||||
|
{
|
||||||
|
mi = new MOUSEINPUT
|
||||||
|
{
|
||||||
|
dwFlags = flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", SetLastError = true)]
|
||||||
|
private static partial uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool SetCursorPos(int x, int y);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static partial bool GetCursorPos(out POINT lpPoint);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -44,6 +44,7 @@ public class GameMemoryReader : IDisposable
|
||||||
public MemoryContext? Context => _ctx;
|
public MemoryContext? Context => _ctx;
|
||||||
public ComponentReader? Components => _components;
|
public ComponentReader? Components => _components;
|
||||||
public GameStateReader? StateReader => _stateReader;
|
public GameStateReader? StateReader => _stateReader;
|
||||||
|
public UIElements? UIElements => _gameStates?.InGame.UIElements;
|
||||||
|
|
||||||
public GameMemoryReader()
|
public GameMemoryReader()
|
||||||
{
|
{
|
||||||
|
|
@ -166,7 +167,15 @@ public class GameMemoryReader : IDisposable
|
||||||
var gs = _gameStates!;
|
var gs = _gameStates!;
|
||||||
|
|
||||||
// Set loading state on terrain before cascade
|
// Set loading state on terrain before cascade
|
||||||
gs.InGame.AreaInstance.SetLoadingState(gs.AreaLoading.IsLoading);
|
// Use controller-based check (controller+IsLoadingOffset == InGameState → not loading)
|
||||||
|
// instead of AreaLoading slot which has a stale hardcoded offset (0x660)
|
||||||
|
var isLoadingForTerrain = false;
|
||||||
|
if (_lastController != 0 && _lastInGameState != 0 && _ctx.Offsets.IsLoadingOffset > 0)
|
||||||
|
{
|
||||||
|
var loadPtr = _ctx.Memory.ReadPointer(_lastController + _ctx.Offsets.IsLoadingOffset);
|
||||||
|
isLoadingForTerrain = loadPtr != 0 && loadPtr != _lastInGameState;
|
||||||
|
}
|
||||||
|
gs.InGame.AreaInstance.SetLoadingState(isLoadingForTerrain);
|
||||||
|
|
||||||
if (!gs.Update())
|
if (!gs.Update())
|
||||||
return snap;
|
return snap;
|
||||||
|
|
@ -179,7 +188,7 @@ public class GameMemoryReader : IDisposable
|
||||||
snap.CurrentGameState = gs.CurrentState;
|
snap.CurrentGameState = gs.CurrentState;
|
||||||
snap.ControllerPreSlots = gs.ControllerPreSlots;
|
snap.ControllerPreSlots = gs.ControllerPreSlots;
|
||||||
snap.InGameStatePtr = gs.InGame.Address;
|
snap.InGameStatePtr = gs.InGame.Address;
|
||||||
snap.IsLoading = gs.AreaLoading.IsLoading;
|
snap.IsLoading = isLoadingForTerrain; // use controller-based check, not broken AreaLoading.IsLoading
|
||||||
snap.IsEscapeOpen = gs.InGame.IsEscapeOpen;
|
snap.IsEscapeOpen = gs.InGame.IsEscapeOpen;
|
||||||
snap.AreaInstancePtr = ai.Address;
|
snap.AreaInstancePtr = ai.Address;
|
||||||
snap.ServerDataPtr = ai.ServerDataPtr;
|
snap.ServerDataPtr = ai.ServerDataPtr;
|
||||||
|
|
@ -249,6 +258,7 @@ public class GameMemoryReader : IDisposable
|
||||||
// Skills & quests — read from hierarchy
|
// Skills & quests — read from hierarchy
|
||||||
snap.PlayerSkills = ai.PlayerSkills.Skills;
|
snap.PlayerSkills = ai.PlayerSkills.Skills;
|
||||||
snap.QuestFlags = ai.QuestFlags.Quests;
|
snap.QuestFlags = ai.QuestFlags.Quests;
|
||||||
|
snap.QuestStates = ai.QuestStates;
|
||||||
|
|
||||||
// Read state flag bytes
|
// Read state flag bytes
|
||||||
if (snap.InGameStatePtr != 0)
|
if (snap.InGameStatePtr != 0)
|
||||||
|
|
@ -261,6 +271,15 @@ public class GameMemoryReader : IDisposable
|
||||||
snap.TerrainHeight = ai.Terrain.TerrainHeight;
|
snap.TerrainHeight = ai.Terrain.TerrainHeight;
|
||||||
snap.Terrain = ai.Terrain.Grid;
|
snap.Terrain = ai.Terrain.Grid;
|
||||||
snap.TerrainWalkablePercent = ai.Terrain.WalkablePercent;
|
snap.TerrainWalkablePercent = ai.Terrain.WalkablePercent;
|
||||||
|
|
||||||
|
// UI tree — root pointer only; tree is read lazily on-demand
|
||||||
|
snap.GameUiPtr = gs.InGame.UIElements.GameUiPtr;
|
||||||
|
|
||||||
|
// Quest linked lists (all quests + tracked merged)
|
||||||
|
snap.QuestLinkedList = gs.InGame.UIElements.ReadQuestLinkedLists();
|
||||||
|
|
||||||
|
// Quest groups from UI element tree
|
||||||
|
snap.UiQuestGroups = gs.InGame.UIElements.ReadQuestGroups();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -183,15 +183,41 @@ public sealed class GameOffsets
|
||||||
public uint QuestTrackedMarker { get; set; } = 0x43020000;
|
public uint QuestTrackedMarker { get; set; } = 0x43020000;
|
||||||
/// <summary>Offset within the quest state object to the encounter state byte (1=locked, 2=started). 0x08.</summary>
|
/// <summary>Offset within the quest state object to the encounter state byte (1=locked, 2=started). 0x08.</summary>
|
||||||
public int QuestObjEncounterStateOffset { get; set; } = 0x08;
|
public int QuestObjEncounterStateOffset { get; set; } = 0x08;
|
||||||
|
/// <summary>Offset within quest state object to QuestStateId int32. 0 = disabled (use ScanQuestStateOffsets to discover).</summary>
|
||||||
|
public int QuestObjStateIdOffset { get; set; } = 0;
|
||||||
|
/// <summary>Offset within quest state object to state text. Interpretation depends on QuestObjStateTextType.</summary>
|
||||||
|
public int QuestObjStateTextOffset { get; set; } = 0;
|
||||||
|
/// <summary>Offset within quest state object to progress text. Interpretation depends on QuestObjStateTextType.</summary>
|
||||||
|
public int QuestObjProgressTextOffset { get; set; } = 0;
|
||||||
|
/// <summary>Offset within quest state object to Quest pointer (follow → +0x00 for quest name wchar*). 0 = disabled.</summary>
|
||||||
|
public int QuestObjQuestPtrOffset { get; set; } = 0;
|
||||||
|
/// <summary>How to read state/progress text: "wchar_ptr" = direct pointer to wchar*, "std_wstring" = inline MSVC std::wstring (32 bytes).</summary>
|
||||||
|
public string QuestObjStateTextType { get; set; } = "wchar_ptr";
|
||||||
|
|
||||||
|
// ── Quest state container (InGameState → WorldData-like object → vector of 12-byte entries) ──
|
||||||
|
/// <summary>AreaInstance → quest state sub-object pointer. Discovered via ScanQuestStateContainers: 0x900.</summary>
|
||||||
|
public int QuestStateObjectOffset { get; set; } = 0x900;
|
||||||
|
/// <summary>Quest state container → StdVector of 12-byte {questId, state, flags} entries.</summary>
|
||||||
|
public int QuestStateVectorOffset { get; set; } = 0x240;
|
||||||
|
/// <summary>Size of each quest state entry in bytes.</summary>
|
||||||
|
public int QuestStateEntrySize { get; set; } = 12;
|
||||||
|
/// <summary>Maximum number of quest state entries to read (sanity limit).</summary>
|
||||||
|
public int QuestStateMaxEntries { get; set; } = 256;
|
||||||
|
|
||||||
// ── QuestStates.dat row layout (119 bytes, non-aligned fields) ──
|
// ── QuestStates.dat row layout (119 bytes, non-aligned fields) ──
|
||||||
/// <summary>Size of each .dat row in bytes. 0x77 = 119. 0 = name resolution disabled.</summary>
|
/// <summary>Size of each .dat row in bytes. 0x68 = 104 (confirmed via CE imul stride). 0 = name resolution disabled.</summary>
|
||||||
public int QuestDatRowSize { get; set; } = 0x77;
|
public int QuestDatRowSize { get; set; } = 0x68;
|
||||||
/// <summary>Dat row → Quest display name wchar* pointer.</summary>
|
/// <summary>Dat row → Quest TableReference (16 bytes: pointer to Quest.dat row at +0x00). Follow Quest.dat row → +0x00 for name wchar*.</summary>
|
||||||
public int QuestDatNameOffset { get; set; } = 0x00;
|
public int QuestDatNameOffset { get; set; } = 0x00;
|
||||||
/// <summary>Dat row → Internal quest ID wchar* pointer (e.g. "TreeOfSouls2").</summary>
|
/// <summary>Dat row → Order int32 (at offset 16 / 0x10).</summary>
|
||||||
|
public int QuestDatOrderOffset { get; set; } = 0x10;
|
||||||
|
/// <summary>Dat row → Text StringReference (quest state text). Offset 52 / 0x34.</summary>
|
||||||
|
public int QuestDatTextOffset { get; set; } = 0x34;
|
||||||
|
/// <summary>Dat row → Message StringReference. Offset 61 / 0x3D.</summary>
|
||||||
|
public int QuestDatMessageOffset { get; set; } = 0x3D;
|
||||||
|
/// <summary>Dat row → Internal quest ID wchar* pointer (legacy, may need update).</summary>
|
||||||
public int QuestDatInternalIdOffset { get; set; } = 0x6B;
|
public int QuestDatInternalIdOffset { get; set; } = 0x6B;
|
||||||
/// <summary>Dat row → Act/phase number int32.</summary>
|
/// <summary>Dat row → Act/phase number int32 (legacy, may need update).</summary>
|
||||||
public int QuestDatActOffset { get; set; } = 0x73;
|
public int QuestDatActOffset { get; set; } = 0x73;
|
||||||
|
|
||||||
// ── Entity / Component ──
|
// ── Entity / Component ──
|
||||||
|
|
@ -283,6 +309,39 @@ public sealed class GameOffsets
|
||||||
/// <summary>How many bytes to scan from InGameState for UIElement pointers (0x1000 = 4KB).</summary>
|
/// <summary>How many bytes to scan from InGameState for UIElement pointers (0x1000 = 4KB).</summary>
|
||||||
public int UiElementScanRange { get; set; } = 0x1000;
|
public int UiElementScanRange { get; set; } = 0x1000;
|
||||||
|
|
||||||
|
// ── Quest Linked Lists (ExileCore-style, on GameUi UIElement tree) ──
|
||||||
|
// Node layout: Next(8) + Prev(8) + QuestPtr(8) + Unused(8) + QuestStateId(1) = 33 bytes
|
||||||
|
// QuestPtr → +0x00 → wchar* internal quest ID (e.g. "TreeOfSouls2")
|
||||||
|
// QuestStateId: 0=completed, 255=not started/locked, other=in progress
|
||||||
|
|
||||||
|
/// <summary>Offset from GameUi UIElement to the full quest linked list head pointer. All quests (117 entries). 0x358.</summary>
|
||||||
|
public int QuestLinkedListOffset { get; set; } = 0x358;
|
||||||
|
/// <summary>Size of each linked list node in bytes. At least 40: Next(8)+Prev(8)+QuestPtr(8)+SharedPtr(8)+StateId(4)+extra(4).</summary>
|
||||||
|
public int QuestLinkedListNodeSize { get; set; } = 40;
|
||||||
|
/// <summary>Offset within linked list node to the Quest object pointer. 0x10.</summary>
|
||||||
|
public int QuestNodeQuestPtrOffset { get; set; } = 0x10;
|
||||||
|
/// <summary>Offset within linked list node to the QuestStateId byte. 0x20.</summary>
|
||||||
|
public int QuestNodeStateIdOffset { get; set; } = 0x20;
|
||||||
|
/// <summary>Offset from Quest.dat row to ptr→wchar* internal ID. 0x00.</summary>
|
||||||
|
public int QuestObjNamePtrOffset { get; set; } = 0x00;
|
||||||
|
/// <summary>Offset from Quest.dat row to int32 Act number. 0x08.</summary>
|
||||||
|
public int QuestObjActOffset { get; set; } = 0x08;
|
||||||
|
/// <summary>Offset from Quest.dat row to ptr→wchar* display name. 0x0C (NOT 8-byte aligned). ExileCore confirmed.</summary>
|
||||||
|
public int QuestObjDisplayNameOffset { get; set; } = 0x0C;
|
||||||
|
/// <summary>Offset from Quest.dat row to ptr→wchar* icon path. 0x14.</summary>
|
||||||
|
public int QuestObjIconOffset { get; set; } = 0x14;
|
||||||
|
/// <summary>Maximum nodes to traverse (sanity limit).</summary>
|
||||||
|
public int QuestLinkedListMaxNodes { get; set; } = 256;
|
||||||
|
/// <summary>Offset within the tracked quest's runtime state object to the objective text (std::wstring). 0x34.</summary>
|
||||||
|
public int QuestStateObjTextOffset { get; set; } = 0x34;
|
||||||
|
|
||||||
|
/// <summary>GameUi child index for the quest panel parent element (child[6]).</summary>
|
||||||
|
public int TrackedQuestPanelChildIndex { get; set; } = 6;
|
||||||
|
/// <summary>Sub-child index within quest panel parent (child[6][1]).</summary>
|
||||||
|
public int TrackedQuestPanelSubChildIndex { get; set; } = 1;
|
||||||
|
/// <summary>Offset from the [6][1] element to the tracked/active quest linked list. Same node layout. 0x318.</summary>
|
||||||
|
public int TrackedQuestLinkedListOffset { get; set; } = 0x318;
|
||||||
|
|
||||||
// ── Terrain (inline in AreaInstance) ──
|
// ── Terrain (inline in AreaInstance) ──
|
||||||
/// <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;
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ public sealed class AreaInstance : RemoteObject
|
||||||
public nint ServerDataPtr { get; private set; }
|
public nint ServerDataPtr { get; private set; }
|
||||||
public nint LocalPlayerPtr { get; private set; }
|
public nint LocalPlayerPtr { get; private set; }
|
||||||
public int EntityCount { get; private set; }
|
public int EntityCount { get; private set; }
|
||||||
|
|
||||||
public EntityList EntityList { get; }
|
public EntityList EntityList { get; }
|
||||||
public PlayerSkills PlayerSkills { get; }
|
public PlayerSkills PlayerSkills { get; }
|
||||||
public QuestFlags QuestFlags { get; }
|
public QuestFlags QuestFlags { get; }
|
||||||
public Terrain Terrain { get; }
|
public Terrain Terrain { get; }
|
||||||
public AreaTemplate AreaTemplate { get; }
|
public AreaTemplate AreaTemplate { get; }
|
||||||
|
public List<QuestStateEntry>? QuestStates { get; private set; }
|
||||||
|
|
||||||
public AreaInstance(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
|
public AreaInstance(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
|
||||||
: base(ctx)
|
: base(ctx)
|
||||||
|
|
@ -101,6 +101,9 @@ public sealed class AreaInstance : RemoteObject
|
||||||
else
|
else
|
||||||
QuestFlags.Reset();
|
QuestFlags.Reset();
|
||||||
|
|
||||||
|
// Quest state container (AI+0x900 → obj → +0x240 vector)
|
||||||
|
QuestStates = ReadQuestStates(mem, offsets);
|
||||||
|
|
||||||
// AreaTemplate — pointer at AreaInstance + AreaTemplateOffset
|
// AreaTemplate — pointer at AreaInstance + AreaTemplateOffset
|
||||||
var areaTemplatePtr = mem.ReadPointer(Address + offsets.AreaTemplateOffset);
|
var areaTemplatePtr = mem.ReadPointer(Address + offsets.AreaTemplateOffset);
|
||||||
if (areaTemplatePtr != 0)
|
if (areaTemplatePtr != 0)
|
||||||
|
|
@ -131,6 +134,45 @@ public sealed class AreaInstance : RemoteObject
|
||||||
Terrain.InvalidateCache();
|
Terrain.InvalidateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<QuestStateEntry>? ReadQuestStates(ProcessMemory mem, GameOffsets offsets)
|
||||||
|
{
|
||||||
|
if (offsets.QuestStateObjectOffset <= 0 || offsets.QuestStateVectorOffset <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var objPtr = mem.ReadPointer(Address + offsets.QuestStateObjectOffset);
|
||||||
|
if (objPtr == 0 || ((ulong)objPtr >> 32) is 0 or >= 0x7FFF)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var vecAddr = objPtr + offsets.QuestStateVectorOffset;
|
||||||
|
var vecBegin = mem.ReadPointer(vecAddr);
|
||||||
|
var vecEnd = mem.ReadPointer(vecAddr + 8);
|
||||||
|
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
|
||||||
|
|
||||||
|
var totalBytes = (int)(vecEnd - vecBegin);
|
||||||
|
var entrySize = offsets.QuestStateEntrySize;
|
||||||
|
if (totalBytes % entrySize != 0) return null;
|
||||||
|
|
||||||
|
var entryCount = totalBytes / entrySize;
|
||||||
|
if (entryCount <= 0 || entryCount > offsets.QuestStateMaxEntries) return null;
|
||||||
|
|
||||||
|
var data = mem.ReadBytes(vecBegin, totalBytes);
|
||||||
|
if (data is null) return null;
|
||||||
|
|
||||||
|
var result = new List<QuestStateEntry>(entryCount);
|
||||||
|
for (var i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
var off = i * entrySize;
|
||||||
|
result.Add(new QuestStateEntry
|
||||||
|
{
|
||||||
|
QuestId = BitConverter.ToInt32(data, off),
|
||||||
|
State = BitConverter.ToInt32(data, off + 4),
|
||||||
|
Flags = BitConverter.ToInt32(data, off + 8),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Clear()
|
protected override void Clear()
|
||||||
{
|
{
|
||||||
AreaLevel = 0;
|
AreaLevel = 0;
|
||||||
|
|
@ -138,6 +180,7 @@ public sealed class AreaInstance : RemoteObject
|
||||||
ServerDataPtr = 0;
|
ServerDataPtr = 0;
|
||||||
LocalPlayerPtr = 0;
|
LocalPlayerPtr = 0;
|
||||||
EntityCount = 0;
|
EntityCount = 0;
|
||||||
|
QuestStates = null;
|
||||||
EntityList.Reset();
|
EntityList.Reset();
|
||||||
PlayerSkills.Reset();
|
PlayerSkills.Reset();
|
||||||
QuestFlags.Reset();
|
QuestFlags.Reset();
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,20 @@ public sealed class InGameState : RemoteObject
|
||||||
public bool IsEscapeOpen { get; private set; }
|
public bool IsEscapeOpen { get; private set; }
|
||||||
public AreaInstance AreaInstance { get; }
|
public AreaInstance AreaInstance { get; }
|
||||||
public WorldData WorldData { get; }
|
public WorldData WorldData { get; }
|
||||||
|
public UIElements UIElements { get; }
|
||||||
|
|
||||||
public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
|
public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
|
||||||
: base(ctx)
|
: base(ctx)
|
||||||
{
|
{
|
||||||
AreaInstance = new AreaInstance(ctx, components, strings, questNames);
|
AreaInstance = new AreaInstance(ctx, components, strings, questNames);
|
||||||
WorldData = new WorldData(ctx);
|
WorldData = new WorldData(ctx);
|
||||||
|
UIElements = new UIElements(ctx, strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool ReadData()
|
protected override bool ReadData()
|
||||||
{
|
{
|
||||||
var mem = Ctx.Memory;
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
// Read the full InGameState struct (0x310 = 784 bytes, 1 RPM)
|
// Read the full InGameState struct (0x310 = 784 bytes, 1 RPM)
|
||||||
_data = mem.Read<IgsStruct>(Address);
|
_data = mem.Read<IgsStruct>(Address);
|
||||||
|
|
@ -39,6 +42,9 @@ public sealed class InGameState : RemoteObject
|
||||||
WorldData.FallbackCameraPtr = _data.CameraPtr;
|
WorldData.FallbackCameraPtr = _data.CameraPtr;
|
||||||
WorldData.Update(_data.WorldDataPtr);
|
WorldData.Update(_data.WorldDataPtr);
|
||||||
|
|
||||||
|
// Cascade to UIElements — pass InGameState address for UiRootStruct chain
|
||||||
|
UIElements.Update(Address);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,5 +54,6 @@ public sealed class InGameState : RemoteObject
|
||||||
IsEscapeOpen = false;
|
IsEscapeOpen = false;
|
||||||
AreaInstance.Reset();
|
AreaInstance.Reset();
|
||||||
WorldData.Reset();
|
WorldData.Reset();
|
||||||
|
UIElements.Reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ public sealed class QuestFlags : RemoteObject
|
||||||
}
|
}
|
||||||
|
|
||||||
var datTableBase = FindDatTableBase(offsets);
|
var datTableBase = FindDatTableBase(offsets);
|
||||||
|
var useStdWString = offsets.QuestObjStateTextType == "std_wstring";
|
||||||
|
|
||||||
var result = new List<QuestSnapshot>(entryCount);
|
var result = new List<QuestSnapshot>(entryCount);
|
||||||
|
|
||||||
|
|
@ -105,6 +106,8 @@ public sealed class QuestFlags : RemoteObject
|
||||||
byte stateId = 0;
|
byte stateId = 0;
|
||||||
bool isTracked = false;
|
bool isTracked = false;
|
||||||
nint questObjPtr = 0;
|
nint questObjPtr = 0;
|
||||||
|
string? stateText = null;
|
||||||
|
string? progressText = null;
|
||||||
|
|
||||||
if (compData is not null && i < compEntryCount)
|
if (compData is not null && i < compEntryCount)
|
||||||
{
|
{
|
||||||
|
|
@ -121,26 +124,51 @@ public sealed class QuestFlags : RemoteObject
|
||||||
{
|
{
|
||||||
questObjPtr = (nint)BitConverter.ToInt64(compData, compOff + offsets.QuestCompanionObjPtrOffset);
|
questObjPtr = (nint)BitConverter.ToInt64(compData, compOff + offsets.QuestCompanionObjPtrOffset);
|
||||||
|
|
||||||
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF
|
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF)
|
||||||
&& offsets.QuestObjEncounterStateOffset > 0)
|
|
||||||
{
|
{
|
||||||
var stateByte = mem.ReadBytes(questObjPtr + offsets.QuestObjEncounterStateOffset, 1);
|
if (offsets.QuestObjEncounterStateOffset > 0)
|
||||||
if (stateByte is { Length: 1 })
|
{
|
||||||
stateId = stateByte[0];
|
var stateByte = mem.ReadBytes(questObjPtr + offsets.QuestObjEncounterStateOffset, 1);
|
||||||
|
if (stateByte is { Length: 1 })
|
||||||
|
stateId = stateByte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read state text from quest state object
|
||||||
|
if (offsets.QuestObjStateTextOffset > 0)
|
||||||
|
stateText = ReadQuestObjString(questObjPtr + offsets.QuestObjStateTextOffset, useStdWString);
|
||||||
|
|
||||||
|
// Read progress text from quest state object
|
||||||
|
if (offsets.QuestObjProgressTextOffset > 0)
|
||||||
|
progressText = ReadQuestObjString(questObjPtr + offsets.QuestObjProgressTextOffset, useStdWString);
|
||||||
|
|
||||||
|
// Read quest name via QuestPtr → +0x00 wchar*
|
||||||
|
if (offsets.QuestObjQuestPtrOffset > 0 && questName is null)
|
||||||
|
{
|
||||||
|
var questPtr = mem.ReadPointer(questObjPtr + offsets.QuestObjQuestPtrOffset);
|
||||||
|
if (questPtr != 0 && ((ulong)questPtr >> 32) is > 0 and < 0x7FFF)
|
||||||
|
{
|
||||||
|
var namePtr = mem.ReadPointer(questPtr);
|
||||||
|
if (namePtr != 0)
|
||||||
|
questName = _strings.ReadNullTermWString(namePtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (datTableBase != 0 && offsets.QuestDatRowSize > 0)
|
if (questName is null)
|
||||||
{
|
{
|
||||||
var rowAddr = datTableBase + idx * offsets.QuestDatRowSize;
|
if (datTableBase != 0 && offsets.QuestDatRowSize > 0)
|
||||||
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
|
{
|
||||||
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
|
var rowAddr = datTableBase + idx * offsets.QuestDatRowSize;
|
||||||
}
|
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
|
||||||
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
|
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
|
||||||
{
|
}
|
||||||
questName = entry?.Name;
|
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
|
||||||
internalId = entry?.InternalId;
|
{
|
||||||
|
questName = entry?.Name;
|
||||||
|
internalId = entry?.InternalId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(new QuestSnapshot
|
result.Add(new QuestSnapshot
|
||||||
|
|
@ -151,12 +179,26 @@ public sealed class QuestFlags : RemoteObject
|
||||||
InternalId = internalId,
|
InternalId = internalId,
|
||||||
StateId = stateId,
|
StateId = stateId,
|
||||||
IsTracked = isTracked,
|
IsTracked = isTracked,
|
||||||
|
StateText = stateText,
|
||||||
|
ProgressText = progressText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads a string from a quest state object field, either as wchar* pointer or MSVC std::wstring.</summary>
|
||||||
|
private string? ReadQuestObjString(nint fieldAddr, bool stdWString)
|
||||||
|
{
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
if (stdWString)
|
||||||
|
return _strings.ReadMsvcWString(fieldAddr);
|
||||||
|
|
||||||
|
var strPtr = mem.ReadPointer(fieldAddr);
|
||||||
|
if (strPtr == 0 || ((ulong)strPtr >> 32) is 0 or >= 0x7FFF) return null;
|
||||||
|
return _strings.ReadNullTermWString(strPtr);
|
||||||
|
}
|
||||||
|
|
||||||
private nint FindDatTableBase(GameOffsets offsets)
|
private nint FindDatTableBase(GameOffsets offsets)
|
||||||
{
|
{
|
||||||
if (offsets.QuestDatRowSize <= 0) return 0;
|
if (offsets.QuestDatRowSize <= 0) return 0;
|
||||||
|
|
|
||||||
528
src/Roboto.Memory/Objects/UIElements.cs
Normal file
528
src/Roboto.Memory/Objects/UIElements.cs
Normal file
|
|
@ -0,0 +1,528 @@
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Roboto.Memory.Objects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the UIElement tree from InGameState → UiRootStruct → GameUi.
|
||||||
|
/// Fully lazy: ReadData() only resolves root pointers (2 RPM).
|
||||||
|
/// Tree nodes are read on-demand via ReadNode/ReadChildren from the UI layer.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UIElements : RemoteObject
|
||||||
|
{
|
||||||
|
// Bulk-read covers offsets 0x00 through 0x468 (Text std::wstring at 0x448 + 32 bytes).
|
||||||
|
private const int BulkReadSize = 0x468;
|
||||||
|
|
||||||
|
/// <summary>Maximum children to read per node (safety limit).</summary>
|
||||||
|
public int MaxChildrenPerNode { get; set; } = 200;
|
||||||
|
|
||||||
|
public nint UiRootPtr { get; private set; }
|
||||||
|
public nint GameUiPtr { get; private set; }
|
||||||
|
|
||||||
|
private readonly MsvcStringReader _strings;
|
||||||
|
|
||||||
|
public UIElements(MemoryContext ctx, MsvcStringReader strings) : base(ctx)
|
||||||
|
{
|
||||||
|
_strings = strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReadData()
|
||||||
|
{
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
// Address = InGameState pointer
|
||||||
|
// Follow: InGameState+UiRootStructOffset → UiRootStruct ptr
|
||||||
|
var uiRootStructPtr = mem.ReadPointer(Address + offsets.UiRootStructOffset);
|
||||||
|
if (uiRootStructPtr == 0 || (ulong)uiRootStructPtr >> 32 is 0 or >= 0x7FFF)
|
||||||
|
{
|
||||||
|
UiRootPtr = 0;
|
||||||
|
GameUiPtr = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UiRootStruct → UiRoot and GameUi element pointers
|
||||||
|
UiRootPtr = mem.ReadPointer(uiRootStructPtr + offsets.UiRootPtrOffset);
|
||||||
|
GameUiPtr = mem.ReadPointer(uiRootStructPtr + offsets.GameUiPtrOffset);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a single UIElement node (1 bulk RPM). Returns null if address is invalid.
|
||||||
|
/// Does NOT read children — call ReadChildren separately.
|
||||||
|
/// </summary>
|
||||||
|
public UIElementNode? ReadNode(nint addr)
|
||||||
|
{
|
||||||
|
if (addr == 0 || (ulong)addr >> 32 is 0 or >= 0x7FFF)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
var buf = mem.ReadBytes(addr, BulkReadSize);
|
||||||
|
if (buf is null || buf.Length < BulkReadSize)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Self-check validation
|
||||||
|
var selfPtr = (nint)BitConverter.ToInt64(buf, offsets.UiElementSelfOffset);
|
||||||
|
if (selfPtr != addr)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var stringId = ParseWStringFromBuffer(buf, offsets.UiElementStringIdOffset);
|
||||||
|
var text = ParseWStringFromBuffer(buf, offsets.UiElementTextOffset);
|
||||||
|
|
||||||
|
var flags = BitConverter.ToUInt32(buf, offsets.UiElementFlagsOffset);
|
||||||
|
var isVisible = (flags & (1u << offsets.UiElementVisibleBit)) != 0;
|
||||||
|
|
||||||
|
var width = BitConverter.ToSingle(buf, offsets.UiElementSizeOffset);
|
||||||
|
var height = BitConverter.ToSingle(buf, offsets.UiElementSizeOffset + 4);
|
||||||
|
|
||||||
|
// Children count (don't read children themselves)
|
||||||
|
var childCount = 0;
|
||||||
|
var vecBegin = (nint)BitConverter.ToInt64(buf, offsets.UiElementChildrenOffset);
|
||||||
|
var vecEnd = (nint)BitConverter.ToInt64(buf, offsets.UiElementChildrenOffset + 8);
|
||||||
|
if (vecBegin != 0 && vecEnd > vecBegin)
|
||||||
|
{
|
||||||
|
childCount = (int)(vecEnd - vecBegin) / 8;
|
||||||
|
if (childCount > 10000) childCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UIElementNode
|
||||||
|
{
|
||||||
|
Address = addr,
|
||||||
|
StringId = stringId,
|
||||||
|
Text = text,
|
||||||
|
IsVisible = isVisible,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
ChildCount = childCount,
|
||||||
|
Children = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On-demand: reads immediate children of a node address.
|
||||||
|
/// Each child is a shallow UIElementNode (no grandchildren).
|
||||||
|
/// </summary>
|
||||||
|
public List<UIElementNode>? ReadChildren(nint nodeAddr)
|
||||||
|
{
|
||||||
|
if (nodeAddr == 0) return null;
|
||||||
|
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
var vecBegin = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset);
|
||||||
|
var vecEnd = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset + 8);
|
||||||
|
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
|
||||||
|
|
||||||
|
var childCount = (int)(vecEnd - vecBegin) / 8;
|
||||||
|
if (childCount <= 0 || childCount > 10000) return null;
|
||||||
|
|
||||||
|
var count = Math.Min(childCount, MaxChildrenPerNode);
|
||||||
|
var ptrData = mem.ReadBytes(vecBegin, count * 8);
|
||||||
|
if (ptrData is null) return null;
|
||||||
|
|
||||||
|
var result = new List<UIElementNode>(count);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var childPtr = (nint)BitConverter.ToInt64(ptrData, i * 8);
|
||||||
|
var child = ReadNode(childPtr);
|
||||||
|
if (child is not null)
|
||||||
|
result.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On-demand text read for a specific node address (1-2 RPM).
|
||||||
|
/// </summary>
|
||||||
|
public string? ReadNodeText(nint nodeAddr)
|
||||||
|
{
|
||||||
|
if (nodeAddr == 0) return null;
|
||||||
|
return ReadUiWString(nodeAddr + Ctx.Offsets.UiElementTextOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the Nth child of a node (0-indexed). Returns null if out of range or invalid.
|
||||||
|
/// </summary>
|
||||||
|
public UIElementNode? ReadChildAtIndex(nint nodeAddr, int index)
|
||||||
|
{
|
||||||
|
if (nodeAddr == 0 || index < 0) return null;
|
||||||
|
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
var vecBegin = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset);
|
||||||
|
var vecEnd = mem.ReadPointer(nodeAddr + offsets.UiElementChildrenOffset + 8);
|
||||||
|
if (vecBegin == 0 || vecEnd <= vecBegin) return null;
|
||||||
|
|
||||||
|
var childCount = (int)(vecEnd - vecBegin) / 8;
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
|
||||||
|
var childPtr = mem.ReadPointer(vecBegin + index * 8);
|
||||||
|
return ReadNode(childPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigates a path of child indices from a starting node address.
|
||||||
|
/// e.g. NavigatePath(gameUiPtr, [6, 1, 0, 0, 0, 2]) → root[6][1][0][0][0][2]
|
||||||
|
/// </summary>
|
||||||
|
public UIElementNode? NavigatePath(nint startAddr, ReadOnlySpan<int> path)
|
||||||
|
{
|
||||||
|
var current = startAddr;
|
||||||
|
UIElementNode? node = null;
|
||||||
|
|
||||||
|
foreach (var idx in path)
|
||||||
|
{
|
||||||
|
node = ReadChildAtIndex(current, idx);
|
||||||
|
if (node is null) return null;
|
||||||
|
current = node.Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads quest groups from the UI tree.
|
||||||
|
/// Path: GameUi[6][1][0][0][0] → quest_display → [0] → title_layout/quest_info_layout
|
||||||
|
/// </summary>
|
||||||
|
public List<UiQuestGroup>? ReadQuestGroups()
|
||||||
|
{
|
||||||
|
if (GameUiPtr == 0) return null;
|
||||||
|
|
||||||
|
// Navigate to the parent that holds quest_display nodes
|
||||||
|
var questParent = NavigatePath(GameUiPtr, [6, 1, 0, 0, 0]);
|
||||||
|
if (questParent is null) return null;
|
||||||
|
|
||||||
|
var questDisplays = ReadChildren(questParent.Address);
|
||||||
|
if (questDisplays is null) return null;
|
||||||
|
|
||||||
|
var groups = new List<UiQuestGroup>();
|
||||||
|
foreach (var qd in questDisplays)
|
||||||
|
{
|
||||||
|
if (!string.Equals(qd.StringId, "quest_display", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// quest_display → [0] (unnamed child with title_layout + quest_info_layout)
|
||||||
|
var qdChildren = ReadChildren(qd.Address);
|
||||||
|
if (qdChildren is null || qdChildren.Count == 0) continue;
|
||||||
|
|
||||||
|
var innerChildren = ReadChildren(qdChildren[0].Address);
|
||||||
|
if (innerChildren is null) continue;
|
||||||
|
|
||||||
|
var group = new UiQuestGroup();
|
||||||
|
|
||||||
|
foreach (var child in innerChildren)
|
||||||
|
{
|
||||||
|
if (string.Equals(child.StringId, "title_layout", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// title_layout → title_label (has quest name text)
|
||||||
|
var titleChildren = ReadChildren(child.Address);
|
||||||
|
if (titleChildren is not null)
|
||||||
|
{
|
||||||
|
foreach (var tc in titleChildren)
|
||||||
|
{
|
||||||
|
if (string.Equals(tc.StringId, "title_label", StringComparison.Ordinal))
|
||||||
|
group.Title = tc.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(child.StringId, "quest_info_layout", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ReadQuestSteps(child.Address, group.Steps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Title is not null || group.Steps.Count > 0)
|
||||||
|
groups.Add(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.Count > 0 ? groups : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadQuestSteps(nint layoutAddr, List<UiQuestStep> steps)
|
||||||
|
{
|
||||||
|
var layoutChildren = ReadChildren(layoutAddr);
|
||||||
|
if (layoutChildren is null) return;
|
||||||
|
|
||||||
|
foreach (var entryNode in layoutChildren)
|
||||||
|
{
|
||||||
|
if (!string.Equals(entryNode.StringId, "quest_info_entry", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var entryChildren = ReadChildren(entryNode.Address);
|
||||||
|
if (entryChildren is null) continue;
|
||||||
|
|
||||||
|
var step = new UiQuestStep();
|
||||||
|
foreach (var part in entryChildren)
|
||||||
|
{
|
||||||
|
if (string.Equals(part.StringId, "quest_info", StringComparison.Ordinal))
|
||||||
|
step.Text = part.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.Text is not null)
|
||||||
|
steps.Add(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BFS search: reads nodes on-demand until a matching StringId is found.
|
||||||
|
/// Walks the live game memory — use sparingly.
|
||||||
|
/// </summary>
|
||||||
|
public UIElementNode? FindByStringId(string id)
|
||||||
|
{
|
||||||
|
if (GameUiPtr == 0) return null;
|
||||||
|
|
||||||
|
var root = ReadNode(GameUiPtr);
|
||||||
|
if (root is null) return null;
|
||||||
|
if (string.Equals(root.StringId, id, StringComparison.Ordinal))
|
||||||
|
return root;
|
||||||
|
|
||||||
|
var queue = new Queue<nint>();
|
||||||
|
var visited = new HashSet<nint> { GameUiPtr };
|
||||||
|
EnqueueChildren(queue, visited, GameUiPtr);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var addr = queue.Dequeue();
|
||||||
|
var node = ReadNode(addr);
|
||||||
|
if (node is null) continue;
|
||||||
|
|
||||||
|
if (string.Equals(node.StringId, id, StringComparison.Ordinal))
|
||||||
|
return node;
|
||||||
|
|
||||||
|
EnqueueChildren(queue, visited, addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads both quest linked lists (all-quests + tracked) and merges them.
|
||||||
|
/// Returns null if GameUi is not available.
|
||||||
|
/// </summary>
|
||||||
|
public List<QuestLinkedEntry>? ReadQuestLinkedLists()
|
||||||
|
{
|
||||||
|
if (GameUiPtr == 0) return null;
|
||||||
|
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
// ── Tracked quests: [6][1]+0x318 — collect into dict keyed by QuestDatPtr ──
|
||||||
|
var trackedMap = new Dictionary<nint, string?>();
|
||||||
|
var elem6 = ReadChildAtIndex(GameUiPtr, offsets.TrackedQuestPanelChildIndex);
|
||||||
|
if (elem6 is not null)
|
||||||
|
{
|
||||||
|
var elem61 = ReadChildAtIndex(elem6.Address, offsets.TrackedQuestPanelSubChildIndex);
|
||||||
|
if (elem61 is not null)
|
||||||
|
{
|
||||||
|
var trackedHead = mem.ReadPointer(elem61.Address + offsets.TrackedQuestLinkedListOffset);
|
||||||
|
if (trackedHead != 0)
|
||||||
|
TraverseTrackedQuests(trackedHead, trackedMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── All quests: GameUi+0x358 ──
|
||||||
|
var allHead = mem.ReadPointer(GameUiPtr + offsets.QuestLinkedListOffset);
|
||||||
|
if (allHead == 0) return null;
|
||||||
|
|
||||||
|
return TraverseAllQuests(allHead, trackedMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks the all-quests linked list. Reads Quest.dat row fields + stateId per node.
|
||||||
|
/// Merges tracked info from the trackedMap.
|
||||||
|
/// </summary>
|
||||||
|
private List<QuestLinkedEntry>? TraverseAllQuests(nint headPtr, Dictionary<nint, string?> trackedMap)
|
||||||
|
{
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
var maxNodes = offsets.QuestLinkedListMaxNodes;
|
||||||
|
var readSize = Math.Max(offsets.QuestLinkedListNodeSize, 48);
|
||||||
|
|
||||||
|
var visited = new HashSet<nint>();
|
||||||
|
var walk = headPtr;
|
||||||
|
var isSentinel = true;
|
||||||
|
var result = new List<QuestLinkedEntry>();
|
||||||
|
|
||||||
|
while (result.Count < maxNodes)
|
||||||
|
{
|
||||||
|
if (walk == 0 || !visited.Add(walk)) break;
|
||||||
|
|
||||||
|
var nodeData = mem.ReadBytes(walk, readSize);
|
||||||
|
if (nodeData is null) break;
|
||||||
|
|
||||||
|
var next = (nint)BitConverter.ToInt64(nodeData, 0);
|
||||||
|
|
||||||
|
if (isSentinel)
|
||||||
|
{
|
||||||
|
isSentinel = false;
|
||||||
|
walk = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var questPtr = (nint)BitConverter.ToInt64(nodeData, offsets.QuestNodeQuestPtrOffset);
|
||||||
|
var stateId = BitConverter.ToInt32(nodeData, offsets.QuestNodeStateIdOffset);
|
||||||
|
|
||||||
|
string? internalId = null;
|
||||||
|
string? displayName = null;
|
||||||
|
var act = -1;
|
||||||
|
|
||||||
|
if (questPtr != 0 && ((ulong)questPtr >> 32) is > 0 and < 0x7FFF)
|
||||||
|
{
|
||||||
|
var idPtr = mem.ReadPointer(questPtr + offsets.QuestObjNamePtrOffset);
|
||||||
|
if (idPtr != 0 && ((ulong)idPtr >> 32) is > 0 and < 0x7FFF)
|
||||||
|
internalId = _strings.ReadNullTermWString(idPtr);
|
||||||
|
|
||||||
|
act = mem.Read<int>(questPtr + offsets.QuestObjActOffset);
|
||||||
|
|
||||||
|
var namePtr = mem.ReadPointer(questPtr + offsets.QuestObjDisplayNameOffset);
|
||||||
|
if (namePtr != 0 && ((ulong)namePtr >> 32) is > 0 and < 0x7FFF)
|
||||||
|
displayName = _strings.ReadNullTermWString(namePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTracked = trackedMap.TryGetValue(questPtr, out var objectiveText);
|
||||||
|
|
||||||
|
result.Add(new QuestLinkedEntry
|
||||||
|
{
|
||||||
|
InternalId = internalId,
|
||||||
|
DisplayName = displayName,
|
||||||
|
Act = act,
|
||||||
|
StateId = stateId,
|
||||||
|
IsTracked = isTracked,
|
||||||
|
ObjectiveText = objectiveText,
|
||||||
|
QuestDatPtr = questPtr,
|
||||||
|
});
|
||||||
|
|
||||||
|
walk = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Count > 0 ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks the tracked-quests linked list. Builds a dict of QuestDatPtr → ObjectiveText.
|
||||||
|
/// Node+0x20 is a pointer to a runtime state object; text is at stateObj+QuestStateObjTextOffset (std::wstring).
|
||||||
|
/// </summary>
|
||||||
|
private void TraverseTrackedQuests(nint headPtr, Dictionary<nint, string?> trackedMap)
|
||||||
|
{
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
var maxNodes = offsets.QuestLinkedListMaxNodes;
|
||||||
|
var readSize = Math.Max(offsets.QuestLinkedListNodeSize, 48);
|
||||||
|
|
||||||
|
var visited = new HashSet<nint>();
|
||||||
|
var walk = headPtr;
|
||||||
|
var isSentinel = true;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (count < maxNodes)
|
||||||
|
{
|
||||||
|
if (walk == 0 || !visited.Add(walk)) break;
|
||||||
|
|
||||||
|
var nodeData = mem.ReadBytes(walk, readSize);
|
||||||
|
if (nodeData is null) break;
|
||||||
|
|
||||||
|
var next = (nint)BitConverter.ToInt64(nodeData, 0);
|
||||||
|
|
||||||
|
if (isSentinel)
|
||||||
|
{
|
||||||
|
isSentinel = false;
|
||||||
|
walk = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var questPtr = (nint)BitConverter.ToInt64(nodeData, offsets.QuestNodeQuestPtrOffset);
|
||||||
|
string? objectiveText = null;
|
||||||
|
|
||||||
|
// +0x20 in tracked list is a pointer to the quest state runtime object
|
||||||
|
var stateObjPtr = (nint)BitConverter.ToInt64(nodeData, 0x20);
|
||||||
|
if (stateObjPtr != 0 && ((ulong)stateObjPtr >> 32) is > 0 and < 0x7FFF)
|
||||||
|
{
|
||||||
|
// Read std::wstring at stateObj + QuestStateObjTextOffset
|
||||||
|
objectiveText = ParseWStringFromMemory(stateObjPtr + offsets.QuestStateObjTextOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questPtr != 0)
|
||||||
|
trackedMap[questPtr] = objectiveText;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
walk = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads an inline MSVC std::wstring from a process memory address.
|
||||||
|
/// Same layout as UIElement strings: Buffer(8) + Reserved(8) + Length(4) + pad(4) + Capacity(4) + pad(4).
|
||||||
|
/// </summary>
|
||||||
|
private string? ParseWStringFromMemory(nint addr)
|
||||||
|
{
|
||||||
|
var strData = Ctx.Memory.ReadBytes(addr, 32);
|
||||||
|
if (strData is null || strData.Length < 28) return null;
|
||||||
|
return ParseWStringFromBuffer(strData, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnqueueChildren(Queue<nint> queue, HashSet<nint> visited, nint parentAddr)
|
||||||
|
{
|
||||||
|
var mem = Ctx.Memory;
|
||||||
|
var offsets = Ctx.Offsets;
|
||||||
|
|
||||||
|
var vecBegin = mem.ReadPointer(parentAddr + offsets.UiElementChildrenOffset);
|
||||||
|
var vecEnd = mem.ReadPointer(parentAddr + offsets.UiElementChildrenOffset + 8);
|
||||||
|
if (vecBegin == 0 || vecEnd <= vecBegin) return;
|
||||||
|
|
||||||
|
var count = Math.Min((int)(vecEnd - vecBegin) / 8, MaxChildrenPerNode);
|
||||||
|
if (count <= 0) return;
|
||||||
|
|
||||||
|
var data = mem.ReadBytes(vecBegin, count * 8);
|
||||||
|
if (data is null) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var ptr = (nint)BitConverter.ToInt64(data, i * 8);
|
||||||
|
if (ptr != 0 && visited.Add(ptr))
|
||||||
|
queue.Enqueue(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ParseWStringFromBuffer(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
if (offset + 32 > buf.Length) return null;
|
||||||
|
|
||||||
|
var length = BitConverter.ToInt32(buf, offset + 0x10);
|
||||||
|
var capacity = BitConverter.ToInt32(buf, offset + 0x18);
|
||||||
|
if (length <= 0 || length > 4096 || capacity < length) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (capacity <= 8)
|
||||||
|
{
|
||||||
|
var byteLen = Math.Min(length * 2, 16);
|
||||||
|
return Encoding.Unicode.GetString(buf, offset, byteLen).TrimEnd('\0');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ptr = (nint)BitConverter.ToInt64(buf, offset);
|
||||||
|
if (ptr == 0 || (ulong)ptr >> 32 is 0 or >= 0x7FFF) return null;
|
||||||
|
var charData = Ctx.Memory.ReadBytes(ptr, Math.Min(length * 2, 512));
|
||||||
|
if (charData is null) return null;
|
||||||
|
return Encoding.Unicode.GetString(charData).TrimEnd('\0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ReadUiWString(nint addr)
|
||||||
|
{
|
||||||
|
var strData = Ctx.Memory.ReadBytes(addr, 32);
|
||||||
|
if (strData is null || strData.Length < 28) return null;
|
||||||
|
return ParseWStringFromBuffer(strData, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Clear()
|
||||||
|
{
|
||||||
|
UiRootPtr = 0;
|
||||||
|
GameUiPtr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -78,6 +78,18 @@ public class GameStateSnapshot
|
||||||
// Quest flags (from ServerData → PlayerServerData)
|
// Quest flags (from ServerData → PlayerServerData)
|
||||||
public List<QuestSnapshot>? QuestFlags;
|
public List<QuestSnapshot>? QuestFlags;
|
||||||
|
|
||||||
|
// Quest states (from AreaInstance → sub-object → vector)
|
||||||
|
public List<QuestStateEntry>? QuestStates;
|
||||||
|
|
||||||
|
// UI tree — root pointer only; tree is read on-demand
|
||||||
|
public nint GameUiPtr;
|
||||||
|
|
||||||
|
// Quest linked lists (all-quests + tracked merged)
|
||||||
|
public List<QuestLinkedEntry>? QuestLinkedList;
|
||||||
|
|
||||||
|
// Quest groups from UI element tree
|
||||||
|
public List<UiQuestGroup>? UiQuestGroups;
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
public Matrix4x4? CameraMatrix;
|
public Matrix4x4? CameraMatrix;
|
||||||
|
|
||||||
|
|
|
||||||
24
src/Roboto.Memory/Snapshots/QuestLinkedEntry.cs
Normal file
24
src/Roboto.Memory/Snapshots/QuestLinkedEntry.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace Roboto.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A quest entry from the GameUi linked lists.
|
||||||
|
/// All-quests list (GameUi+0x358) provides Id/Name/Act/StateId.
|
||||||
|
/// Tracked-quests list ([6][1]+0x318) adds ObjectiveText.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuestLinkedEntry
|
||||||
|
{
|
||||||
|
/// <summary>Internal quest ID from Quest.dat row, e.g. "TreeOfSouls".</summary>
|
||||||
|
public string? InternalId { get; init; }
|
||||||
|
/// <summary>Display name from Quest.dat row, e.g. "Secrets in the Dark".</summary>
|
||||||
|
public string? DisplayName { get; init; }
|
||||||
|
/// <summary>Act number from Quest.dat row.</summary>
|
||||||
|
public int Act { get; init; }
|
||||||
|
/// <summary>State: 0=done, -1(0xFFFFFFFF)=locked, positive=in-progress step.</summary>
|
||||||
|
public int StateId { get; init; }
|
||||||
|
/// <summary>True if this quest appears in the tracked-quests list.</summary>
|
||||||
|
public bool IsTracked { get; init; }
|
||||||
|
/// <summary>Objective text from the tracked quest's runtime state object (std::wstring at +0x34).</summary>
|
||||||
|
public string? ObjectiveText { get; init; }
|
||||||
|
/// <summary>Raw Quest.dat row pointer — used as key for merging tracked info.</summary>
|
||||||
|
public nint QuestDatPtr { get; init; }
|
||||||
|
}
|
||||||
13
src/Roboto.Memory/Snapshots/QuestStateEntry.cs
Normal file
13
src/Roboto.Memory/Snapshots/QuestStateEntry.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Roboto.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A quest state entry from the AreaInstance quest state container.
|
||||||
|
/// 12-byte struct: {int QuestId, int State, int Flags}.
|
||||||
|
/// Discovered via ScanQuestStateContainers at AI+0x900 → obj → +0x240 vector.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuestStateEntry
|
||||||
|
{
|
||||||
|
public int QuestId { get; init; }
|
||||||
|
public int State { get; init; }
|
||||||
|
public int Flags { get; init; }
|
||||||
|
}
|
||||||
17
src/Roboto.Memory/Snapshots/UIElementNode.cs
Normal file
17
src/Roboto.Memory/Snapshots/UIElementNode.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Roboto.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lightweight snapshot of a single UIElement from the game's UI tree.
|
||||||
|
/// Built by UIElements RemoteObject during cold tick reads.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UIElementNode
|
||||||
|
{
|
||||||
|
public nint Address { get; init; }
|
||||||
|
public string? StringId { get; init; }
|
||||||
|
public string? Text { get; init; }
|
||||||
|
public bool IsVisible { get; init; }
|
||||||
|
public float Width { get; init; }
|
||||||
|
public float Height { get; init; }
|
||||||
|
public int ChildCount { get; init; }
|
||||||
|
public List<UIElementNode>? Children { get; init; }
|
||||||
|
}
|
||||||
22
src/Roboto.Memory/Snapshots/UiQuestEntry.cs
Normal file
22
src/Roboto.Memory/Snapshots/UiQuestEntry.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Roboto.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A quest group from the UI element tree (one per quest_display).
|
||||||
|
/// Contains the quest title and current objective steps.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UiQuestGroup
|
||||||
|
{
|
||||||
|
/// <summary>Quest title from title_layout → title_label (e.g. "Treacherous Ground").</summary>
|
||||||
|
public string? Title { get; set; }
|
||||||
|
/// <summary>Current quest objective steps.</summary>
|
||||||
|
public List<UiQuestStep> Steps { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single quest objective from quest_info_entry → quest_info.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UiQuestStep
|
||||||
|
{
|
||||||
|
/// <summary>Objective text (e.g. "Search Clearfell for the entrance to the Mud Burrow").</summary>
|
||||||
|
public string? Text { get; set; }
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue