Quest givers: givers killed as enemies keep their death script

"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>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-30 18:57:43 +01:00
1 parent 471b8b13d5
commit 7ec0fa2b8e
3 files changed
+87 -15

No files matched your search

+11
View File
@@ -238,6 +238,17 @@ 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 took it for running and reset 16 available quests and spawned their NPCs on
Borf (repaired once, then saved). 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) ## Quest variables are bit fields (measured 2026-10-28)
Found while repairing "Kräuter für Kräuterhexe" (402; the user knows it as Found while repairing "Kräuter für Kräuterhexe" (402; the user knows it as
+18
View File
@@ -266,3 +266,21 @@ Possibly related (2026-10-25, once): right after a game start the main menu
texts were drawn as solid gold blocks (the gold text texture without the texts were drawn as solid gold blocks (the gold text texture without the
font's letter mask). Gone after a restart; Alt+Tab does not cause it. The font's letter mask). Gone after a restart; Alt+Tab does not cause it. The
log showed nothing unusual (all hooks and patches applied). log showed nothing unusual (all hooks and patches applied).
## Frame drops while walking (2026-10-27, deferred 2026-10-30)
Reported 2026-10-27. Measured 2026-10-29 (about 54 min, temporary timing in
the Flip, engine tick and map border, since removed): 549 frames over 40 ms,
mostly 0-1 per 10 s, worst 833 ms. Present (Flip) was slow only once; the
time goes to the game's own engine tick (227 ticks of 18-70 ms). Our tick
callbacks were slow only at the 10-min autosave. The map border is drawn only
while the map is open, so it is not the cause. Next step if resumed: time the
parts of the game tick (sector streaming while walking is the main suspect).
Deferred by the user 2026-10-30.
## Eingraben clears slows, roots and stuns (deferred 2026-10-30)
Step 3 of Eingraben (docs/dwarf-arts.md): cast clears slowing, rooting and
stunning effects on the hero. The effect kinds are unknown; plan: temporary
log of the effects added to the hero while the user gets slowed or frozen,
then remove those kinds on cast. Deferred by the user 2026-10-30.
+58 -15
View File
@@ -50,6 +50,7 @@ constexpr DWORD kBodyWaitMs = 30000; // spawn anyway if the body is still there
constexpr uint16_t kScriptStartQuest = 0x14; // {0x0b quest id} constexpr uint16_t kScriptStartQuest = 0x14; // {0x0b quest id}
constexpr size_t kStartQuestSize = 9; constexpr size_t kStartQuestSize = 9;
constexpr uint16_t kScriptAnswer = 0x3c; // dialog answer {0x01 text, 0x01 label} constexpr uint16_t kScriptAnswer = 0x3c; // dialog answer {0x01 text, 0x01 label}
constexpr uint16_t kScriptChangeType = 0x4f; // {0x01 "Res:<id>", 0x02 creature type}
struct Giver { struct Giver {
uint32_t scriptId = 0; uint32_t scriptId = 0;
@@ -67,6 +68,7 @@ bool g_scanned = false;
std::vector<uint8_t> g_code; // FunkCode.bin std::vector<uint8_t> g_code; // FunkCode.bin
std::map<int32_t, Giver> g_givers; // quest id -> giver std::map<int32_t, Giver> g_givers; // quest id -> giver
std::map<int32_t, std::vector<std::string>> g_variables; // quest id -> variables only it sets std::map<int32_t, std::vector<std::string>> g_variables; // quest id -> variables only it sets
std::set<int32_t> g_giverTurns; // quests that turn their giver into an enemy
std::vector<Pending> g_pending; std::vector<Pending> g_pending;
bool g_repairAfterLoad = false; bool g_repairAfterLoad = false;
@@ -99,21 +101,31 @@ void ForEachInstruction(const uint8_t* label, Fn fn) {
} }
} }
// The spawn instructions in the code: "Res:<number>" with a dialog. // The script id of an instruction's first argument "Res:<number>", or 0.
void ScanSpawns(const std::map<std::string, int32_t>& dialogs) { uint32_t InstructionScriptId(size_t at, uint16_t size) {
static const char kRes[] = "res:"; static const char kRes[] = "res:";
for (size_t at = 0; at + 9 < g_code.size(); ++at) { if (size <= 9 || g_code[at + 4] != game::kScriptArgString ||
if (U16(at) != game::kScriptSpawn || g_code[at + 4] != game::kScriptArgString || _strnicmp(reinterpret_cast<const char*>(&g_code[at + 5]), kRes, 4) != 0 ||
_strnicmp(reinterpret_cast<const char*>(&g_code[at + 5]), kRes, 4) != 0) { !memchr(&g_code[at + 9], 0, size - 9)) {
continue; return 0;
}
const uint16_t size = U16(at + 2);
if (size <= 9 || size > kMaxInstruction || at + size > g_code.size()) {
continue;
} }
char* numberEnd = nullptr; char* numberEnd = nullptr;
const unsigned long scriptId = std::strtoul(reinterpret_cast<const char*>(&g_code[at + 9]), &numberEnd, 10); const unsigned long scriptId = std::strtoul(reinterpret_cast<const char*>(&g_code[at + 9]), &numberEnd, 10);
if (scriptId == 0 || *numberEnd != 0) { return *numberEnd == 0 ? static_cast<uint32_t>(scriptId) : 0;
}
// The spawn instructions in the code: "Res:<number>" with a dialog.
void ScanSpawns(const std::map<std::string, int32_t>& dialogs) {
for (size_t at = 0; at + 9 < g_code.size(); ++at) {
if (U16(at) != game::kScriptSpawn) {
continue;
}
const uint16_t size = U16(at + 2);
if (size > kMaxInstruction || at + size > g_code.size()) {
continue;
}
const uint32_t scriptId = InstructionScriptId(at, size);
if (scriptId == 0) {
continue; continue;
} }
for (size_t tag = at + 9; tag + 1 < at + size; ++tag) { for (size_t tag = at + 9; tag + 1 < at + size; ++tag) {
@@ -126,13 +138,31 @@ void ScanSpawns(const std::map<std::string, int32_t>& dialogs) {
} }
const auto dialog = dialogs.find(name); const auto dialog = dialogs.find(name);
if (dialog != dialogs.end() && !g_givers.count(dialog->second)) { if (dialog != dialogs.end() && !g_givers.count(dialog->second)) {
g_givers[dialog->second] = {static_cast<uint32_t>(scriptId), g_givers[dialog->second] = {scriptId,
std::vector<uint8_t>(g_code.begin() + at, g_code.begin() + at + size)}; std::vector<uint8_t>(g_code.begin() + at, g_code.begin() + at + size)};
} }
} }
} }
} }
// The quests that change their giver's creature type (opcode 0x4f): as
// "Familienbande" (702, change702: Res:17061 type 668 -> 262), they turn the
// giver into an enemy the hero is meant to kill.
void ScanGiverTurns() {
for (const uint8_t* label = game::ScriptLabelsBegin(); label && label < game::ScriptLabelsEnd();
label += game::kScriptLabelSize) {
const auto giver = g_givers.find(game::ScriptLabelQuest(label));
if (giver == g_givers.end()) {
continue;
}
ForEachInstruction(label, [&](size_t at, uint16_t opcode, uint16_t size) {
if (opcode == kScriptChangeType && InstructionScriptId(at, size) == giver->second.scriptId) {
g_giverTurns.insert(giver->first);
}
});
}
}
// The variables each quest's labels set; a variable set by several quests // The variables each quest's labels set; a variable set by several quests
// (or by labels of none) is not any one quest's. // (or by labels of none) is not any one quest's.
void ScanVariables() { void ScanVariables() {
@@ -206,7 +236,9 @@ void Scan() {
} }
ScanSpawns(dialogs); ScanSpawns(dialogs);
ScanVariables(); ScanVariables();
re::Log("questgiver: %u quest givers in %u bytes of script code", static_cast<unsigned>(g_givers.size()), ScanGiverTurns();
re::Log("questgiver: %u quest givers (%u turn into enemies) in %u bytes of script code",
static_cast<unsigned>(g_givers.size()), static_cast<unsigned>(g_giverTurns.size()),
static_cast<unsigned>(g_code.size())); static_cast<unsigned>(g_code.size()));
} }
@@ -265,10 +297,12 @@ bool IsRunning(int index) {
return (flags & game::kQuestInProgress) && !(flags & game::kQuestDone); return (flags & game::kQuestInProgress) && !(flags & game::kQuestDone);
} }
// Quests whose giver turns into an enemy are left alone: a missing giver may
// be the one the hero killed as the quest asks.
void RepairAfterLoad() { void RepairAfterLoad() {
for (const auto& [id, giver] : g_givers) { for (const auto& [id, giver] : g_givers) {
const int index = game::QuestIndex(id); const int index = game::QuestIndex(id);
if (index < 0 || !IsRunning(index) || LivingGiver(giver)) { if (index < 0 || !IsRunning(index) || g_giverTurns.count(id) || LivingGiver(giver)) {
continue; continue;
} }
re::Log("questgiver: quest %d '%s' in progress without its giver Res:%u", id, re::Log("questgiver: quest %d '%s' in progress without its giver Res:%u", id,
@@ -311,7 +345,10 @@ struct Registration {
// Script_RunLabel (0x46bd80, cdecl (label, object, flag), plain ret): a label // Script_RunLabel (0x46bd80, cdecl (label, object, flag), plain ret): a label
// of a running quest run for its dead giver (her death label) is skipped and // of a running quest run for its dead giver (her death label) is skipped and
// the quest restarted. // the quest restarted. Not when the giver died as an enemy: some quests turn
// the giver hostile and their death label goes on ("Familienbande", 702:
// answer change702 turns the giver Res:17061 and two relatives against the
// hero, their death label tot702 sends him back to the girl Res:17060).
uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag); uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag);
RE_HOOK(0x0046bd80, Script_RunLabel); RE_HOOK(0x0046bd80, Script_RunLabel);
uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag) { uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag) {
@@ -324,6 +361,12 @@ uint32_t __cdecl Script_RunLabel(uint32_t label, uint8_t* object, int flag) {
Scan(); Scan();
const auto giver = g_givers.find(questId); const auto giver = g_givers.find(questId);
if (giver != g_givers.end() && game::ObjectScriptId(creature) == giver->second.scriptId) { if (giver != g_givers.end() && game::ObjectScriptId(creature) == giver->second.scriptId) {
if (game::IsHostileTo(creature, game::GetPlayerHero())) {
re::Log("questgiver: giver Res:%u of quest %d died as an enemy; '%s' runs",
giver->second.scriptId, questId,
game::ScriptLabelName(labels + label * game::kScriptLabelSize));
return Script_RunLabel_original(label, object, flag);
}
re::Log("questgiver: giver Res:%u of quest %d died; '%s' skipped, quest restarts", re::Log("questgiver: giver Res:%u of quest %d died; '%s' skipped, quest restarts",
giver->second.scriptId, questId, giver->second.scriptId, questId,
game::ScriptLabelName(labels + label * game::kScriptLabelSize)); game::ScriptLabelName(labels + label * game::kScriptLabelSize));