From 55e48dd3024cc0cf2e5a90f02007d3d8da3496cb Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 28 Oct 2026 00:07:06 +0100 Subject: [PATCH] Quests: main quest items no longer refused, restart resets bit-field variables AddMainQuestItem refused every new main quest item once the list held an entry with script id 0 (scripts give such items); the Goldbeeren of quest 402 were picked up and lost. The script id test is patched out. Quest variables are bit fields (op 0x44 sets a bit): the quest giver restart now writes the values directly, 0 and 1 (available) for the variable named after the quest. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 1 + docs/quests.md | 41 +++++++++++++++++++++++++++++++++++++--- src/game/engine/quests.h | 35 +++++++++++++++++++++++++++++++--- src/mods/questgiver.cpp | 28 +++++++++++++-------------- src/mods/questitems.cpp | 37 ++++++++++++++++++++++++++++++++++++ tools/names.txt | 4 ++++ 6 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 src/mods/questitems.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e722046..701b4b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ add_library(sacredK SHARED src/mods/followers.cpp src/mods/questfail.cpp src/mods/questgiver.cpp + src/mods/questitems.cpp src/mods/questnpc.cpp src/mods/anticheat.cpp src/mods/loadingscreen.cpp diff --git a/docs/quests.md b/docs/quests.md index 54950fa..a79ca1f 100644 --- a/docs/quests.md +++ b/docs/quests.md @@ -196,7 +196,8 @@ Dialogs without a quest (healers, closed shops: `geschlossen101`, 4 trigger area, 0x0f end quest, 0x14 start quest (#id), 0x17 named position, 0x1a dialog text, 0x35 book text, 0x3a / 0x42 if / else if (0x49 var == n, 0x40 "@npc"), 0x3c dialog answer (text, label), 0x3f - compass, 0x44 set variable, 0x48 move NPC, 0x56 dialog state. + compass, 0x43 / 0x44 / 0x45 / 0x4b variables (see below), 0x48 move NPC, + 0x56 dialog state. - Script NPC names: "Res:" is a script id (object +0x3c, `0x44a390`); the quest manager maps it to the object (`0x464ec0`, map +0x7668); a spawn sets the entry. @@ -227,8 +228,8 @@ load we need a repair ... if not completed or correctly running". `mods/questgiver.cpp`: a quest's giver is the NPC whose spawn instruction carries a dialog with an answer that accepts the quest (85 givers). When a label of a running quest runs for its dead giver, it is skipped, the quest -restarts (failed entry at the giver, the variables only its labels set back to -0), and once the body is gone the spawn instruction runs again. After a load, +restarts (failed entry at the giver, the variables only its labels set back: +0, the one named after the quest 1), and once the body is gone the spawn instruction runs again. After a load, an accepted quest (flag 4) without its giver gets the same. Not yet seen in the game (the escort of "Hetzjagd" arrived alive); see `docs/todo.md` for the audit of how quests can fail. @@ -237,6 +238,40 @@ Lesson (2026-10-26): flag 1 means available, not running; a first version took it for running and reset 16 available quests and spawned their NPCs on Borf (repaired once, then saved). +## Quest variables are bit fields (measured 2026-10-28) + +Found while repairing "Kräuter für Kräuterhexe" (402; the user knows it as +Windgeflüster). Variables live in the quest manager's vector +0x7550 / ++0x7554 (0x24-byte entries: name[0x20], value +0x20; `game::ScriptVariable`). +Script writes: 0x43 sets the value, 0x44 sets bit n (0x49bb30: value |= 1 << +n), 0x45 clears bit n (0x49c450), 0x4b adds n (0x49cc20). Conditions +(`Script_Condition` 0x498aa0): 0x49 bit n set, 0x4a bit n clear (logged "if +IsNotVarBit"), 0x48 value == n; a missing variable is false. The variable +named after a quest has bit 0 while the quest is available: all 87 quests not +yet accepted in a dwarf save had value 1, and nothing in the quest's own labels +sets it. Quest 402: accepting sets bit 1, the berries bit 2, the reward bit 3; +the giver's dialog offers the quest on bit 0. + +The quest giver restart first ran 0x44 with 0 ("variable = 0"), which set bit +0 and cleared nothing: the giver kept saying "Zögerst du in deinen Taten?". +A reset to 0 then matched no dialog branch at all. Now the value is written +directly: 0, or 1 for the quest's own variable. Confirmed 2026-10-28. + +## Main quest items refused (request 2026-10-28) + +"I cannot finish the Quest Windgeflüster." The berries (type 6029) are an item +`Region4Init` places once on the ground near the Goldbeerenbusch (op 0x5e, +pickup label `take402`); picking it up logged "cInventoryEntry::addMainQuestItem() +Already has mainQuestItem(6029)" and the item was gone. +`cInventoryEntry::AddMainQuestItem` (0x5d8240, thiscall (type, script id, +notify)) keeps {type, script id} pairs and refuses a new item when an entry +has the same type or script id 0 (0x5d83c6); scripts that give an item +without a "Res:" id add it with 0 (0x486bd8). The dwarf held {4609, 0}, so +every later main quest item was refused (a bug of the original). +`mods/questitems.cpp` removes the script id test. The save was repaired once +by running `take402` for the hero after the load (quest accepted, bit 2 +missing); the quest then finished. Confirmed 2026-10-28. + ## Escorts never left behind (2026-10-26) "I have lost a follower NPC from the quest 'Die Sklaven der Orks', it's no diff --git a/src/game/engine/quests.h b/src/game/engine/quests.h index 36e01ef..15fa44c 100644 --- a/src/game/engine/quests.h +++ b/src/game/engine/quests.h @@ -77,10 +77,20 @@ inline uint32_t ScriptLabelCodeSize(const uint8_t* label) { // args are tagged (0x01 a string, 0x0b an int32, ...). Script_RunLabel // (0x46bd80) reads a label's code and runs it instruction by instruction with // 0x475970. Opcodes used here: 1 spawns a creature ("Res: