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:
1 parent
471b8b13d5
commit
7ec0fa2b8e
3 files changed
+83
-11
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
+54
-11
@@ -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 script id of an instruction's first argument "Res:<number>", or 0.
|
||||||
|
uint32_t InstructionScriptId(size_t at, uint16_t size) {
|
||||||
|
static const char kRes[] = "res:";
|
||||||
|
if (size <= 9 || g_code[at + 4] != game::kScriptArgString ||
|
||||||
|
_strnicmp(reinterpret_cast<const char*>(&g_code[at + 5]), kRes, 4) != 0 ||
|
||||||
|
!memchr(&g_code[at + 9], 0, size - 9)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char* numberEnd = nullptr;
|
||||||
|
const unsigned long scriptId = std::strtoul(reinterpret_cast<const char*>(&g_code[at + 9]), &numberEnd, 10);
|
||||||
|
return *numberEnd == 0 ? static_cast<uint32_t>(scriptId) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// The spawn instructions in the code: "Res:<number>" with a dialog.
|
// The spawn instructions in the code: "Res:<number>" with a dialog.
|
||||||
void ScanSpawns(const std::map<std::string, int32_t>& dialogs) {
|
void ScanSpawns(const std::map<std::string, int32_t>& dialogs) {
|
||||||
static const char kRes[] = "res:";
|
|
||||||
for (size_t at = 0; at + 9 < g_code.size(); ++at) {
|
for (size_t at = 0; at + 9 < g_code.size(); ++at) {
|
||||||
if (U16(at) != game::kScriptSpawn || g_code[at + 4] != game::kScriptArgString ||
|
if (U16(at) != game::kScriptSpawn) {
|
||||||
_strnicmp(reinterpret_cast<const char*>(&g_code[at + 5]), kRes, 4) != 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const uint16_t size = U16(at + 2);
|
const uint16_t size = U16(at + 2);
|
||||||
if (size <= 9 || size > kMaxInstruction || at + size > g_code.size()) {
|
if (size > kMaxInstruction || at + size > g_code.size()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
char* numberEnd = nullptr;
|
const uint32_t scriptId = InstructionScriptId(at, size);
|
||||||
const unsigned long scriptId = std::strtoul(reinterpret_cast<const char*>(&g_code[at + 9]), &numberEnd, 10);
|
if (scriptId == 0) {
|
||||||
if (scriptId == 0 || *numberEnd != 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));
|
||||||
|
|||||||
Reference in new issue
Block a user