"Familienbande" (702) turns its giver hostile and wants him killed; the giver death rule restarted it instead. A giver hostile to the hero at death now runs the game's label, and the load repair skips quests whose script changes their giver's creature type (opcode 0x4f). Deferred notes: frame drops while walking, Eingraben clearing slows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
304 lines
18 KiB
Markdown
304 lines
18 KiB
Markdown
# 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.
|
|
|
|
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
|
|
"<region> (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:<dialog name>` (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:<script id>", type, position, 0x09 dialog,
|
|
0x05 death label), 3 NPC mode (0x09 dialog, 0x0a follow + death label),
|
|
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, 0x43 / 0x44 / 0x45 / 0x4b variables (see below), 0x48 move NPC,
|
|
0x56 dialog state.
|
|
- Script NPC names: "Res:<n>" is a script id (object +0x3c, `0x44a390`); the
|
|
quest manager maps it to the object (`0x464ec0`, map +0x7668); a spawn sets
|
|
the entry.
|
|
- Example "Jagd auf die Wilderer" (1501): `Region15Init` spawns the elf
|
|
(Res:17013, type 134, dialog auftrag1501); accepting (`trigger1501`: 0x14)
|
|
runs `QIS_OnEnter1501`: poachers with death label `tot1501`, the elf follows
|
|
with death label `ex1501` (variable "1501" = 1, then `tot1501`); `tot1501`
|
|
once all poachers are dead: variable 0 -> back to the elf, 1 -> quest ended
|
|
without the reward. "Hetzjagd" (1502): the daughter (Res:17015, the giver)
|
|
is spawned only when the hero walks into trigger area `hetz1_1502`, with
|
|
three cultists; her father (Res:17016) only asks for her.
|
|
|
|
## Escorts on the follower list (request 2026-10-26)
|
|
|
|
"Furchtbares Verbrechen" (9008) did not go on at its target point: the
|
|
script's test there ("@npc", `0x498aa0` case 0x40) looks the NPC up in the
|
|
hero's follower list (+0x39c, 9-byte entries), which was empty after a load,
|
|
while the NPC still had its leader (+0x251) and follow bit (+0x1f4: 4 for the
|
|
follow order, 0x100 for script escorts). `mods/followers.cpp` adds such NPCs
|
|
back with `cCreature::AddFollower` `0x54b070` once after loading. Town NPCs
|
|
that run after the hero for a few seconds (status 0x404) are left out.
|
|
Confirmed 2026-10-26.
|
|
|
|
## Quest givers come back (request 2026-10-26)
|
|
|
|
"Change the death script to spawn her back to her starting point", and "on
|
|
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:
|
|
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.
|
|
|
|
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).
|
|
|
|
Givers killed as enemies (2026-10-30): "Familienbande" (702, book title
|
|
"Mädchen bei Ausritt verschwunden") was restarted twice when the hero killed
|
|
its giver. The quest wants that: back at the giver with the girl
|
|
(Res:17060), answer `change702` changes the creature type of the giver
|
|
Res:17061 (668 -> 262) and two relatives (opcode 0x4f {"Res:<id>", 0x02
|
|
type}), they attack, and their death label `tot702` sends the hero back to the
|
|
girl (`ziel702`, `exit702` ends the quest). Now a giver hostile to the hero
|
|
at death runs the label as the game has it, and the load repair skips quests
|
|
whose labels change their giver's type (0x4f on the giver; 2 of the 85 known
|
|
givers, 702 among them). Confirmed in game 2026-10-30.
|
|
|
|
## 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
|
|
longer following me" (Borf). Measured in the log: the husband (Res:17584,
|
|
type 86; spawned in `startcode.bin` with death label `drop2412`) fell behind,
|
|
the game's own catch-up teleport moved him once (type 86 line), then the hero
|
|
walked on, the sectors behind were switched off, and the game removed him
|
|
and ran `drop2412` (quest variable 5, a token item res:17585 dropped where he
|
|
was; picking it up runs `take2412`, the "husband lost" branch). His object id
|
|
was reused right after: removed, not dead. The teleport potion came 50 s
|
|
later and was not involved; portals and the potion take followers along.
|
|
|
|
`mods/followers.cpp`: every 500 ms each follower of the local hero more than
|
|
40 cells away (sectors are switched off at 65 and more) on the same level is
|
|
teleported to the hero with `cCreature::Teleport` 0x54d840 (thiscall (cell x,
|
|
cell y, 0, 1), ret 0x10). Borf's husband was respawned once by a temporary
|
|
repair (his spawn instruction, variable 1, token removed, `follow2412`); the
|
|
quest then finished with him alive.
|