quests and queststate work

This commit is contained in:
Boki 2026-03-05 11:26:30 -05:00
parent 94b460bbc8
commit 445ae1387c
27 changed files with 3815 additions and 179 deletions

1
.gitignore vendored
View file

@ -34,3 +34,4 @@ nul
# Extras
lib/extras
lib/ExileCore-master

View file

@ -9,6 +9,7 @@
"ControlZone",
"CritterAI",
"DiesAfterTime",
"F",
"Functions",
"GlobalAudioParamEvents",
"HideoutDoodad",

View file

@ -11,20 +11,35 @@
"Metadata/Chests/EzomyteChest_02",
"Metadata/Chests/EzomyteChest_05",
"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/MossyBoulder1",
"Metadata/Chests/MossyBoulder2",
"Metadata/Chests/MossyChest11",
"Metadata/Chests/MossyChest11MagicAndRare",
"Metadata/Chests/MossyChest13",
"Metadata/Chests/MossyChest14",
"Metadata/Chests/MossyChest14MagicAndRare",
"Metadata/Chests/MossyChest17",
"Metadata/Chests/MossyChest20",
"Metadata/Chests/MossyChest21",
"Metadata/Chests/MossyChest26",
"Metadata/Chests/MuddyChest1",
"Metadata/Chests/SirenEggs/SirenEgg_02",
"Metadata/Critters/BloodWorm/BloodWormBrown",
"Metadata/Critters/Chicken/Chicken_kingsmarch",
"Metadata/Critters/Crow/Crow",
"Metadata/Critters/Ferret/Ferret",
"Metadata/Critters/Hedgehog/HedgehogSlow",
"Metadata/Critters/Spider/NurseryWebSpider",
"Metadata/Critters/Weta/Basic",
"Metadata/Effects/BeamEffect",
"Metadata/Effects/Effect",
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummy",
"Metadata/Effects/Microtransactions/Town_Portals/PersonSplitPortal/_PersonSplitPortalPrespawnDummyMarble",
@ -32,19 +47,37 @@
"Metadata/Effects/Microtransactions/foot_prints/harvest02/footprints_harvest",
"Metadata/Effects/PermanentEffect",
"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/MudBurrower/mudburrower_chasm",
"Metadata/Effects/Spells/monsters_effects/Act1_FOUR/MudBurrower/mudburrower_chasm_body",
"Metadata/Effects/Spells/sandstorm_swipe/sandstorm_swipe_storm",
"Metadata/MiscellaneousObjects/AreaTransitionBlockage",
"Metadata/MiscellaneousObjects/AreaTransitionDoodad",
"Metadata/MiscellaneousObjects/AreaTransition_Animate",
"Metadata/MiscellaneousObjects/BossTargetMarkerSerialized",
"Metadata/MiscellaneousObjects/BossTargetMarkerSerialized2",
"Metadata/MiscellaneousObjects/CameraZoom/MinorZoomIn",
"Metadata/MiscellaneousObjects/CameraZoom/TreeOfSouls",
"Metadata/MiscellaneousObjects/Checkpoint",
"Metadata/MiscellaneousObjects/CheckpointTutorial",
"Metadata/MiscellaneousObjects/Doodad",
"Metadata/MiscellaneousObjects/DoodadInvisible",
"Metadata/MiscellaneousObjects/DoodadNoBlocking",
"Metadata/MiscellaneousObjects/Environment/EnvLineEnd",
"Metadata/MiscellaneousObjects/Environment/EnvLineStart",
"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_6_4",
"Metadata/MiscellaneousObjects/Environment/NonDefaultFlows/FlowSink_8_8",
"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/HealingWell",
"Metadata/MiscellaneousObjects/LeagueIncursionNew/IncursionPedestalCrystal_1",
@ -58,19 +91,46 @@
"Metadata/MiscellaneousObjects/ReviveIcon",
"Metadata/MiscellaneousObjects/ServerDoodadHidden",
"Metadata/MiscellaneousObjects/Stash",
"Metadata/MiscellaneousObjects/TargetMarker1",
"Metadata/MiscellaneousObjects/TutorialArrow",
"Metadata/MiscellaneousObjects/Waypoint",
"Metadata/MiscellaneousObjects/WorldItem",
"Metadata/Monsters/BansheeRemake/WitchHut/Objects/AmbushLocation",
"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/FungusZombieMedium",
"Metadata/Monsters/Hags/Objects/BossRoomMinimapIcon",
"Metadata/Monsters/Hags/UrchinHag1",
"Metadata/Monsters/Hags/UrchinHagBoss",
"Metadata/Monsters/HuhuGrub/CinematicHuhuGrub",
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeEmerge1",
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeEmergeSummoned1_",
"Metadata/Monsters/HuhuGrub/HuhuGrubLarvaeRanged1",
"Metadata/Monsters/InvisibleFire/MDCarrionCroneWave",
"Metadata/Monsters/MonsterMods/GroundOnDeath/ShockedGroundDaemonParent",
"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/SlingUrchin1",
"Metadata/Monsters/Werewolves/WerewolfPack1",
@ -84,6 +144,14 @@
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryOneHandAxePhysics__",
"Metadata/Monsters/Zombies/Lumberjack/LumberingDrownedDryUnarmed",
"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/ClearfellPosting3",
"Metadata/NPC/Four_Act1/DogTrader_Entrance",
@ -96,6 +164,9 @@
"Metadata/NPC/Four_Act1/HoodedMentor",
"Metadata/NPC/Four_Act1/HoodedMentorAfterIronCount",
"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/RenlyAfterIronCount",
"Metadata/NPC/Four_Act1/RenlyIntro",
@ -103,6 +174,8 @@
"Metadata/NPC/Four_Act1/UnaAfterHealHoodedMentor",
"Metadata/NPC/Four_Act1/UnaAfterIronCount",
"Metadata/NPC/Four_Act1/UnaHoodedOneInjured",
"Metadata/NPC/Four_Act1/UnaTreeSummon",
"Metadata/NPC/Four_Act1/UnaTreeSummonKneeling",
"Metadata/NPC/League/Incursion/AlvaIncursionWild",
"Metadata/Pet/AzmeriStag/AzmeriStag",
"Metadata/Pet/BabyBossesHumans/BabyBrutus/BabyBrutus",
@ -128,20 +201,48 @@
"Metadata/Pet/ScavengerBat/ScavengerBat",
"Metadata/Pet/WayfinderWolf/WayfinderWolf",
"Metadata/Projectiles/CarrionCroneIceSpear",
"Metadata/Projectiles/Fireball",
"Metadata/Projectiles/HagBossIceShard",
"Metadata/Projectiles/HuhuGrubLarvaeMortar",
"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/Spark",
"Metadata/Projectiles/Twister",
"Metadata/QuestObjects/Four_Act1/TreeOfSoulsRoots",
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1",
"Metadata/Terrain/Doodads/Gallows/ClearfellBull1_CountKilled",
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2",
"Metadata/Terrain/Doodads/Gallows/ClearfellBull2_CountKilled",
"Metadata/Terrain/Gallows/Act1/1_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/CampsiteController",
"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/HoodedMentorController",
"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_town_ExileEncampment/Objects/Act1_finished_LightController",
"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/VisitedAct2_DisableRendering",
"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/ForestEntrance",
"Metadata/Terrain/Tools/AudioTools/G1_2/HagArena",
"Metadata/Terrain/Tools/AudioTools/G1_2/RiverRapidsMedium",
"Metadata/Terrain/Tools/AudioTools/G1_3/TunnelA",
"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/InsideWaterMillAudio"
]

