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 <noreply@anthropic.com>
This commit is contained in:
1 parent
e539cab604
commit
55e48dd302
6 files changed
+126
-20
No files matched your search
@@ -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
|
||||
|
||||
+38
-3
@@ -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:<n>" 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
|
||||
|
||||
@@ -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:<script id>", type,
|
||||
// position, 0x09 + its dialog, ...; e.g. Region15Init), 0x44 sets a script
|
||||
// variable ({0x01 name, 0x0b value}).
|
||||
// position, 0x09 + its dialog, ...; e.g. Region15Init). Script variables are
|
||||
// bit fields ({0x01 name, 0x0b n}; measured 2026-10-27): 0x43 sets the value,
|
||||
// 0x44 sets bit n (0x49bb30), 0x45 clears bit n (0x49c450), 0x4b adds n
|
||||
// (0x49cc20); conditions 0x49 / 0x4a test bit n set / clear, 0x48 the value
|
||||
// (0x498aa0; a missing variable is false).
|
||||
constexpr uint16_t kScriptSpawn = 0x01;
|
||||
constexpr uint16_t kScriptSetVariable = 0x44;
|
||||
constexpr uint16_t kScriptSetVarValue = 0x43;
|
||||
constexpr uint16_t kScriptSetVarBit = 0x44;
|
||||
constexpr uint16_t kScriptClearVarBit = 0x45;
|
||||
constexpr uint16_t kScriptAddToVar = 0x4b;
|
||||
inline bool IsScriptVariableWrite(uint16_t opcode) {
|
||||
return opcode == kScriptSetVarValue || opcode == kScriptSetVarBit || opcode == kScriptClearVarBit ||
|
||||
opcode == kScriptAddToVar;
|
||||
}
|
||||
constexpr uint8_t kScriptArgString = 0x01;
|
||||
constexpr uint8_t kScriptArgInt = 0x0b;
|
||||
constexpr uint8_t kScriptArgDialog = 0x09;
|
||||
@@ -138,6 +148,25 @@ inline uint8_t* QuestManager() { return reinterpret_cast<uint8_t*>(0x00aaaf00);
|
||||
constexpr size_t kBookBegin = 0x424;
|
||||
constexpr size_t kBookEnd = 0x428;
|
||||
constexpr size_t kBookEntrySize = 0x174;
|
||||
|
||||
// The script variables: a vector at quest manager +0x7550 / +0x7554 of 0x24-byte
|
||||
// entries {name[0x20], value} (read by the conditions, 0x498aa0 case 0x49).
|
||||
constexpr size_t kVariablesBegin = 0x7550;
|
||||
constexpr size_t kVariablesEnd = 0x7554;
|
||||
constexpr size_t kVariableSize = 0x24;
|
||||
constexpr size_t kVariableValue = 0x20;
|
||||
|
||||
// The value slot of script variable `name`, nullptr if it does not exist yet.
|
||||
inline int32_t* ScriptVariable(const char* name) {
|
||||
uint8_t* begin = *reinterpret_cast<uint8_t**>(QuestManager() + kVariablesBegin);
|
||||
uint8_t* end = *reinterpret_cast<uint8_t**>(QuestManager() + kVariablesEnd);
|
||||
for (uint8_t* entry = begin; entry && entry < end; entry += kVariableSize) {
|
||||
if (strncmp(reinterpret_cast<const char*>(entry), name, kVariableValue) == 0) {
|
||||
return reinterpret_cast<int32_t*>(entry + kVariableValue);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
constexpr size_t kBookCapacity = 0x42c;
|
||||
constexpr size_t kBookKind = 0x00; // 3 main quest (ids 1..99), 4 side quest (0x46c450)
|
||||
constexpr size_t kBookState = 0x04;
|
||||
|
||||
+14
-14
@@ -5,7 +5,7 @@
|
||||
// completed or correctly running").
|
||||
//
|
||||
// Measured: the quest's script (FunkCode.bin) gives the giver a death label
|
||||
// when she starts following ("ex1501": quest variable "1501" = 1, then the
|
||||
// when she starts following ("ex1501": bit 1 of quest variable "1501", then the
|
||||
// poachers' death label "tot1501", which ends the quest without a reward once
|
||||
// all poachers are dead; until then it stays in progress with no giver). The
|
||||
// body is removed. The giver is placed by her region's start script
|
||||
@@ -141,7 +141,7 @@ void ScanVariables() {
|
||||
label += game::kScriptLabelSize) {
|
||||
const int32_t quest = game::ScriptLabelQuest(label);
|
||||
ForEachInstruction(label, [&](size_t at, uint16_t opcode, uint16_t size) {
|
||||
if (opcode == game::kScriptSetVariable && g_code[at + 4] == game::kScriptArgString &&
|
||||
if (game::IsScriptVariableWrite(opcode) && g_code[at + 4] == game::kScriptArgString &&
|
||||
memchr(&g_code[at + 5], 0, size - 5)) {
|
||||
setters[reinterpret_cast<const char*>(&g_code[at + 5])].insert(quest);
|
||||
}
|
||||
@@ -218,20 +218,20 @@ void* LivingGiver(const Giver& giver) {
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// The variables are bit fields; the scripts' own writes only set or clear bits
|
||||
// (a quest's progress builds up bit by bit), so the value is cleared directly.
|
||||
void ResetVariables(int32_t questId) {
|
||||
for (const std::string& name : g_variables[questId]) {
|
||||
std::vector<uint8_t> code(kInstructionHeader);
|
||||
code.push_back(game::kScriptArgString);
|
||||
code.insert(code.end(), name.begin(), name.end());
|
||||
code.push_back(0);
|
||||
code.push_back(game::kScriptArgInt);
|
||||
code.insert(code.end(), 4, 0);
|
||||
code[0] = static_cast<uint8_t>(game::kScriptSetVariable);
|
||||
code[1] = 0;
|
||||
code[2] = static_cast<uint8_t>(code.size());
|
||||
code[3] = static_cast<uint8_t>(code.size() >> 8);
|
||||
game::RunScriptInstruction(code.data());
|
||||
re::Log("questgiver: quest %d variable '%s' = 0", questId, name.c_str());
|
||||
int32_t* value = game::ScriptVariable(name.c_str());
|
||||
// The variable named after the quest keeps bit 0: "available", which the
|
||||
// giver's offer tests (auftrag402: IsVarBit 402 0; measured 2026-10-28:
|
||||
// all 87 quests not yet accepted had value 1).
|
||||
const int32_t fresh = name == std::to_string(questId) ? 1 : 0;
|
||||
if (!value || *value == fresh) {
|
||||
continue;
|
||||
}
|
||||
re::Log("questgiver: quest %d variable '%s' %d -> %d", questId, name.c_str(), *value, fresh);
|
||||
*value = fresh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Main quest items blocked by an entry without a script id (request of
|
||||
// 2026-10-27: "I cannot finish the Quest Windgeflüster").
|
||||
//
|
||||
// Measured: picking at the Goldbeerenbusch runs take402, which gives the hero
|
||||
// the berries (item type 6029) and sets the quest's variable; the giver's
|
||||
// dialog then asks for type 6029 in the hero's main quest items. The game
|
||||
// logged "addMainQuestItem() Already has mainQuestItem(6029)" and the berries
|
||||
// never arrived: the dwarf's list held {4609, 0}.
|
||||
//
|
||||
// cInventoryEntry::AddMainQuestItem (0x5d8240, thiscall (type, script id,
|
||||
// notify), list of {type, script id} at +0/+4) refuses a new item when an entry
|
||||
// has the same type or a script id of 0 (0x5d83c6: cmp [edx+4], ebx (0)).
|
||||
// Scripts that hand out an item without a "Res:" id add it with script id 0
|
||||
// (0x486bcb..0x486bd8), so after such an item every later main quest item was
|
||||
// refused. The mod drops the script id test; the same type still blocks.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "re/log.h"
|
||||
#include "re/patch.h"
|
||||
|
||||
namespace mods::questitems {
|
||||
namespace {
|
||||
|
||||
// AddMainQuestItem's duplicate test: `cmp [edx+4], ebx; je 0x5d8418` (ebx 0).
|
||||
constexpr uint32_t kScriptIdTest = 0x005d83c6;
|
||||
|
||||
void Startup() {
|
||||
const uint8_t from[5] = {0x39, 0x5a, 0x04, 0x74, 0x4d};
|
||||
const uint8_t to[5] = {0x90, 0x90, 0x90, 0x90, 0x90};
|
||||
const bool ok = re::ReplaceCode(kScriptIdTest, from, to, sizeof(to));
|
||||
re::Log("questitems: main quest items without a script id no longer block others%s", ok ? "" : " (patch FAILED)");
|
||||
}
|
||||
RE_STARTUP(QuestItemsStartup, Startup);
|
||||
|
||||
} // namespace
|
||||
} // namespace mods::questitems
|
||||
@@ -444,3 +444,7 @@
|
||||
0x007b64c0 cParticleSystem_grenadeimpact::Update # vf08 (dt), ret 4; phase +0x20a9
|
||||
0x007613e0 cParticleSystem::DrawParticles # colour table row (+0x3d) * 256 + life (+0x2c)
|
||||
0x00763ea0 cParticleSystem::UpdateParticles # thiscall (dt, emitter rows, mode), particles 0x40 bytes at +0x68
|
||||
0x005d8240 cInventoryEntry::AddMainQuestItem # thiscall (type, script id, notify): {type, script id} pairs; refused if same type (script id 0 test patched out)
|
||||
0x0049bb30 QuestManager_SetVarBit # script op 0x44: variable |= 1 << n
|
||||
0x0049c450 QuestManager_ClearVarBit # script op 0x45: variable &= ~(1 << n)
|
||||
0x0049cc20 QuestManager_AddToVar # script op 0x4b: variable += n
|
||||
Reference in new issue
Block a user