diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc index 2a1e3d0d..d4021bf9 100644 --- a/src/QuestMetadata.cc +++ b/src/QuestMetadata.cc @@ -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)}, }); } diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh index e557bf5d..c72fd98e 100644 --- a/src/QuestMetadata.hh +++ b/src/QuestMetadata.hh @@ -87,6 +87,7 @@ struct QuestMetadata { }; std::vector bb_map_designate_opcodes; std::vector create_item_mask_entries; + std::vector 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> 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; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index b26cc660..7c124331 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -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>(); - for (size_t z = 0; z < 0x14; z++) { - ret.meta.header_unknown_a5->at(z) = static_cast(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(); - meta.header_unknown_a5 = std::make_shared>(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()) { diff --git a/src/QuestScript.hh b/src/QuestScript.hh index ae5ade72..86ba9af9 100644 --- a/src/QuestScript.hh +++ b/src/QuestScript.hh @@ -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 unused = 0xFF; } __packed_ws__(FloorAssignment, 8); - /* 0398 */ parray unknown_a5; + /* 0398 */ le_uint32_t unknown_a5; + /* 039C */ parray solo_unlock_flags; /* 03AC */ parray floor_assignments; /* 042C */ parray create_item_mask_entries; /* 122C */