View file

@ -50,10 +50,17 @@
"QuestCompanionObjPtrOffset": "0x10",
"QuestTrackedMarker": "0x43020000",
"QuestObjEncounterStateOffset": 8,
"QuestDatRowSize": "0x77",
"QuestDatRowSize": "0x68",
"QuestDatNameOffset": 0,
"QuestDatOrderOffset": "0x10",
"QuestDatTextOffset": "0x34",
"QuestDatMessageOffset": "0x3D",
"QuestDatInternalIdOffset": "0x6B",
"QuestDatActOffset": "0x73",
"QuestStateObjectOffset": "0x900",
"QuestStateVectorOffset": "0x240",
"QuestStateEntrySize": 12,
"QuestStateMaxEntries": "0x100",
"ComponentListOffset": "0x10",
"EntityHeaderOffset": 8,
"ComponentLookupOffset": "0x28",
@ -94,5 +101,15 @@
"UiElementVisibleBit": 11,
"UiElementSizeOffset": "0x288",
"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"
}

View file

@ -1,3 +1,5 @@
{
"GooGoGaaGa": "GooGoGaaGa_Default_Copy"
"GooGoGaaGa": "GooGoGaaGa_Default_Copy",
"terdsare": "terdsare_Default",
"dudemoko": "dudemoko_Default"
}

