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:
khallaandClaude Opus 5.5 committed 2026-10-28 00:07:06 +01:00
1 parent e539cab604
commit 55e48dd302
6 files changed
+126 -20

No files matched your search

+32 -3
View File
@@ -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
View File
@@ -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;
}
}
+37
View File
@@ -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