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
@@ -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
|
||||
Reference in new issue
Block a user