View 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
}
]
}

View 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
}
]
}

View file

@ -16,7 +16,10 @@ public partial class MemoryNodeViewModel : ObservableObject
[ObservableProperty] private string _name;
[ObservableProperty] private string _value = "";
[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; } = [];
@ -93,8 +96,6 @@ public partial class MemoryViewModel : ObservableObject
private MemoryNodeViewModel? _currentStateNode;
private MemoryNodeViewModel? _isLoadingNode;
private MemoryNodeViewModel? _escapeStateNode;
private MemoryNodeViewModel? _activeStatesNode;
private MemoryNodeViewModel? _statesNode;
private MemoryNodeViewModel? _terrainCells;
private MemoryNodeViewModel? _terrainGrid;
private MemoryNodeViewModel? _terrainWalkable;
@ -102,7 +103,6 @@ public partial class MemoryViewModel : ObservableObject
private MemoryNodeViewModel? _entityTypesNode;
private MemoryNodeViewModel? _entityListNode;
private MemoryNodeViewModel? _skillsNode;
private MemoryNodeViewModel? _questsNode;
private MemoryNodeViewModel? _areaRawName;
private MemoryNodeViewModel? _areaDisplayName;
private MemoryNodeViewModel? _areaAct;
@ -110,6 +110,8 @@ public partial class MemoryViewModel : ObservableObject
private MemoryNodeViewModel? _areaHasWaypoint;
private MemoryNodeViewModel? _areaMonsterLevel;
private MemoryNodeViewModel? _worldAreaId;
private MemoryNodeViewModel? _uiElementsNode;
private MemoryNodeViewModel? _questLinkedListNode;
partial void OnIsEnabledChanged(bool value)
{
@ -178,8 +180,6 @@ public partial class MemoryViewModel : ObservableObject
_currentStateNode = new MemoryNodeViewModel("Current State:");
_isLoadingNode = new MemoryNodeViewModel("Loading:");
_escapeStateNode = new MemoryNodeViewModel("Escape:");
_activeStatesNode = new MemoryNodeViewModel("Controller") { IsExpanded = true };
_statesNode = new MemoryNodeViewModel("State Slots") { IsExpanded = true };
gameState.Children.Add(_gsPattern);
gameState.Children.Add(_gsBase);
gameState.Children.Add(_gsController);
@ -188,8 +188,6 @@ public partial class MemoryViewModel : ObservableObject
gameState.Children.Add(_currentStateNode);
gameState.Children.Add(_isLoadingNode);
gameState.Children.Add(_escapeStateNode);
gameState.Children.Add(_activeStatesNode);
gameState.Children.Add(_statesNode);
// InGameState children
var inGameStateGroup = new MemoryNodeViewModel("InGameState");
@ -215,14 +213,12 @@ public partial class MemoryViewModel : ObservableObject
_playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" };
_playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" };
_playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" };
_skillsNode = new MemoryNodeViewModel("Skills") { IsExpanded = false };
_questsNode = new MemoryNodeViewModel("Quests") { IsExpanded = false };
_skillsNode = new MemoryNodeViewModel("Skills");
player.Children.Add(_playerPos);
player.Children.Add(_playerLife);
player.Children.Add(_playerMana);
player.Children.Add(_playerEs);
player.Children.Add(_skillsNode);
player.Children.Add(_questsNode);
// Entities
var entitiesGroup = new MemoryNodeViewModel("Entities");
@ -259,11 +255,19 @@ public partial class MemoryViewModel : ObservableObject
areaTemplateGroup.Children.Add(_areaMonsterLevel);
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(areaTemplateGroup);
inGameStateGroup.Children.Add(player);
inGameStateGroup.Children.Add(entitiesGroup);
inGameStateGroup.Children.Add(terrain);
inGameStateGroup.Children.Add(_questLinkedListNode);
inGameStateGroup.Children.Add(_uiElementsNode);
RootNodes.Add(process);
RootNodes.Add(gameState);
@ -334,103 +338,6 @@ public partial class MemoryViewModel : ObservableObject
_isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading);
_escapeStateNode!.Set(snap.IsEscapeOpen ? "Open" : "Closed", !snap.IsEscapeOpen);
// Controller dump — show qwords before state slots to find active state offset
if (_activeStatesNode is not null)
{
var pre = snap.ControllerPreSlots;
_activeStatesNode.Value = pre.Length > 0
? $"controller+0x00..0x{pre.Length * 8:X} ({pre.Length} qwords)"
: "no data";
_activeStatesNode.ValueColor = "#8b949e";
while (_activeStatesNode.Children.Count > pre.Length)
_activeStatesNode.Children.RemoveAt(_activeStatesNode.Children.Count - 1);
for (var i = 0; i < pre.Length; i++)
{
var (off, val, match, changed, derefInfo) = pre[i];
var label = $"+0x{off:X2}:";
var changeTag = changed ? " [CHANGED]" : "";
var derefTag = derefInfo != null ? $" ({derefInfo})" : "";
var display = val == 0
? "0"
: match != null
? $"0x{val:X} ← {match}{changeTag}"
: $"0x{val:X}{derefTag}{changeTag}";
var color = changed ? "#f85149"
: match != null ? "#3fb950"
: derefInfo != null && derefInfo.Contains('→') && derefInfo.Contains("State") ? "#d29922" // yellow for indirect state match
: val == 0 ? "#484f58"
: "#8b949e";
if (i < _activeStatesNode.Children.Count)
{
_activeStatesNode.Children[i].Name = label;
_activeStatesNode.Children[i].Value = display;
_activeStatesNode.Children[i].ValueColor = color;
}
else
{
var node = new MemoryNodeViewModel(label) { Value = display, ValueColor = color };
_activeStatesNode.Children.Add(node);
}
}
}
// State Slots — show pointer + int32 at +0x08 for each state slot
if (_statesNode is not null && snap.StateSlots.Length > 0)
{
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
if (snap.Attached)
{
@ -593,48 +500,106 @@ public partial class MemoryViewModel : ObservableObject
}
}
// Quest states with rich info from companion vector
if (_questsNode is not null)
// Quest Linked Lists (all quests + tracked merged from GameUi)
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);
_questsNode.Value = $"{snap.QuestFlags.Count} quest states ({named} named)";
_questsNode.ValueColor = "#3fb950";
var active = snap.QuestLinkedList.Count(q => q.StateId > 0);
var tracked = snap.QuestLinkedList.Count(q => q.IsTracked);
_questLinkedListNode.Value = $"{snap.QuestLinkedList.Count} total, {active} active, {tracked} tracked";
_questLinkedListNode.ValueColor = "#3fb950";
while (_questsNode.Children.Count > snap.QuestFlags.Count)
_questsNode.Children.RemoveAt(_questsNode.Children.Count - 1);
for (var i = 0; i < snap.QuestFlags.Count; i++)
// Only update children if expanded
if (_questLinkedListNode.IsExpanded)
{
var q = snap.QuestFlags[i];
var trackedPrefix = q.IsTracked ? "[T] " : "";
var stateLabel = q.StateId switch { 1 => "locked", 2 => "started", _ => $"s{q.StateId}" };
var label = $"{trackedPrefix}{q.QuestName ?? (q.QuestStateIndex > 0 ? $"#{q.QuestStateIndex}" : $"[{i}]")}";
var value = q.InternalId is not null
? $"idx={q.QuestStateIndex} {stateLabel} id={q.InternalId}"
: $"idx={q.QuestStateIndex} {stateLabel}";
var sorted = snap.QuestLinkedList
.Where(q => q.StateId > 0)
.OrderByDescending(q => q.IsTracked)
.ThenByDescending(q => q.StateId > 0)
.ThenBy(q => q.Act)
.ThenBy(q => q.DisplayName)
.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;
_questsNode.Children[i].Value = value;
_questsNode.Children[i].ValueColor = color;
}
else
{
var node = new MemoryNodeViewModel(label) { Value = value, ValueColor = color };
_questsNode.Children.Add(node);
var q = sorted[i];
var prefix = q.IsTracked ? "[T] " : "";
var stateLabel = $"step {q.StateId}";
var label = $"{prefix}{q.DisplayName ?? q.InternalId ?? $"[{i}]"}";
var value = $"Act{q.Act} {stateLabel}";
var color = q.IsTracked ? "#58a6ff" : "#d29922";
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
{
_questsNode.Value = "—";
_questsNode.ValueColor = "#484f58";
_questsNode.Children.Clear();
_questLinkedListNode.Value = "—";
_questLinkedListNode.ValueColor = "#484f58";
_questLinkedListNode.Children.Clear();
}
}
@ -688,9 +653,101 @@ public partial class MemoryViewModel : ObservableObject
_terrainWalkable!.Set("?", false);
}
// UIElements tree
UpdateUiElementsTree(snap.GameUiPtr);
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)
{
// Skip rendering entirely during loading — terrain data is stale/invalid
@ -1390,6 +1447,90 @@ public partial class MemoryViewModel : ObservableObject
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]
private void ScanAreaTemplateExecute()
{
@ -1401,4 +1542,18 @@ public partial class MemoryViewModel : ObservableObject
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);
}
}

