# Quests ## Quest data (measured 2026-10-17 / 2026-10-25) - Quest table `g_quests` (vector begin/end `0xaa9e98` / `0xaa9e9c`), 0x124 bytes per quest: id +0, internal name +4 (ANSI), scripts +0x10c (run on trigger), +0x114 (one-time, cleared after it ran), +0x118 success end, +0x11c failure end (only quests with one can fail), flags +0x120: 1 triggered, 2 done, 4 in progress, 8 busy (while the trigger runs its scripts). `game/engine/quests.h`. - Quest manager: static object `0xaaaf00` (script interpreter). Quest book vector +0x424..+0x428, plain 0x174-byte entries (serialized field by field): state +4 (2 active, 100 solved, 101 failed), quest id +8, map position +0x14..+0x20; per player (16) a selected entry index at +0x3a8 + player * 8. - Script opcodes (keyword table `0x452540`): LoseQuest `0x36` and ExitQuest `0x0f` -> `0x48f110` -> `Quest_End` `0x46ced0` (thiscall (index, ?, success), ret 0xc; LoseQuest success 0, ExitQuest 1). QuestDone `0x0a` tests flag 2, QuestInProgress `0x0c` flag 4. - `Quest_Trigger` `0x46de10` (cdecl (index, ?, ?, ?), plain ret): does nothing if flag 1 is set; else sets 1 | 8, runs +0x114 (then cleared) and +0x10c, clears 8. Talking to a giver triggered a quest (flags 0 -> 1, no book entry yet); accepting it set flag 4 and added a book entry, state 2. - `Quest_End`: if the quest is in progress and not done, runs the end script (success / failure) and sets done (flags 3); the book entry's state becomes 100 / 101. Quests with ids 2600..3599 (the generic "Bringe NPC zu ...", "Eskortiere ...", ...) are copied to the end of the table under a new id (largest + 1, at least 10000): the game repeats those itself. - Compiled quest scripts: `bin\sgf.bin` (labels such as `LoseQuest_10255`, texts `res:...`; random side quests use `DQ_` names, time limits `DQ_ZEITFENSTER_TAGE`). - Example (Gladiator, Silber, 2026-10-25): failed Hetzjagd 1502, Pony Express 5005, Rette Jungfrau 2504, Eskortiere Händler zu geheimen Treffen 1403, all flags 3, book state 101. ## Failed quests restartable (request 2026-10-17) A failed quest is not final: talking to the quest giver again offers it again. `mods/questfail.cpp`: after `Quest_End` with success 0 (its failure script has run) the quest's flags 2/4/8 are cleared and flag 1 is kept (book entry: see below); quests failed before the mod are reset once after loading. Confirmed in the log (the four quests above reset, 2026-10-25); the giver offering them again is still to be tested in the game. 2026-10-26: the restart first cleared flag 1 as well. Measured: a quest's script labels, its giver's dialog too, run only while flag 1 is set (Script_RunLabel `0x46bf11`; "Dialog faild" in the game log otherwise), and accepting it (opcode 0x14, `0x48f320`) needs it too; the game sets it when the hero enters the quest's region (`Quest_Trigger`). A restarted quest is now "available": flags 1. 2026-10-30 (quest audit, `docs/quest-audit.md`): a restart also resets the quest's variables, deletes what its `QIS_OnEnter` created, brings back the giver and the NPCs it needs, and gives the giver the accept dialog again; some failures stay final (generic quests, no giver, story endings, NPCs gone for good). The script facts come from `mods/questscript.cpp`. Marker back to the giver (request 2026-10-25: "quests that failed, should have the blue marker point back to the quest giver"): - Measured: the maps draw a marker for every book entry at its compass cell +0x18/+0x1c, colour from `0x4975d0` (thiscall (entry, kind), ret 8): open side quest `ff006cff` (blue), main `ffffff80`, solved green, failed red. The compass target +0x10/+0x14 is set by the scripts' QuestkompassPos (`0x49a7a0`: a cell, radius +0x20 "rad:") and QuestkompassOBJ (`0x49af70`: 0xffffffff + object id, or 0xeeeeeeee + script NPC id); `0x4a6740` resolves it into +0x18/+0x1c. The game keeps no giver per quest. - A quest is accepted in a dialog answer (`cDialogManager::Answer` 0x49a090 -> quest script -> `QuestManager_AddBookEntry` 0x46c450). The mod keeps the map cell of the NPC nearest to the hero then (within 400) as the giver, saved with the hero data (`questGivers`: quest id -> cell, merged from save game and hero file; quest ids and givers are the same in every world). - On failure the book entry stays (state 101) with its compass on the giver and is drawn blue; without a known giver it is removed as before (all quests that failed before this change). Accepting the quest again removes the failed entry. ## Quest book (measured 2026-10-25) - Book entries (quest manager +0x424, 0x174 bytes): kind +0 (3 main quest, 4 side quest: ids 1..99 are main quests, 0x46c450), state +4, quest id +8, title / text ids +0x24 / +0x28, progress log text ids +0x2c.., map position +0x18 / +0x1c (0x497830), chapter +0x16c (u16). A new entry (0x46c450, thiscall (quest index, ?), ret 8, called by the script interpreter 0x48f602) copies its chapter from the player's current main quest entry (+0x3a4 + slot * 8; selected entry +0x3a8 + slot * 8, read by 0x49dd40). - cUI_Book (one per book mode, 0x7a8[mode] of the book window; quest mode +0x166 = 0): list builder 0x6b04c0 (thiscall, from the rebuild 0x6b1670), page-full check 0x6b1a70 (thiscall (tab, page), ret 8), render 0x6b27f0 (thiscall (renderer, bookmarks only), ret 8: the left page's rows, then the selected row's detail record on the right, 0x6b1cd0 thiscall (renderer, record, sub-page), ret 0xc), events 0x6b3620 (thiscall (event), ret 4). Current tab +0x15e, page +0x162; per tab pages +0x97c + tab * 0xc (0x1c bytes: title text +0, selected row +4, rows +0x10) and detail records +0x9d0 + tab * 0xc (0x40 bytes). Row (0x14 bytes): u16 flags (4 spacer, 0x40 done, 0x80 entry flag 1), title +4, detail record +8, book entry +0xc, icon +0x10. Act tabs: chapter text table 0x9de7e4 (9000 "Pfade des Schicksals", 9001 "Unter blutigen Bannern", 9002 "Die rastlosen Toten", 9003 "Zeitenwende"); bookmarks: 7 controls at +0x168, 0xbc apart. - The book's list and the quest scripts run on the same thread (logged). - Quest table (Gladiator, Silber): 28 main quests, 87 side quests in id blocks of 100 (1xx .. 25xx, 2 to 7 each; their accepted quests lie together on the map), about 120 other side quests (5xxx, 6xxx, 9xxx, 102xx, 15xxx), 352 generic ones (2600..3599) and bookkeeping entries (0, 99 "MP-Start", 5081..5089 "Region 1 befreit"). ## Quest book as one list (request 2026-10-25) "Show all quests, unknown ones as ???, so a player knows how many quests an area holds"; then: "get rid of the tabs ... a section for story, and then a section per region", the game's page turning kept. `mods/questbook.cpp`: the game's builder runs on a reordered copy of the book (story, then a section " (known/all)" with "???" rows, other side quests joined to the nearest area, "Unentdeckt" counts), all in the first tab; rows are mapped back to the real entries afterwards (ours to none, which clicks ignore). The bookmarks are hidden, pages titled "Quests", each page selects its first quest, a click on a heading keeps the selection. Counted: side quests whose name is unique in the table. Area names (measured 2026-10-25): an area is an id block (1xx .. 25xx); its name is the game's name of the region most of its known quests lie in, as the megamap names the hero's region: book cell +0x18/+0x1c (x 53.66563 = world units) -> `World_SectorAt` 0x634970 -> `Sector_Region` 0x622e30 -> `Region_NameKey` 0x425370 -> text (`game/engine/regions.h`). Several blocks lie in one named region (regions 1, 2, 3 are all `UI_REGION_SOUTHKERN`), so blocks with the same name make one section; blocks without a known position are "Gebiet N". "Erledigte anzeigen" (request 2026-10-25: "some kind of 'Show completed' checkmark. If unchecked, completed quests are not in the list (as well as completed regions)"): the list's first row, an own entry whose state draws the solved mark (100) when on and no icon (0) when off; its row carries the marker entry 0xfffffffe. A click toggles it (`sacredK.ini` `[questbook] ShowCompleted`, default 1) and calls `cUI_Book::Rebuild` 0x6b1670 (thiscall, no arguments; keeps tab and page if the page still exists, else ours clamps it). Off leaves out solved entries (main and side) and areas whose quests are all known and solved; failed quests stay (restartable). ## Quest NPCs after the quest (request 2026-10-25) Request: NPCs related to a quest are drawn white on the minimaps and turn towards the player; once their quest is finished they should be drawn blue and stop turning, like other NPCs. `src/mods/questnpc.cpp`. Measured: - The zoomed map view (0x6cc950, call at 0x6cec6b) draws a creature white when `cCreature::IsQuestNpc` (0x54fa30) is true: creature flags +0x200 bit 0x100 / 0x200 (escorted NPCs, set by the quest manager 0x46ae54..), or a dialog +0x245 in 1..59999 whose type (`cDialogManager::DialogType` 0x49a180, record +0x48 of the 0x50-byte records at manager 0xaaaf00 +0x755c, name at record +4) is below 50. - Some quests move their giver on to dialog 0 at the end (`DLG_15074_START` type 10 → `_OFFEN` 126 → `_ZIEL` 11 → 0); many leave it on the quest's dialog: `auftrag10`, `auftrag2411`, `AuftragEnde202`, `auftrag303` kept their dialog with the quest done (flags 3, book entry solved). - The turning, found with a hardware write watch (DR0) on the facing +0x70: the idle action (`cCreature::IdleAction` 0x544700, Think's action 0 handler) turns every creature with a dialog +0x245 in 1..99999 towards the local hero each Think step (0x546334..0x5463d5, `cCreature::SetDirection` 0x44df60 → 0x44ddc0), unless its type is of kind 4 in the type table (`TypeTable_IsKind4` 0x4265c0, call at 0x546357) or object flag +0x14 bit 2 is set. So every NPC with a dialog turns, not only quest NPCs. - Script labels (vector 0xaa9688..0xaa968c, 0x54 bytes each; quest scripts and dialog answers are indices into it, run by `Script_RunLabel` 0x46bd80): name +0, the quest the label belongs to +0x48 (-1 none; the label only runs once that quest is triggered, 0x46bf11). Each dialog has a label `Dialog:` (logged 2026-10-25: `Dialog:rg1_goodie1_auftrag` 9004 Vermisstes Kind, the Aufgeregter Bauer; `Dialog:auftrag_wueste` 9000; `Dialog:auftrag11a1` 1101; `Dialog:DLG_15003_ZIEL` 15003; `Dialog:geschlossen101` none). The quest of a dialog is its label's quest (`game/engine/quests.h`, script labels), looked up by the dialog record's name; cached per dialog. A first version read the quest id from the numbers in the dialog's name; that missed dialogs without one (the Aufgeregter Bauer's `rg1_goodie1_auftrag`, user report 2026-10-25) and misread others (`auftrag11a1`). When that quest exists, is done (flag 0x2) and not in progress (0x4): - the hook on `IsQuestNpc` returns false (blue on the minimap); - the type check call at 0x546357 is redirected to a thunk that also answers true, so the idle action skips the turn. Dialogs without a quest (healers, closed shops: `geschlossen101`, `heilen_global`) stay as the game has them. ## Quest scripts (measured 2026-10-26) - Code: `binNetScriptFunkCode.bin` (`binAddonNetScript` differs only past 0x9a653), open as the quest manager's FILE* +4 (`0xaaaf04`). Instructions {u16 opcode, u16 size with the header, args}; args tagged: 0x01 string, 0x0b int32, 0x09 dialog, 0x05 label (e.g. a death label), ... Jump table of the executor `0x475970` at `0x4787b0` (opcode * 4). - Labels: the script label table (`0xaa9688`, 0x54 bytes; from `Vectoren.bin`): name, +0x40 code offset, +0x44 code size, +0x48 quest. `Script_RunLabel` `0x46bd80` (cdecl (label, object, flag)) reads the code and runs it with `0x475970` (quest manager thiscall (code, &offset, quest, object, flag, 0), ret 0x18; any memory buffer with offset 0 works). - Opcodes seen: 1 spawn ("Res: