define most of the remining fields in BB extended quest header

This commit is contained in:
Martin Michelsen
2025-11-28 14:36:13 -08:00
parent 7ec267a7c0
commit 435ac82c18
4 changed files with 37 additions and 21 deletions
+9
View File
@@ -115,6 +115,9 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->enemy_exp_overrides != other.enemy_exp_overrides) {
throw runtime_error("quest version has different enemy EXP overrides");
}
if (this->solo_unlock_flags != other.solo_unlock_flags) {
throw runtime_error(std::format("quest version has a different set of solo unlock flags"));
}
if (!this->create_item_mask_entries.empty() &&
!other.create_item_mask_entries.empty() &&
this->create_item_mask_entries != other.create_item_mask_entries) {
@@ -237,6 +240,11 @@ phosg::JSON QuestMetadata::json() const {
create_item_mask_entries_json.emplace_back(item.str());
}
auto solo_unlock_flags_json = phosg::JSON::list();
for (uint16_t flag : this->solo_unlock_flags) {
solo_unlock_flags_json.emplace_back(flag);
}
return phosg::JSON::dict({
{"CategoryID", this->category_id},
{"QuestNumber", this->quest_number},
@@ -259,6 +267,7 @@ phosg::JSON QuestMetadata::json() const {
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
{"EnemyEXPOverrides", std::move(enemy_exp_overrides_json)},
{"CreateItemMasks", std::move(create_item_mask_entries_json)},
{"SoloUnlockFlags", std::move(solo_unlock_flags_json)},
});
}
+2 -1
View File
@@ -87,6 +87,7 @@ struct QuestMetadata {
};
std::vector<FloorAssignment> bb_map_designate_opcodes;
std::vector<CreateItemMask> create_item_mask_entries;
std::vector<uint16_t> solo_unlock_flags;
// Unknown header fields. These are not used by the client, so they are not required to match across quest versions;
// however, we still parse them in case we later discover that they had some server-side meaning.
@@ -95,9 +96,9 @@ struct QuestMetadata {
uint8_t header_unknown_a3 = 0; // DCv1 - V3
uint8_t header_unknown_a4 = 0; // BB only
uint16_t header_unknown_a6 = 0; // BB only
uint32_t header_unknown_a5 = 0; // BB only
int16_t header_episode = -1; // -1 = unspecified; BB only; newserv uses script analysis instead
int16_t header_language = -1; // -1 = unspecified; DCv1 and later; newserv uses the filename instead
std::shared_ptr<parray<uint8_t, 0x14>> header_unknown_a5; // BB only; null for non-BB quests
// Fields that may be different across quest versions (and are only used on VersionedQuest, not Quest)
std::string name;
+22 -19
View File
@@ -3058,6 +3058,9 @@ std::string disassemble_quest_script(
if (meta.joinable) {
lines.emplace_back(".joinable");
}
for (uint16_t flag : meta.solo_unlock_flags) {
lines.emplace_back(std::format(".solo_unlock_flag 0x{:04X}", flag));
}
for (const auto& mask : meta.create_item_mask_entries) {
lines.emplace_back(std::format(".allow_create_item {}", mask.str()));
}
@@ -3069,10 +3072,7 @@ std::string disassemble_quest_script(
}
if (is_v4(version)) {
lines.emplace_back(std::format(".header_unknown_a4 0x{:02X}", meta.header_unknown_a4));
if (meta.header_unknown_a5) {
auto formatted = phosg::format_data_string(meta.header_unknown_a5->data(), meta.header_unknown_a5->size());
lines.emplace_back(std::format(".header_unknown_a5 {}", formatted));
}
lines.emplace_back(std::format(".header_unknown_a5 0x{:08X}", meta.header_unknown_a5));
lines.emplace_back(std::format(".header_unknown_a6 0x{:04X}", meta.header_unknown_a6));
}
lines.emplace_back();
@@ -4290,7 +4290,11 @@ AssembledQuestScript assemble_quest_script(
throw std::runtime_error("too many .allow_create_item directives; at most 64 are allowed");
}
ret.meta.create_item_mask_entries.emplace_back(line.text.substr(19));
} else if (line.text.starts_with(".solo_unlock_flag ")) {
if (ret.meta.solo_unlock_flags.size() >= 8) {
throw std::runtime_error("too many .solo_unlock_flag directives; at most 8 are allowed");
}
ret.meta.solo_unlock_flags.emplace_back(stoul(line.text.substr(18), nullptr, 0));
} else if (line.text.starts_with(".quest_num ")) {
ret.meta.quest_number = stoul(line.text.substr(11), nullptr, 0);
} else if (line.text.starts_with(".language ")) {
@@ -4317,17 +4321,10 @@ AssembledQuestScript assemble_quest_script(
ret.meta.header_unknown_a3 = stoul(line.text.substr(19), nullptr, 0);
} else if (line.text.starts_with(".header_unknown_a4 ")) {
ret.meta.header_unknown_a4 = stoul(line.text.substr(19), nullptr, 0);
} else if (line.text.starts_with(".header_unknown_a5 ")) {
ret.meta.header_unknown_a5 = stoul(line.text.substr(19), nullptr, 0);
} else if (line.text.starts_with(".header_unknown_a6 ")) {
ret.meta.header_unknown_a6 = stoul(line.text.substr(19), nullptr, 0);
} else if (line.text.starts_with(".header_unknown_a5 ")) {
std::string data = phosg::parse_data_string(line.text.substr(19));
if (data.size() != 0x14) {
throw std::runtime_error(".header_unknown_a5 directive must specify 0x14 bytes of data");
}
ret.meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x14>>();
for (size_t z = 0; z < 0x14; z++) {
ret.meta.header_unknown_a5->at(z) = static_cast<uint8_t>(data[z]);
}
}
}
});
@@ -4981,10 +4978,10 @@ AssembledQuestScript assemble_quest_script(
header.name.encode(ret.meta.name, ret.meta.language);
header.short_description.encode(ret.meta.short_description, ret.meta.language);
header.long_description.encode(ret.meta.long_description, ret.meta.language);
if (ret.meta.header_unknown_a5) {
header.unknown_a5 = *ret.meta.header_unknown_a5;
} else {
header.unknown_a5.clear(0);
header.unknown_a5 = ret.meta.header_unknown_a5;
header.solo_unlock_flags.clear(0xFFFF);
for (size_t z = 0; z < ret.meta.solo_unlock_flags.size(); z++) {
header.solo_unlock_flags[z] = ret.meta.solo_unlock_flags[z];
}
phosg::StringReader code_r(code_w.str());
for (size_t z = 0; z < bb_map_designate_args_offsets.size(); z++) {
@@ -5142,7 +5139,13 @@ void populate_quest_metadata_from_script(
if ((header.text_offset >= sizeof(PSOQuestHeaderBB)) && (header.label_table_offset >= sizeof(PSOQuestHeaderBB))) {
r.go(0);
const auto& header = r.get<PSOQuestHeaderBB>();
meta.header_unknown_a5 = std::make_shared<parray<uint8_t, 0x14>>(header.unknown_a5);
meta.header_unknown_a5 = header.unknown_a5;
for (size_t z = 0; z < header.solo_unlock_flags.size(); z++) {
uint16_t flag = header.solo_unlock_flags[z];
if (flag != 0xFFFF) {
meta.solo_unlock_flags.emplace_back(flag);
}
}
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
const auto& item = header.create_item_mask_entries[z];
if (!item.is_valid()) {
+4 -1
View File
@@ -95,6 +95,8 @@ struct CreateItemMaskEntry {
operator QuestMetadata::CreateItemMask() const;
} __packed_ws__(CreateItemMaskEntry, 0x38);
// Some quest authoring tools don't generate the full quest header, hence the
// split structure here.
struct PSOQuestHeaderBBBase {
/* 0000 */ le_uint32_t text_offset = 0;
/* 0004 */ le_uint32_t label_table_offset = 0;
@@ -125,7 +127,8 @@ struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
parray<uint8_t, 3> unused = 0xFF;
} __packed_ws__(FloorAssignment, 8);
/* 0398 */ parray<uint8_t, 0x14> unknown_a5;
/* 0398 */ le_uint32_t unknown_a5;
/* 039C */ parray<le_uint16_t, 8> solo_unlock_flags;
/* 03AC */ parray<FloorAssignment, 0x10> floor_assignments;
/* 042C */ parray<CreateItemMaskEntry, 0x40> create_item_mask_entries;
/* 122C */