View file

@ -150,7 +150,19 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
var config = new BotConfig();
var reader = new GameMemoryReader();
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);

View file

@ -784,8 +784,24 @@
Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />
<Button Content="Quest Objects" Command="{Binding ScanQuestObjectsExecuteCommand}"
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}"
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>
<TextBox Text="{Binding ScanResult, Mode=OneWay}" FontFamily="Consolas"
FontSize="10" Foreground="#e6edf3" Background="#0d1117"
@ -803,6 +819,11 @@
BorderThickness="1" CornerRadius="8" Padding="8">
<TreeView ItemsSource="{Binding RootNodes}"
Background="Transparent">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}"
x:DataType="vm:MemoryNodeViewModel">

View file

@ -22,6 +22,8 @@ public class GameState
public DangerLevel Danger { get; set; }
public Matrix4x4? CameraMatrix { 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)
public ThreatMap Threats { get; set; } = new();

View 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; } = [];
}

View file

@ -1,5 +1,6 @@
using System.Numerics;
using Roboto.Core;
using Roboto.Memory;
namespace Roboto.Data;
@ -89,6 +90,18 @@ public sealed class GameDataCache
public volatile string? CurrentAreaName;
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 ──
public volatile GameState? LatestState;

View file

@ -144,6 +144,10 @@ public sealed class MemoryPoller : IDisposable
_cache.AreaHash = state.AreaHash;
_cache.AreaLevel = state.AreaLevel;
_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.ColdTickTimestamp = Environment.TickCount64;
@ -331,25 +335,42 @@ public sealed class MemoryPoller : IDisposable
if (snap.QuestFlags is { Count: > 0 })
{
state.ActiveQuests = snap.QuestFlags.Select(q => new QuestProgress
{
QuestStateIndex = q.QuestStateIndex,
QuestName = q.QuestName,
InternalId = q.InternalId,
StateId = q.StateId,
IsTracked = q.IsTracked,
StateText = q.StateText,
ProgressText = q.ProgressText,
}).ToList();
// StateId: 1=available/in-progress, 2=completed, 3+=special
// Filter to non-completed quests for ActiveQuests
state.ActiveQuests = snap.QuestFlags
.Where(q => q.StateId != 2) // exclude completed
.Select(q => new QuestProgress
{
QuestStateIndex = q.QuestStateIndex,
QuestName = q.QuestName,
InternalId = q.InternalId,
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("Quest state indices ({Count}): [{Indices}]", snap.QuestFlags.Count, indices);
_lastQuestCount = snap.QuestFlags.Count;
Log.Debug("Active quests: {Active}/{Total} (filtered ES!=2)",
activeCount, 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)
{
state.Terrain = new WalkabilitySnapshot

View 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

View file

@ -44,6 +44,7 @@ public class GameMemoryReader : IDisposable
public MemoryContext? Context => _ctx;
public ComponentReader? Components => _components;
public GameStateReader? StateReader => _stateReader;
public UIElements? UIElements => _gameStates?.InGame.UIElements;
public GameMemoryReader()
{
@ -166,7 +167,15 @@ public class GameMemoryReader : IDisposable
var gs = _gameStates!;
// 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())
return snap;
@ -179,7 +188,7 @@ public class GameMemoryReader : IDisposable
snap.CurrentGameState = gs.CurrentState;
snap.ControllerPreSlots = gs.ControllerPreSlots;
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.AreaInstancePtr = ai.Address;
snap.ServerDataPtr = ai.ServerDataPtr;
@ -249,6 +258,7 @@ public class GameMemoryReader : IDisposable
// Skills & quests — read from hierarchy
snap.PlayerSkills = ai.PlayerSkills.Skills;
snap.QuestFlags = ai.QuestFlags.Quests;
snap.QuestStates = ai.QuestStates;
// Read state flag bytes
if (snap.InGameStatePtr != 0)
@ -261,6 +271,15 @@ public class GameMemoryReader : IDisposable
snap.TerrainHeight = ai.Terrain.TerrainHeight;
snap.Terrain = ai.Terrain.Grid;
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)

View file

@ -183,15 +183,41 @@ public sealed class GameOffsets
public uint QuestTrackedMarker { get; set; } = 0x43020000;
/// <summary>Offset within the quest state object to the encounter state byte (1=locked, 2=started). 0x08.</summary>
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) ──
/// <summary>Size of each .dat row in bytes. 0x77 = 119. 0 = name resolution disabled.</summary>
public int QuestDatRowSize { get; set; } = 0x77;
/// <summary>Dat row → Quest display name wchar* pointer.</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; } = 0x68;
/// <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;
/// <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;
/// <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;
// ── Entity / Component ──
@ -283,6 +309,39 @@ public sealed class GameOffsets
/// <summary>How many bytes to scan from InGameState for UIElement pointers (0x1000 = 4KB).</summary>
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) ──
/// <summary>Offset from AreaInstance to inline TerrainStruct (dump: 0xCC0).</summary>
public int TerrainListOffset { get; set; } = 0xCC0;

View file

@ -15,12 +15,12 @@ public sealed class AreaInstance : RemoteObject
public nint ServerDataPtr { get; private set; }
public nint LocalPlayerPtr { get; private set; }
public int EntityCount { get; private set; }
public EntityList EntityList { get; }
public PlayerSkills PlayerSkills { get; }
public QuestFlags QuestFlags { get; }
public Terrain Terrain { get; }
public AreaTemplate AreaTemplate { get; }
public List<QuestStateEntry>? QuestStates { get; private set; }
public AreaInstance(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
: base(ctx)
@ -101,6 +101,9 @@ public sealed class AreaInstance : RemoteObject
else
QuestFlags.Reset();
// Quest state container (AI+0x900 → obj → +0x240 vector)
QuestStates = ReadQuestStates(mem, offsets);
// AreaTemplate — pointer at AreaInstance + AreaTemplateOffset
var areaTemplatePtr = mem.ReadPointer(Address + offsets.AreaTemplateOffset);
if (areaTemplatePtr != 0)
@ -131,6 +134,45 @@ public sealed class AreaInstance : RemoteObject
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()
{
AreaLevel = 0;
@ -138,6 +180,7 @@ public sealed class AreaInstance : RemoteObject
ServerDataPtr = 0;
LocalPlayerPtr = 0;
EntityCount = 0;
QuestStates = null;
EntityList.Reset();
PlayerSkills.Reset();
QuestFlags.Reset();

View file

@ -14,17 +14,20 @@ public sealed class InGameState : RemoteObject
public bool IsEscapeOpen { get; private set; }
public AreaInstance AreaInstance { get; }
public WorldData WorldData { get; }
public UIElements UIElements { get; }
public InGameState(MemoryContext ctx, ComponentReader components, MsvcStringReader strings, QuestNameLookup? questNames)
: base(ctx)
{
AreaInstance = new AreaInstance(ctx, components, strings, questNames);
WorldData = new WorldData(ctx);
UIElements = new UIElements(ctx, strings);
}
protected override bool ReadData()
{
var mem = Ctx.Memory;
var offsets = Ctx.Offsets;
// Read the full InGameState struct (0x310 = 784 bytes, 1 RPM)
_data = mem.Read<IgsStruct>(Address);
@ -39,6 +42,9 @@ public sealed class InGameState : RemoteObject
WorldData.FallbackCameraPtr = _data.CameraPtr;
WorldData.Update(_data.WorldDataPtr);
// Cascade to UIElements — pass InGameState address for UiRootStruct chain
UIElements.Update(Address);
return true;
}
@ -48,5 +54,6 @@ public sealed class InGameState : RemoteObject
IsEscapeOpen = false;
AreaInstance.Reset();
WorldData.Reset();
UIElements.Reset();
}
}

View file

@ -94,6 +94,7 @@ public sealed class QuestFlags : RemoteObject
}
var datTableBase = FindDatTableBase(offsets);
var useStdWString = offsets.QuestObjStateTextType == "std_wstring";
var result = new List<QuestSnapshot>(entryCount);
@ -105,6 +106,8 @@ public sealed class QuestFlags : RemoteObject
byte stateId = 0;
bool isTracked = false;
nint questObjPtr = 0;
string? stateText = null;
string? progressText = null;
if (compData is not null && i < compEntryCount)
{
@ -121,26 +124,51 @@ public sealed class QuestFlags : RemoteObject
{
questObjPtr = (nint)BitConverter.ToInt64(compData, compOff + offsets.QuestCompanionObjPtrOffset);
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF
&& offsets.QuestObjEncounterStateOffset > 0)
if (questObjPtr != 0 && ((ulong)questObjPtr >> 32) is > 0 and < 0x7FFF)
{
var stateByte = mem.ReadBytes(questObjPtr + offsets.QuestObjEncounterStateOffset, 1);
if (stateByte is { Length: 1 })
stateId = stateByte[0];
if (offsets.QuestObjEncounterStateOffset > 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;
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
}
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
{
questName = entry?.Name;
internalId = entry?.InternalId;
if (datTableBase != 0 && offsets.QuestDatRowSize > 0)
{
var rowAddr = datTableBase + idx * offsets.QuestDatRowSize;
questName = ResolveDatString(rowAddr + offsets.QuestDatNameOffset);
internalId = ResolveDatString(rowAddr + offsets.QuestDatInternalIdOffset);
}
else if (_nameLookup is not null && _nameLookup.TryGet(idx, out var entry))
{
questName = entry?.Name;
internalId = entry?.InternalId;
}
}
result.Add(new QuestSnapshot
@ -151,12 +179,26 @@ public sealed class QuestFlags : RemoteObject
InternalId = internalId,
StateId = stateId,
IsTracked = isTracked,
StateText = stateText,
ProgressText = progressText,
});
}
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)
{
if (offsets.QuestDatRowSize <= 0) return 0;

View 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;
}
}

View file

@ -78,6 +78,18 @@ public class GameStateSnapshot
// Quest flags (from ServerData → PlayerServerData)
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
public Matrix4x4? CameraMatrix;

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}