From bd1cdfdb978e4deace33b41df866f4549cc784cc Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 26 Nov 2025 22:33:50 -0800 Subject: [PATCH] further improve quest assembler/disassembler matching --- src/Client.cc | 2 +- src/Main.cc | 66 ++--- src/Map.cc | 4 +- src/Quest.cc | 135 +++------- src/Quest.hh | 10 +- src/QuestMetadata.cc | 62 ++++- src/QuestMetadata.hh | 20 +- src/QuestScript.cc | 355 ++++++++++++++------------ src/QuestScript.hh | 18 +- src/ReceiveCommands.cc | 6 +- src/ServerState.cc | 11 +- system/quests/download/q000-gc-e.bin | Bin 638 -> 634 bytes system/quests/download/q072-gc-e.bin | Bin 11206 -> 11206 bytes system/quests/events/q073-bb-e.bin | Bin 12375 -> 12465 bytes system/quests/retrieval/q087-gc-j.bin | Bin 1695 -> 1693 bytes system/quests/retrieval/q138-gc-s.bin | Bin 26665 -> 26591 bytes 16 files changed, 342 insertions(+), 347 deletions(-) diff --git a/src/Client.cc b/src/Client.cc index 3313cef5..cd8d9e23 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -424,7 +424,7 @@ bool Client::can_play_quest( if (!q->has_version_any_language(this->version())) { return false; } - if (num_players > q->meta.max_players) { + if ((q->meta.max_players > 0) && (num_players > q->meta.max_players)) { return false; } return this->evaluate_quest_availability_expression( diff --git a/src/Main.cc b/src/Main.cc index 8069d437..87d023fb 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1576,7 +1576,7 @@ Action a_encode_qst( } auto vq = make_shared(); - vq->version = version; + vq->meta.version = version; vq->bin_contents = bin_data; vq->dat_contents = dat_data; vq->pvr_contents = pvr_data; @@ -3152,8 +3152,8 @@ Action a_check_quest_reassembly( for (const auto& [_, q] : s->quest_index->quests_by_number) { for (const auto& [_, vq] : q->versions) { auto decompressed_bin = prs_decompress(*vq->bin_contents); - auto disassembled = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->version, vq->language, vq->map_file, false, false); - auto reassembly = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->version, vq->language, vq->map_file, true, false); + auto disassembled = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false); + auto reassembly = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, true, false); string include_dir = phosg::dirname(vq->bin_filename()); AssembledQuestScript assembled; try { @@ -3162,44 +3162,44 @@ Action a_check_quest_reassembly( {"system/quests/includes"}, {"system/quests/includes", "system/client-functions/System"}, false); + if (vq->json_contents) { + assembled.meta.apply_json_overrides(*vq->json_contents); + } if (assembled.data != decompressed_bin) { throw std::runtime_error("Reassembled quest script does not match original"); } - if (assembled.quest_number != vq->meta.quest_number) { - throw std::runtime_error(std::format("Reassembled quest number {} does not match original ({})", - assembled.quest_number, vq->meta.quest_number)); + // Don't check quest number, since we override it based on the filename + if (assembled.meta.version != vq->meta.version) { + throw std::runtime_error(std::format("Reassembled quest version ({}) does not match original ({})", + phosg::name_for_enum(assembled.meta.version), phosg::name_for_enum(vq->meta.version))); } - if (assembled.version != vq->version) { - throw std::runtime_error(std::format("Reassembled quest version {} does not match original ({})", - phosg::name_for_enum(assembled.version), phosg::name_for_enum(vq->version))); + if (assembled.meta.language != vq->meta.language) { + throw std::runtime_error(std::format("Reassembled quest language ({}) does not match original ({})", + name_for_language(assembled.meta.language), name_for_language(vq->meta.language))); } - if (assembled.language != vq->language) { - throw std::runtime_error(std::format("Reassembled quest language {} does not match original ({})", - name_for_language(assembled.language), name_for_language(vq->language))); + if (assembled.meta.episode != vq->meta.episode) { + throw std::runtime_error(std::format("Reassembled quest episode ({}) does not match original ({})", + name_for_episode(assembled.meta.episode), name_for_episode(vq->meta.episode))); } - if (assembled.episode != vq->meta.episode) { - throw std::runtime_error(std::format("Reassembled quest episode {} does not match original ({})", - name_for_episode(assembled.episode), name_for_episode(vq->meta.episode))); + if (assembled.meta.joinable != vq->meta.joinable) { + throw std::runtime_error(std::format("Reassembled quest joinable ({}) does not match original ({})", + assembled.meta.joinable, vq->meta.joinable)); } - if (assembled.joinable != vq->meta.joinable) { - throw std::runtime_error(std::format("Reassembled quest joinable {} does not match original ({})", - assembled.joinable, vq->meta.joinable)); + if (assembled.meta.max_players != vq->meta.max_players) { + throw std::runtime_error(std::format("Reassembled quest max_players ({}) does not match original ({})", + assembled.meta.max_players, vq->meta.max_players)); } - if (assembled.max_players != vq->meta.max_players) { - throw std::runtime_error(std::format("Reassembled quest max_players {} does not match original ({})", - assembled.max_players, vq->meta.max_players)); + if (assembled.meta.name != vq->meta.name) { + throw std::runtime_error(std::format("Reassembled quest name ({}) does not match original ({})", + assembled.meta.name, vq->meta.name)); } - if (assembled.name != vq->meta.name) { - throw std::runtime_error(std::format("Reassembled quest name \"{}\" does not match original (\"{}\")", - assembled.name, vq->meta.name)); + if (assembled.meta.short_description != vq->meta.short_description) { + throw std::runtime_error(std::format("Reassembled quest short description ({}) does not match original ({})", + assembled.meta.short_description, vq->meta.short_description)); } - if (assembled.short_description != vq->meta.short_description) { - throw std::runtime_error(std::format("Reassembled quest short description \"{}\" does not match original (\"{}\")", - assembled.short_description, vq->meta.short_description)); - } - if (assembled.long_description != vq->meta.long_description) { - throw std::runtime_error(std::format("Reassembled quest long description \"{}\" does not match original (\"{}\")", - assembled.long_description, vq->meta.long_description)); + if (assembled.meta.long_description != vq->meta.long_description) { + throw std::runtime_error(std::format("Reassembled quest long description ({}) does not match original ({})", + assembled.meta.long_description, vq->meta.long_description)); } } catch (const std::exception& e) { phosg::log_error_f("================ DISASSEMBLY:"); @@ -3210,10 +3210,10 @@ Action a_check_quest_reassembly( phosg::log_error_f("================ BINDIFF:"); phosg::print_binary_diff(stderr, decompressed_bin.data(), decompressed_bin.size(), assembled.data.data(), assembled.data.size(), isatty(fileno(stderr)), 3, 0); } - phosg::log_info_f("... {} {} {} ({}) FAILED", phosg::name_for_enum(vq->version), name_for_language(vq->language), vq->bin_filename(), vq->meta.name); + phosg::log_info_f("... {} {} {} ({}) FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); throw; } - phosg::log_info_f("... {} {} {} ({}) OK", phosg::name_for_enum(vq->version), name_for_language(vq->language), vq->bin_filename(), vq->meta.name); + phosg::log_info_f("... {} {} {} ({}) OK", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); } } }); diff --git a/src/Map.cc b/src/Map.cc index 19dd1d0f..deec83e1 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -4583,7 +4583,7 @@ shared_ptr SuperMap::add_enemy_and_children( case 0x00FD: // TObjNpcNgcBase case 0x00FE: // TObjNpcNgcBase case 0x00FF: // TObjNpcNgcBase - case 0x0100: // Unknown NPC + case 0x0100: // Momoka // All of these have a default child count of zero add(EnemyType::NON_ENEMY_NPC); break; @@ -4923,7 +4923,7 @@ shared_ptr SuperMap::add_enemy_and_children( case 0x00C4: // TBoss3VoloptCore or subclass case 0x00C6: // TBoss3VoloptMonitor case 0x00C7: // TBoss3VoloptHiraisin - case 0x0118: + case 0x0118: // __QUEST_NPC__ add(EnemyType::UNKNOWN); break; diff --git a/src/Quest.cc b/src/Quest.cc index b583cfff..dcdeff47 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -200,10 +200,10 @@ void VersionedQuest::assert_valid() const { if (this->meta.quest_number == 0xFFFFFFFF) { throw runtime_error("quest number is not set"); } - if (this->version == Version::UNKNOWN) { + if (this->meta.version == Version::UNKNOWN) { throw runtime_error("version is not set"); } - if (this->language == Language::UNKNOWN) { + if (this->meta.language == Language::UNKNOWN) { throw runtime_error("language is not set"); } switch (this->meta.episode) { @@ -216,7 +216,7 @@ void VersionedQuest::assert_valid() const { } break; case Episode::EP2: - if (is_v1_or_v2(this->version)) { + if (is_v1_or_v2(this->meta.version)) { throw runtime_error("v1 or v2 quest specifies Episode 2"); } for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { @@ -227,7 +227,7 @@ void VersionedQuest::assert_valid() const { } break; case Episode::EP3: - if (!is_ep3(this->version)) { + if (!is_ep3(this->meta.version)) { throw runtime_error("non-Ep3 quest specifies Episode 3"); } for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { @@ -237,7 +237,7 @@ void VersionedQuest::assert_valid() const { } break; case Episode::EP4: - if (!is_v4(this->version)) { + if (!is_v4(this->meta.version)) { throw runtime_error("non-v4 quest specifies Episode 4"); } for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { @@ -252,9 +252,6 @@ void VersionedQuest::assert_valid() const { default: throw runtime_error("episode is not valid"); } - if (this->meta.max_players == 0) { - throw runtime_error("max players is not set"); - } if (!this->bin_contents) { throw runtime_error("bin file is missing"); } @@ -264,12 +261,6 @@ void VersionedQuest::assert_valid() const { if (!this->map_file) { throw runtime_error("parsed map file is missing"); } - if (this->meta.common_item_set_name.empty() != !this->meta.common_item_set) { - throw runtime_error("common item set name/pointer mismatch"); - } - if (this->meta.rare_item_set_name.empty() != !this->meta.rare_item_set) { - throw runtime_error("rare item set name/pointer mismatch"); - } if (this->meta.allowed_drop_modes && !(this->meta.allowed_drop_modes & (1 << static_cast(this->meta.default_drop_mode)))) { throw runtime_error("default drop mode is not allowed"); @@ -290,7 +281,7 @@ string VersionedQuest::pvr_filename() const { string VersionedQuest::xb_filename() const { return std::format("quest{}_{}.dat", - this->meta.quest_number, static_cast(tolower(char_for_language(this->language)))); + this->meta.quest_number, static_cast(tolower(char_for_language(this->meta.language)))); } string VersionedQuest::encode_qst() const { @@ -301,8 +292,8 @@ string VersionedQuest::encode_qst() const { files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents); } string xb_filename = std::format("quest{}_{}.dat", - this->meta.quest_number, static_cast(tolower(char_for_language(language)))); - return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded); + this->meta.quest_number, static_cast(tolower(char_for_language(this->meta.language)))); + return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->meta.version, this->is_dlq_encoded); } Quest::Quest(shared_ptr initial_version) @@ -314,8 +305,8 @@ phosg::JSON Quest::json() const { auto versions_json = phosg::JSON::list(); for (const auto& [_, vq] : this->versions) { versions_json.emplace_back(phosg::JSON::dict({ - {"Version", phosg::name_for_enum(vq->version)}, - {"Language", ::name_for_language(vq->language)}, + {"Version", phosg::name_for_enum(vq->meta.version)}, + {"Language", ::name_for_language(vq->meta.language)}, {"Name", vq->meta.name}, {"ShortDescription", vq->meta.short_description}, {"LongDescription", vq->meta.long_description}, @@ -357,9 +348,9 @@ void Quest::add_version(shared_ptr vq) { if (this->meta.create_item_mask_entries.empty()) { this->meta.create_item_mask_entries = vq->meta.create_item_mask_entries; } - this->versions.emplace(this->versions_key(vq->version, vq->language), vq); + this->versions.emplace(this->versions_key(vq->meta.version, vq->meta.language), vq); - size_t lang_index = static_cast(vq->language); + size_t lang_index = static_cast(vq->meta.language); auto& name_by_language = this->names_by_language.at(lang_index); if (name_by_language.empty()) { name_by_language = vq->meta.name; @@ -435,12 +426,7 @@ shared_ptr Quest::version(Version v, Language language) co return it->second; } -QuestIndex::QuestIndex( - const string& directory, - shared_ptr category_index, - const unordered_map>& common_item_sets, - const unordered_map>& rare_item_sets, - bool raise_on_any_failure) +QuestIndex::QuestIndex(const string& directory, shared_ptr category_index, bool raise_on_any_failure) : directory(directory), category_index(category_index) { @@ -600,6 +586,7 @@ QuestIndex::QuestIndex( // All quests have a bin file (even in Episode 3, though its format is // different), so we use bin_files as the primary list of all quests that // should be indexed + unordered_map> parsed_json_files; for (auto& [basename, entry] : bin_files) { try { auto vq = make_shared(); @@ -625,9 +612,9 @@ QuestIndex::QuestIndex( vq->meta.category_id = categories.at(basename); if (entry.assembled) { - vq->meta.quest_number = entry.assembled->quest_number; - vq->version = entry.assembled->version; - vq->language = entry.assembled->language; + vq->meta.quest_number = entry.assembled->meta.quest_number; + vq->meta.version = entry.assembled->meta.version; + vq->meta.language = entry.assembled->meta.language; } else { // Get the number from the first token if (quest_number_token.empty()) { @@ -650,30 +637,17 @@ QuestIndex::QuestIndex( {"xb", Version::XB_V3}, {"bb", Version::BB_V4}, }); - vq->version = name_to_version.at(version_token); + vq->meta.version = name_to_version.at(version_token); // Get the language from the last token if (language_token.size() != 1) { throw runtime_error("language token is not a single character"); } - vq->language = language_for_char(language_token[0]); + vq->meta.language = language_for_char(language_token[0]); } auto bin_decompressed = prs_decompress(*entry.data); - populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->version, vq->language); - - // If the quest was assembled (that is, if it came from a .bin.txt file), - // the metadata from the source file overrides any automatically-detected - // values from above - if (entry.assembled) { - vq->meta.quest_number = entry.assembled->quest_number; - vq->meta.episode = entry.assembled->episode; - vq->meta.joinable = entry.assembled->joinable; - vq->meta.max_players = entry.assembled->max_players; - vq->meta.name = entry.assembled->name; - vq->meta.short_description = entry.assembled->short_description; - vq->meta.long_description = entry.assembled->long_description; - } + populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->meta.version, vq->meta.language); // Find the corresponding dat and pvr files with the same basename as the // bin file; if not found, look for them without the language suffix @@ -722,57 +696,13 @@ QuestIndex::QuestIndex( } } if (json_filedata) { - auto metadata_json = phosg::JSON::parse(*json_filedata->data); try { - vq->meta.description_flag = metadata_json.at("DescriptionFlag").as_int(); - } catch (const out_of_range&) { - } - try { - vq->meta.available_expression = make_shared(metadata_json.get_string("AvailableIf")); - } catch (const out_of_range&) { - } - try { - vq->meta.enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); - } catch (const out_of_range&) { - } - try { - vq->meta.allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand"); - } catch (const out_of_range&) { - } - try { - vq->meta.joinable = metadata_json.get_bool("Joinable"); - } catch (const out_of_range&) { - } - try { - vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister"); - } catch (const out_of_range&) { - } - try { - vq->meta.enemy_exp_overrides = QuestMetadata::parse_enemy_exp_overrides(metadata_json.at("EnemyEXPOverrides")); - } catch (const out_of_range&) { - } - try { - vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string(); - } catch (const out_of_range&) { - } - if (!vq->meta.common_item_set_name.empty()) { - vq->meta.common_item_set = common_item_sets.at(vq->meta.common_item_set_name); - } - try { - vq->meta.rare_item_set_name = metadata_json.at("RareItemSetName").as_string(); - } catch (const out_of_range&) { - } - if (!vq->meta.rare_item_set_name.empty()) { - vq->meta.rare_item_set = rare_item_sets.at(vq->meta.rare_item_set_name); - } - try { - vq->meta.allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int(); - } catch (const out_of_range&) { - } - try { - vq->meta.default_drop_mode = phosg::enum_for_name(metadata_json.at("DefaultDropMode").as_string()); + vq->json_contents = parsed_json_files.at(json_filedata); } catch (const out_of_range&) { + vq->json_contents = make_shared(phosg::JSON::parse(*json_filedata->data)); + parsed_json_files.emplace(json_filedata, vq->json_contents); } + vq->meta.apply_json_overrides(*vq->json_contents); } vq->assert_valid(); @@ -793,8 +723,8 @@ QuestIndex::QuestIndex( q_it->second->add_version(vq); static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({})", filenames_str, - phosg::name_for_enum(vq->version), - char_for_language(vq->language), + phosg::name_for_enum(vq->meta.version), + char_for_language(vq->meta.language), vq->meta.quest_number, vq->meta.name); } else { @@ -804,8 +734,8 @@ QuestIndex::QuestIndex( this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q); static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})", filenames_str, - phosg::name_for_enum(vq->version), - char_for_language(vq->language), + phosg::name_for_enum(vq->meta.version), + char_for_language(vq->meta.language), vq->meta.quest_number, vq->meta.name, name_for_episode(vq->meta.episode), @@ -956,13 +886,12 @@ shared_ptr VersionedQuest::create_download_quest(Language overri string decompressed_bin = prs_decompress(*this->bin_contents); void* data_ptr = decompressed_bin.data(); - switch (this->version) { + switch (this->meta.version) { case Version::DC_NTE: if (decompressed_bin.size() < sizeof(PSOQuestHeaderDCNTE)) { throw runtime_error("bin file is too small for header"); } - // There's no known language field in this version, so we don't write - // anything here + // There's no known language field in this version, so we don't write anything here break; case Version::DC_11_2000: case Version::DC_V1: @@ -986,11 +915,11 @@ shared_ptr VersionedQuest::create_download_quest(Language overri case Version::GC_NTE: case Version::GC_V3: case Version::XB_V3: - if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) { + if (decompressed_bin.size() < sizeof(PSOQuestHeaderV3)) { throw runtime_error("bin file is too small for header"); } if (override_language != Language::UNKNOWN) { - reinterpret_cast(data_ptr)->language = override_language; + reinterpret_cast(data_ptr)->language = override_language; } break; case Version::BB_V4: diff --git a/src/Quest.hh b/src/Quest.hh index 2c5634cd..709dee56 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -71,12 +71,11 @@ struct VersionedQuest { // Most of these default values are intentionally invalid; we use these // values to check if each field was parsed during quest indexing. - Version version = Version::UNKNOWN; - Language language = Language::UNKNOWN; std::shared_ptr bin_contents; std::shared_ptr dat_contents; std::shared_ptr map_file; std::shared_ptr pvr_contents; + std::shared_ptr json_contents; bool is_dlq_encoded = false; void assert_valid() const; @@ -132,12 +131,7 @@ struct QuestIndex { std::map> quests_by_name; std::map>> quests_by_category_id_and_number; - QuestIndex( - const std::string& directory, - std::shared_ptr category_index, - const std::unordered_map>& common_item_sets, - const std::unordered_map>& rare_item_sets, - bool raise_on_any_failure); + QuestIndex(const std::string& directory, std::shared_ptr category_index, bool raise_on_any_failure); phosg::JSON json() const; std::shared_ptr get(uint32_t quest_number) const; diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc index 17d4865a..761ea434 100644 --- a/src/QuestMetadata.cc +++ b/src/QuestMetadata.cc @@ -2,6 +2,53 @@ using namespace std; +void QuestMetadata::apply_json_overrides(const phosg::JSON& json) { + try { + this->description_flag = json.at("DescriptionFlag").as_int(); + } catch (const out_of_range&) { + } + try { + this->available_expression = make_shared(json.get_string("AvailableIf")); + } catch (const out_of_range&) { + } + try { + this->enabled_expression = make_shared(json.get_string("EnabledIf")); + } catch (const out_of_range&) { + } + try { + this->allow_start_from_chat_command = json.get_bool("AllowStartFromChatCommand"); + } catch (const out_of_range&) { + } + try { + this->joinable = json.get_bool("Joinable"); + } catch (const out_of_range&) { + } + try { + this->lock_status_register = json.get_int("LockStatusRegister"); + } catch (const out_of_range&) { + } + try { + this->enemy_exp_overrides = QuestMetadata::parse_enemy_exp_overrides(json.at("EnemyEXPOverrides")); + } catch (const out_of_range&) { + } + try { + this->common_item_set_name = json.at("CommonItemSetName").as_string(); + } catch (const out_of_range&) { + } + try { + this->rare_item_set_name = json.at("RareItemSetName").as_string(); + } catch (const out_of_range&) { + } + try { + this->allowed_drop_modes = json.at("AllowedDropModes").as_int(); + } catch (const out_of_range&) { + } + try { + this->default_drop_mode = phosg::enum_for_name(json.at("DefaultDropMode").as_string()); + } catch (const out_of_range&) { + } +} + void QuestMetadata::assign_default_areas(Version version, Episode episode) { for (size_t z = 0; z < 0x12; z++) { this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z); @@ -34,10 +81,11 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { "quest version has a different joinability state (existing: {}, new: {})", this->joinable ? "true" : "false", other.joinable ? "true" : "false")); } - if (this->max_players != other.max_players) { + bool this_has_player_limit = (this->max_players != 0) && (this->max_players != 4); + bool other_has_player_limit = (other.max_players != 0) && (other.max_players != 4); + if ((this_has_player_limit || other_has_player_limit) && (this->max_players != other.max_players)) { throw runtime_error(std::format( - "quest version has a different maximum player count (existing: {}, new: {})", - this->max_players, other.max_players)); + "quest version has a different maximum player count (existing: {}, new: {})", this->max_players, other.max_players)); } if (this->lock_status_register != other.lock_status_register) { throw runtime_error(std::format( @@ -136,17 +184,11 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { "quest version has different common table name (existing: {}, new: {})", this->common_item_set_name, other.common_item_set_name)); } - if (this->common_item_set != other.common_item_set) { - throw runtime_error("quest version has different common table"); - } if (this->rare_item_set_name != other.rare_item_set_name) { throw runtime_error(std::format( "quest version has different rare table name (existing: {}, new: {})", this->rare_item_set_name, other.rare_item_set_name)); } - if (this->rare_item_set != other.rare_item_set) { - throw runtime_error("quest version has different rare table"); - } if (this->allowed_drop_modes != other.allowed_drop_modes) { throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})", this->allowed_drop_modes, other.allowed_drop_modes)); @@ -182,7 +224,7 @@ phosg::JSON QuestMetadata::json() const { {"Episode", name_for_episode(this->episode)}, {"FloorAssignments", std::move(floors_json)}, {"Joinable", this->joinable}, - {"MaxPlayers", this->max_players}, + {"MaxPlayers", this->max_players ? 4 : this->max_players}, {"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)}, {"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)}, {"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)}, diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh index 3f7a1acf..51d410dc 100644 --- a/src/QuestMetadata.hh +++ b/src/QuestMetadata.hh @@ -22,6 +22,9 @@ struct QuestMetadata { // is used in both the Quest and VersionedQuest structures; in Quest, the // name and description are used only internally. + Version version; + Language language; + // Fields that must match across all quest versions uint32_t category_id = 0xFFFFFFFF; uint32_t quest_number = 0xFFFFFFFF; @@ -38,8 +41,6 @@ struct QuestMetadata { std::shared_ptr enabled_expression; std::string common_item_set_name; // blank = use default std::string rare_item_set_name; // blank = use default - std::shared_ptr common_item_set; - std::shared_ptr rare_item_set; uint8_t allowed_drop_modes = 0x00; // 0 = use server default ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 bool allow_start_from_chat_command = false; @@ -47,7 +48,6 @@ struct QuestMetadata { std::unordered_map enemy_exp_overrides; // Extra header fields (only used on BB) - std::shared_ptr> bb_unknown_a5; struct CreateItemMask { struct Range { uint8_t min = 0x00; @@ -74,19 +74,31 @@ struct QuestMetadata { }; std::vector create_item_mask_entries; + // 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. + uint16_t header_unknown_a1 = 0xFFFF; // All versions + uint16_t header_unknown_a2 = 0xFFFF; // All versions + uint8_t header_unknown_a3 = 0; // DCv1 - V3 + uint8_t header_unknown_a4 = 0; // BB only + uint16_t header_unknown_a6 = 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; std::string short_description; std::string long_description; size_t text_offset; size_t label_table_offset; - Language language; static std::unordered_map parse_enemy_exp_overrides(const phosg::JSON& json); static inline uint32_t exp_override_key(Difficulty difficulty, uint8_t floor, EnemyType enemy_type) { return (static_cast(difficulty) << 24) | (static_cast(floor) << 16) | static_cast(enemy_type); } + void apply_json_overrides(const phosg::JSON& json); + void assign_default_areas(Version version, Episode episode); void assert_compatible(const QuestMetadata& other) const; phosg::JSON json() const; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index e623b1ee..741be5ed 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -2251,7 +2251,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsE[0-2] = result point (x, y, z as floats) // regsE[3] = result code (0 = failed, 1 = success) // labelF = control point entries (array of valueA VectorXYZTF structures) - {0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS}, + {0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::VECTOR4F_LIST}}, F_V3_V4 | F_ARGS}, // Same as npc_text, but only applies to a specific player slot. // valueA = client ID @@ -3030,59 +3030,43 @@ std::string disassemble_quest_script( QuestMetadata meta; populate_quest_metadata_from_script(meta, bin_data, bin_size, version, language); - switch (version) { - case Version::DC_NTE: - lines.emplace_back(".name " + escape_string(meta.name)); - break; - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - lines.emplace_back(std::format(".quest_num {}", meta.quest_number)); - lines.emplace_back(std::format(".language {}", char_for_language(meta.language))); - lines.emplace_back(".name " + escape_string(meta.name)); - lines.emplace_back(".short_desc " + escape_string(meta.short_description)); - lines.emplace_back(".long_desc " + escape_string(meta.long_description)); - break; - case Version::PC_NTE: - case Version::PC_V2: - lines.emplace_back(std::format(".quest_num {}", meta.quest_number)); - lines.emplace_back(std::format(".language {}", char_for_language(meta.language))); - lines.emplace_back(".name " + escape_string(meta.name)); - lines.emplace_back(".short_desc " + escape_string(meta.short_description)); - lines.emplace_back(".long_desc " + escape_string(meta.long_description)); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - case Version::XB_V3: - lines.emplace_back(std::format(".quest_num {}", meta.quest_number)); - lines.emplace_back(std::format(".episode {}", token_name_for_episode(meta.episode))); - lines.emplace_back(std::format(".language {}", char_for_language(meta.language))); - lines.emplace_back(".name " + escape_string(meta.name)); - lines.emplace_back(".short_desc " + escape_string(meta.short_description)); - lines.emplace_back(".long_desc " + escape_string(meta.long_description)); - break; - case Version::BB_V4: - lines.emplace_back(std::format(".quest_num {}", meta.quest_number)); - lines.emplace_back(std::format(".episode {}", token_name_for_episode(meta.episode))); - lines.emplace_back(std::format(".language {}", char_for_language(meta.language))); - lines.emplace_back(std::format(".max_players {}", meta.max_players)); - if (meta.joinable) { - lines.emplace_back(".joinable"); - } - lines.emplace_back(std::format(".name {}", escape_string(meta.name))); - lines.emplace_back(std::format(".short_desc {}", escape_string(meta.short_description))); - lines.emplace_back(std::format(".long_desc {}", escape_string(meta.long_description))); - if (meta.bb_unknown_a5) { - lines.emplace_back(std::format(".bb_unknown_a5 {}", phosg::format_data_string(meta.bb_unknown_a5->data(), meta.bb_unknown_a5->size()))); - } - for (const auto& mask : meta.create_item_mask_entries) { - lines.emplace_back(std::format(".allow_create_item {}", mask.str())); - } - break; - default: - throw logic_error("invalid quest version"); + if (version != Version::DC_NTE) { + lines.emplace_back(std::format(".quest_num {}", meta.quest_number)); + } + lines.emplace_back(".name " + escape_string(meta.name)); + if (version != Version::DC_NTE) { + lines.emplace_back(std::format(".short_desc {}", escape_string(meta.short_description))); + lines.emplace_back(std::format(".long_desc {}", escape_string(meta.long_description))); + } + if (!is_v1_or_v2(version) || (version == Version::GC_NTE)) { + lines.emplace_back(std::format(".episode {}", token_name_for_episode(meta.episode))); + lines.emplace_back(std::format(".header_episode 0x{:02X}", meta.header_episode)); + } + lines.emplace_back(std::format(".language {}", char_for_language(meta.language))); + if (!is_pre_v1(version) && !is_v4(version) && (static_cast(meta.language) != meta.header_language)) { + lines.emplace_back(std::format(".header_language 0x{:02X}", meta.header_language)); + } + if (is_v4(version)) { + lines.emplace_back(std::format(".max_players {}", meta.max_players)); + if (meta.joinable) { + lines.emplace_back(".joinable"); + } + for (const auto& mask : meta.create_item_mask_entries) { + lines.emplace_back(std::format(".allow_create_item {}", mask.str())); + } + } + lines.emplace_back(std::format(".header_unknown_a1 0x{:04X}", meta.header_unknown_a1)); + lines.emplace_back(std::format(".header_unknown_a2 0x{:04X}", meta.header_unknown_a2)); + if (!is_pre_v1(version) && !is_v4(version)) { + lines.emplace_back(std::format(".header_unknown_a3 0x{:02X}", meta.header_unknown_a3)); + } + 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_a6 0x{:04X}", meta.header_unknown_a6)); } lines.emplace_back(); @@ -3218,6 +3202,7 @@ std::string disassemble_quest_script( } } if (fs.enemy_sets) { + uint8_t area = meta.area_for_floor.at(floor); for (size_t z = 0; z < fs.enemy_set_count; z++) { // Only NPCs use script labels; no other enemies do const auto& ene_set = fs.enemy_sets[z]; @@ -3227,9 +3212,9 @@ std::string disassemble_quest_script( ((ene_set.base_type >= 0x002B) && (ene_set.base_type <= 0x002D)) || ((ene_set.base_type >= 0x0030) && (ene_set.base_type <= 0x0033)) || ((ene_set.base_type >= 0x0045) && (ene_set.base_type <= 0x0047)) || - ((ene_set.base_type >= 0x00D0) && (ene_set.base_type <= 0x00D7)) || + ((ene_set.base_type >= 0x00D0) && (ene_set.base_type <= 0x00D7) && (area == 0)) || ((ene_set.base_type >= 0x00F0) && (ene_set.base_type <= 0x0100)) || - ((ene_set.base_type >= 0x0110) && (ene_set.base_type <= 0x0112)) || + ((ene_set.base_type >= 0x0110) && (ene_set.base_type <= 0x0112) && (area == 0)) || (ene_set.base_type == 0x00A9) || (ene_set.base_type == 0x0118)) && (ene_set.param5 > 0) && (ene_set.param5 < label_table.size())) { @@ -3553,12 +3538,20 @@ std::string disassemble_quest_script( break; } case Type::R_REG: - case Type::W_REG: { - uint8_t reg = label_r.get_u8(); + case Type::R_REG32: + case Type::W_REG: + case Type::W_REG32: { + uint32_t reg = ((arg.type == Type::R_REG32) || (arg.type == Type::W_REG32)) + ? label_r.get_u32l() + : label_r.get_u8(); if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back((def->opcode == 0x004C) ? ArgStackValue::Type::REG_PTR : ArgStackValue::Type::REG, reg); } - dasm_arg = std::format("r{}", reg); + if (reg > 0xFF) { + dasm_arg = std::format("r{} /* warning: register number out of range */", reg); + } else { + dasm_arg = std::format("r{}", reg); + } break; } case Type::R_REG_SET: { @@ -4257,17 +4250,7 @@ AssembledQuestScript assemble_quest_script( } // Collect metadata directives - Version quest_version = Version::UNKNOWN; - string quest_name; - string quest_short_desc; - string quest_long_desc; - int64_t quest_num = -1; - Language quest_language = Language::ENGLISH; - Episode quest_episode = Episode::EP1; - uint8_t quest_max_players = 4; - bool quest_joinable = false; - std::shared_ptr> bb_unknown_a5; - std::vector create_item_mask_entries; + AssembledQuestScript ret; for (const auto& line : lines) { if (line.text.empty()) { continue; @@ -4279,15 +4262,18 @@ AssembledQuestScript assemble_quest_script( } else if (line.text.starts_with(".version ")) { string name = line.text.substr(9); phosg::strip_leading_whitespace(name); - quest_version = phosg::enum_for_name(name); + ret.meta.version = phosg::enum_for_name(name); + if ((ret.meta.episode == Episode::NONE) && is_v1_or_v2(ret.meta.version) && (ret.meta.version != Version::GC_NTE)) { + ret.meta.episode = Episode::EP1; + } } else if (line.text.starts_with(".name ")) { - quest_name = phosg::parse_data_string(line.text.substr(6)); + ret.meta.name = phosg::parse_data_string(line.text.substr(6)); } else if (line.text.starts_with(".short_desc ")) { - quest_short_desc = phosg::parse_data_string(line.text.substr(12)); + ret.meta.short_description = phosg::parse_data_string(line.text.substr(12)); } else if (line.text.starts_with(".long_desc ")) { - quest_long_desc = phosg::parse_data_string(line.text.substr(11)); + ret.meta.long_description = phosg::parse_data_string(line.text.substr(11)); } else if (line.text.starts_with(".allow_create_item ")) { - if (create_item_mask_entries.size() >= 0x40) { + if (ret.meta.create_item_mask_entries.size() >= 0x40) { throw std::runtime_error("too many .allow_create_item directives; at most 64 are allowed"); } @@ -4295,42 +4281,56 @@ AssembledQuestScript assemble_quest_script( phosg::strip_whitespace(args_str); QuestMetadata::CreateItemMask mask(line.text.substr(19)); - create_item_mask_entries.emplace_back(mask); + ret.meta.create_item_mask_entries.emplace_back(mask); } else if (line.text.starts_with(".quest_num ")) { - quest_num = stoul(line.text.substr(11), nullptr, 0); + ret.meta.quest_number = stoul(line.text.substr(11), nullptr, 0); } else if (line.text.starts_with(".language ")) { auto code = line.text.substr(10); if (code.size() != 1) { throw runtime_error(".language directive argument is invalid"); } - quest_language = language_for_char(code[0]); + ret.meta.language = language_for_char(code[0]); } else if (line.text.starts_with(".episode ")) { - quest_episode = episode_for_token_name(line.text.substr(9)); + ret.meta.episode = episode_for_token_name(line.text.substr(9)); } else if (line.text.starts_with(".max_players ")) { - quest_max_players = stoul(line.text.substr(12), nullptr, 0); + ret.meta.max_players = stoul(line.text.substr(12), nullptr, 0); } else if (line.text.starts_with(".joinable ")) { - quest_joinable = true; - } else if (line.text.starts_with(".bb_unknown_a5 ")) { - std::string data = phosg::parse_data_string(line.text.substr(15)); + ret.meta.joinable = true; + } else if (line.text.starts_with(".header_language ")) { + ret.meta.header_language = stoul(line.text.substr(17), nullptr, 0); + } else if (line.text.starts_with(".header_episode ")) { + ret.meta.header_episode = stoul(line.text.substr(16), nullptr, 0); + } else if (line.text.starts_with(".header_unknown_a1 ")) { + ret.meta.header_unknown_a1 = stoul(line.text.substr(19), nullptr, 0); + } else if (line.text.starts_with(".header_unknown_a2 ")) { + ret.meta.header_unknown_a2 = stoul(line.text.substr(19), nullptr, 0); + } else if (line.text.starts_with(".header_unknown_a3 ")) { + 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_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() != 0x94) { - throw std::runtime_error(".bb_unknown_a5 directive must specify 0x94 bytes of data"); + throw std::runtime_error(".header_unknown_a5 directive must specify 0x94 bytes of data"); } - bb_unknown_a5 = std::make_shared>(); + ret.meta.header_unknown_a5 = std::make_shared>(); for (size_t z = 0; z < 0x94; z++) { - bb_unknown_a5->at(z) = static_cast(data[z]); + ret.meta.header_unknown_a5->at(z) = static_cast(data[z]); } } } }); } - if (quest_version == Version::PC_PATCH || quest_version == Version::BB_PATCH || quest_version == Version::UNKNOWN) { + if (ret.meta.version == Version::PC_PATCH || ret.meta.version == Version::BB_PATCH || ret.meta.version == Version::UNKNOWN) { throw runtime_error(".version directive is missing or invalid"); } - if (quest_num < 0) { + if (ret.meta.quest_number < 0) { throw runtime_error(".quest_num directive is missing or invalid"); } - if (quest_name.empty()) { + if (ret.meta.name.empty()) { throw runtime_error(".name directive is missing or invalid"); } @@ -4488,8 +4488,8 @@ AssembledQuestScript assemble_quest_script( throw runtime_error("data not found for native include: " + filename); }; - bool version_has_args = F_HAS_ARGS & v_flag(quest_version); - const auto& opcodes = opcodes_by_name_for_version(quest_version); + bool version_has_args = F_HAS_ARGS & v_flag(ret.meta.version); + const auto& opcodes = opcodes_by_name_for_version(ret.meta.version); phosg::StringWriter code_w; for (const auto& line : lines) { wrap_exceptions_with_line_ref(line, [&]() -> void { @@ -4509,11 +4509,11 @@ AssembledQuestScript assemble_quest_script( code_w.write(phosg::parse_data_string(line.text.substr(6))); } else if (line.text.starts_with(".cstr ")) { string data = phosg::parse_data_string(line.text.substr(6)); - if (uses_utf16(quest_version)) { + if (uses_utf16(ret.meta.version)) { code_w.write(tt_utf8_to_utf16(data)); code_w.put_u16l(0); } else { - code_w.write((quest_language == Language::JAPANESE) ? tt_utf8_to_sega_sjis(data) : tt_utf8_to_8859(data)); + code_w.write((ret.meta.language == Language::JAPANESE) ? tt_utf8_to_sega_sjis(data) : tt_utf8_to_8859(data)); code_w.put_u8(0); } } else if (line.text.starts_with(".zero ")) { @@ -4538,11 +4538,11 @@ AssembledQuestScript assemble_quest_script( phosg::strip_whitespace(filename); string native_text = get_native_include(filename); string code; - if (is_ppc(quest_version)) { + if (is_ppc(ret.meta.version)) { code = std::move(ResourceDASM::PPC32Emulator::assemble(native_text).code); - } else if (is_x86(quest_version)) { + } else if (is_x86(ret.meta.version)) { code = std::move(ResourceDASM::X86Emulator::assemble(native_text).code); - } else if (is_sh4(quest_version)) { + } else if (is_sh4(ret.meta.version)) { code = std::move(ResourceDASM::SH4Emulator::assemble(native_text).code); } else { throw runtime_error("unknown architecture"); @@ -4603,7 +4603,7 @@ AssembledQuestScript assemble_quest_script( try { auto add_cstr = [&](const string& text, bool bin) -> void { - switch (quest_version) { + switch (ret.meta.version) { case Version::DC_NTE: code_w.write(bin ? text : tt_utf8_to_sega_sjis(text)); code_w.put_u8(0); @@ -4616,7 +4616,7 @@ AssembledQuestScript assemble_quest_script( case Version::GC_EP3_NTE: case Version::GC_EP3: case Version::XB_V3: - code_w.write(bin ? text : ((quest_language == Language::JAPANESE) ? tt_utf8_to_sega_sjis(text) : tt_utf8_to_8859(text))); + code_w.write(bin ? text : ((ret.meta.language == Language::JAPANESE) ? tt_utf8_to_sega_sjis(text) : tt_utf8_to_8859(text))); code_w.put_u8(0); break; case Version::PC_NTE: @@ -4715,9 +4715,9 @@ AssembledQuestScript assemble_quest_script( size_t label_index; auto it = labels_by_name.find(name); if (it == labels_by_name.end()) { - if (strict) { + if (strict || !name.starts_with("label")) { throw runtime_error("label not defined: " + name); - } else if (name.starts_with("label")) { + } else { size_t used_chars; label_index = std::stoul(name.substr(5), &used_chars, 16); if (used_chars != name.size() - 5) { @@ -4831,8 +4831,12 @@ AssembledQuestScript assemble_quest_script( } }); } - while (code_w.size() & 3) { - code_w.put_u8(0); + + // Allow label table to be misaligned on x86 architectures in non-strict mode (some official quests do this) + if (strict || !is_x86(ret.meta.version)) { + while (code_w.size() & 3) { + code_w.put_u8(0); + } } // Assign all register numbers and patch the code section if needed @@ -4871,43 +4875,55 @@ AssembledQuestScript assemble_quest_script( } // Generate header + auto set_basic_header_fields = [&](HeaderT& header) -> void { + ret.meta.text_offset = sizeof(header); + ret.meta.label_table_offset = ret.meta.text_offset + code_w.size(); + header.text_offset = ret.meta.text_offset; + header.label_table_offset = ret.meta.label_table_offset; + header.size = ret.meta.label_table_offset + label_table.size() * sizeof(label_table[0]); + header.unknown_a1 = ret.meta.header_unknown_a1; + header.unknown_a2 = ret.meta.header_unknown_a2; + }; phosg::StringWriter w; - switch (quest_version) { + switch (ret.meta.version) { case Version::DC_NTE: { PSOQuestHeaderDCNTE header; - header.text_offset = sizeof(header); - header.label_table_offset = sizeof(header) + code_w.size(); - header.size = header.label_table_offset + label_table.size() * sizeof(label_table[0]); - header.name.encode(quest_name, Language::JAPANESE); + set_basic_header_fields(header); + header.name.encode(ret.meta.name, Language::JAPANESE); w.put(header); break; } - case Version::DC_11_2000: + case Version::DC_11_2000: { + PSOQuestHeaderDC112000 header; + set_basic_header_fields(header); + 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); + break; + } case Version::DC_V1: case Version::DC_V2: { PSOQuestHeaderDC header; - header.text_offset = sizeof(header); - header.label_table_offset = sizeof(header) + code_w.size(); - header.size = header.label_table_offset + label_table.size() * sizeof(label_table[0]); - header.language = quest_language; - header.quest_number = quest_num; - header.name.encode(quest_name, quest_language); - header.short_description.encode(quest_short_desc, quest_language); - header.long_description.encode(quest_long_desc, quest_language); + set_basic_header_fields(header); + header.language = (ret.meta.header_language >= 0) ? static_cast(ret.meta.header_language) : ret.meta.language; + header.unknown_a3 = ret.meta.header_unknown_a3; + header.quest_number = ret.meta.quest_number; + 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); w.put(header); break; } case Version::PC_NTE: case Version::PC_V2: { PSOQuestHeaderPC header; - header.text_offset = sizeof(header); - header.label_table_offset = sizeof(header) + code_w.size(); - header.size = header.label_table_offset + label_table.size() * sizeof(label_table[0]); - header.language = quest_language; - header.quest_number = quest_num; - header.name.encode(quest_name, quest_language); - header.short_description.encode(quest_short_desc, quest_language); - header.long_description.encode(quest_long_desc, quest_language); + set_basic_header_fields(header); + header.language = (ret.meta.header_language >= 0) ? static_cast(ret.meta.header_language) : ret.meta.language; + header.unknown_a3 = ret.meta.header_unknown_a3; + header.quest_number = ret.meta.quest_number; + 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); w.put(header); break; } @@ -4916,43 +4932,44 @@ AssembledQuestScript assemble_quest_script( case Version::GC_EP3_NTE: case Version::GC_EP3: case Version::XB_V3: { - PSOQuestHeaderGC header; - header.text_offset = sizeof(header); - header.label_table_offset = sizeof(header) + code_w.size(); - header.size = header.label_table_offset + label_table.size() * sizeof(label_table[0]); - header.language = quest_language; - header.quest_number = quest_num; - header.name.encode(quest_name, quest_language); - header.short_description.encode(quest_short_desc, quest_language); - header.long_description.encode(quest_long_desc, quest_language); + PSOQuestHeaderV3 header; + set_basic_header_fields(header); + header.language = (ret.meta.header_language >= 0) ? static_cast(ret.meta.header_language) : ret.meta.language; + header.unknown_a3 = ret.meta.header_unknown_a3; + header.quest_number = ret.meta.quest_number; + 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); w.put(header); break; } case Version::BB_V4: { PSOQuestHeaderBB header; - header.text_offset = sizeof(header); - header.label_table_offset = sizeof(header) + code_w.size(); - header.size = header.label_table_offset + label_table.size() * sizeof(label_table[0]); - header.quest_number = quest_num; - if (quest_episode == Episode::EP4) { + set_basic_header_fields(header); + header.quest_number = ret.meta.quest_number; + header.unknown_a6 = ret.meta.header_unknown_a6; + if (ret.meta.header_episode >= 0) { + header.episode = ret.meta.header_episode; + } else if (ret.meta.episode == Episode::EP4) { header.episode = 2; - } else if (quest_episode == Episode::EP2) { + } else if (ret.meta.episode == Episode::EP2) { header.episode = 1; } else { header.episode = 0; } - header.max_players = (quest_max_players == 4) ? 0 : quest_max_players; - header.joinable = quest_joinable ? 1 : 0; - header.name.encode(quest_name, quest_language); - header.short_description.encode(quest_short_desc, quest_language); - header.long_description.encode(quest_long_desc, quest_language); - if (bb_unknown_a5) { - header.unknown_a5 = *bb_unknown_a5; + header.max_players = ret.meta.max_players; + header.joinable = ret.meta.joinable ? 1 : 0; + header.unknown_a4 = ret.meta.header_unknown_a4; + 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); } - for (size_t z = 0; z < create_item_mask_entries.size(); z++) { - header.create_item_mask_entries[z] = create_item_mask_entries[z]; + for (size_t z = 0; z < ret.meta.create_item_mask_entries.size(); z++) { + header.create_item_mask_entries[z] = ret.meta.create_item_mask_entries[z]; } w.put(header); break; @@ -4962,18 +4979,9 @@ AssembledQuestScript assemble_quest_script( } w.write(code_w.str()); w.write(label_table.data(), label_table.size() * sizeof(label_table[0])); - return AssembledQuestScript{ - .data = std::move(w.str()), - .quest_number = quest_num, - .version = quest_version, - .language = quest_language, - .episode = quest_episode, - .joinable = quest_joinable, - .max_players = quest_max_players, - .name = quest_name, - .short_description = quest_short_desc, - .long_description = quest_long_desc, - }; + ret.data = std::move(w.str()); + + return ret; } void populate_quest_metadata_from_script( @@ -4984,6 +4992,8 @@ void populate_quest_metadata_from_script( switch (version) { case Version::DC_NTE: { const auto& header = r.get(); + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; meta.episode = Episode::EP1; meta.max_players = 4; meta.name = header.name.decode(language); @@ -4997,6 +5007,8 @@ void populate_quest_metadata_from_script( } case Version::DC_11_2000: { const auto& header = r.get(); + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; meta.episode = Episode::EP1; meta.max_players = 4; meta.language = (language == Language::UNKNOWN) ? Language::JAPANESE : language; @@ -5013,6 +5025,10 @@ void populate_quest_metadata_from_script( case Version::DC_V1: case Version::DC_V2: { const auto& header = r.get(); + meta.header_language = static_cast(header.language); + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; + meta.header_unknown_a3 = header.unknown_a3; meta.episode = Episode::EP1; meta.max_players = 4; meta.language = (language != Language::UNKNOWN) ? language : ((static_cast(header.language) < 5) ? header.language : Language::ENGLISH); @@ -5029,6 +5045,10 @@ void populate_quest_metadata_from_script( case Version::PC_NTE: case Version::PC_V2: { const auto& header = r.get(); + meta.header_language = static_cast(header.language); + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; + meta.header_unknown_a3 = header.unknown_a3; meta.episode = Episode::EP1; meta.max_players = 4; if (meta.quest_number == 0xFFFFFFFF) { @@ -5051,7 +5071,11 @@ void populate_quest_metadata_from_script( // same as Episode 3 maps and download quests. Quest scripts (handled // here) are only used offline in story mode, but can be disassembled // with disassemble_quest_script, hence we need to be able to parse them. - const auto& header = r.get(); + const auto& header = r.get(); + meta.header_language = static_cast(header.language); + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; + meta.header_unknown_a3 = header.unknown_a3; meta.episode = Episode::EP1; meta.max_players = 4; if (meta.quest_number == 0xFFFFFFFF) { @@ -5067,9 +5091,14 @@ void populate_quest_metadata_from_script( } case Version::BB_V4: { const auto& header = r.get(); + meta.header_episode = header.episode; + meta.header_unknown_a1 = header.unknown_a1; + meta.header_unknown_a2 = header.unknown_a2; + meta.header_unknown_a4 = header.unknown_a4; + meta.header_unknown_a6 = header.unknown_a6; meta.episode = episode_for_quest_episode_number(header.episode); meta.joinable |= header.joinable; - meta.max_players = header.max_players ? header.max_players : 4; + meta.max_players = header.max_players; if (meta.quest_number == 0xFFFFFFFF) { meta.quest_number = header.quest_number; } @@ -5084,7 +5113,7 @@ void populate_quest_metadata_from_script( (header.label_table_offset >= sizeof(PSOQuestHeaderBB))) { r.go(0); const auto& header = r.get(); - meta.bb_unknown_a5 = std::make_shared>(header.unknown_a5); + meta.header_unknown_a5 = std::make_shared>(header.unknown_a5); 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 53e8a22b..def2caf3 100644 --- a/src/QuestScript.hh +++ b/src/QuestScript.hh @@ -62,9 +62,7 @@ struct PSOQuestHeaderPC { /* 0394 */ } __packed_ws__(PSOQuestHeaderPC, 0x394); -// TODO: Is the XB quest header format the same as on GC? If not, make a -// separate struct; if so, rename this struct to V3. -struct PSOQuestHeaderGC { +struct PSOQuestHeaderV3 { /* 0000 */ le_uint32_t text_offset = 0; /* 0004 */ le_uint32_t label_table_offset = 0; /* 0008 */ le_uint32_t size = 0; @@ -81,7 +79,7 @@ struct PSOQuestHeaderGC { /* 0034 */ pstring short_description; /* 00B4 */ pstring long_description; /* 01D4 */ -} __packed_ws__(PSOQuestHeaderGC, 0x1D4); +} __packed_ws__(PSOQuestHeaderV3, 0x1D4); struct CreateItemMaskEntry { parray data1_fields; @@ -104,7 +102,7 @@ struct PSOQuestHeaderBBBase { /* 000C */ le_uint16_t unknown_a1 = 0xFFFF; /* 000E */ le_uint16_t unknown_a2 = 0xFFFF; /* 0010 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests - /* 0012 */ le_uint16_t unknown_a3 = 0; + /* 0012 */ le_uint16_t unknown_a6 = 0; /* 0014 */ uint8_t episode = 0; // 0 = Ep1, 1 = Ep2, 2 = Ep4 /* 0015 */ uint8_t max_players = 0; // 0 means no limit (that is, 4) /* 0016 */ uint8_t joinable = 0; @@ -136,15 +134,7 @@ std::string disassemble_quest_script( struct AssembledQuestScript { std::string data; - int64_t quest_number = -1; - Version version = Version::UNKNOWN; - Language language = Language::UNKNOWN; - Episode episode = Episode::NONE; - bool joinable = false; - uint8_t max_players = 4; - std::string name; - std::string short_description; - std::string long_description; + QuestMetadata meta; }; AssembledQuestScript assemble_quest_script( const std::string& text, diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 8297b9f9..065effba 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -363,7 +363,7 @@ static asio::awaitable on_login_complete(shared_ptr c) { lobby_data.guild_card_number = c->login->account->account_id; send_command_t(c, 0x64, 0x01, cmd); } else { - c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name); + c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->meta.language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); string xb_filename = vq->xb_filename(); @@ -2495,7 +2495,7 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi lc->channel->disconnect(); break; } - lc->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name); + lc->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->meta.language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); @@ -4979,7 +4979,7 @@ static asio::awaitable on_6F(shared_ptr c, Channel::Message& msg) if (!vq) { throw std::logic_error("cannot find patch enable quest version after it was previously found during login"); } - c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name); + c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->meta.language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); string xb_filename = vq->xb_filename(); diff --git a/src/ServerState.cc b/src/ServerState.cc index ca89cd97..a2afb656 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -508,8 +508,8 @@ ItemData ServerState::parse_item_description(Version version, const string& desc } shared_ptr ServerState::common_item_set(Version logic_version, shared_ptr q) const { - if (q && q->meta.common_item_set) { - return q->meta.common_item_set; + if (q && !q->meta.common_item_set_name.empty()) { + return this->common_item_sets.at(q->meta.common_item_set_name); } else if (is_v1_or_v2(logic_version) && (logic_version != Version::GC_NTE)) { // TODO: We should probably have a v1 common item set at some point too return this->common_item_sets.at("common-table-v1-v2"); @@ -521,8 +521,8 @@ shared_ptr ServerState::common_item_set(Version logic_versi } shared_ptr ServerState::rare_item_set(Version logic_version, shared_ptr q) const { - if (q && q->meta.rare_item_set) { - return q->meta.rare_item_set; + if (q && !q->meta.rare_item_set_name.empty()) { + return this->rare_item_sets.at(q->meta.rare_item_set_name); } else if (is_v1(logic_version)) { return this->rare_item_sets.at("rare-table-v1"); } else if (is_v2(logic_version) && (logic_version != Version::GC_NTE)) { @@ -2150,8 +2150,7 @@ void ServerState::load_ep3_tournament_state() { void ServerState::load_quest_index(bool raise_on_any_failure) { config_log.info_f("Collecting quests"); - this->quest_index = make_shared( - "system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets, raise_on_any_failure); + this->quest_index = make_shared("system/quests", this->quest_category_index, raise_on_any_failure); } void ServerState::compile_functions(bool raise_on_any_failure) { diff --git a/system/quests/download/q000-gc-e.bin b/system/quests/download/q000-gc-e.bin index d5451615667f27ba242976cabb50331d33bf2baf..e2c366a8631deac373b865af37d123000dc29c9f 100644 GIT binary patch literal 634 zcmV-=0)_oQ)Bykhm;?;`&xWpW@NO>b}@RsQ`mFEKGMFg7UZ|Nk83 zZgX`0-g6*fa%CWM|8QY-yI|P{uclN3f+v+?z}kw z-KWRhlRra{&Jhn&7Bp;Bs!@jsX4&vrgcRz- zrhF{`AOsK|W&cS|b9HbmAZhz_bB*sV3P~XUXkm6`AYmYC|8HU-W^ZyJdGO~h08mcw z=x$|YWc2bNbZ;QxXZ>VvcTsNjaPJZL#BO8?X>NUX(d}txVRRa_aB>c_Y-Me@|65K= za$#*@asz81*NDwg=(IS$3RfD_75-&R;_>Qor6FUbV?fqtbb{Q|YyoVDBl~l@0MPiv zNK#gAV={P7bBwcS#sOqa!gy`AUg>T_k_5Rq(7RbhE^YBbj%VnD3kWq^Psa)D_t z#%)1k?r>1xd4BzwAo&bs`DMLqZ(~#}Ys~q)E?h|tQsDa#0JH9s0Z9qi`Rf$*Lm_C* zXYiXrVrL(108m^265wg(G$wgM^d|&pJaK@mq&B}kOK)gwWtIN}Fh11q!U1i0j>_|O zZ9egI255A2|8P1lFJ^Umdc$dPY%Vb~FfQRD^(aPh0sjPqCs^ U`2YX-|Nr2v0D$KaVBzKf0LD!%RR910 literal 638 zcmV-^0)hQM)Bykhm;?;`&0Qmn3 z=7(-`bl!9SAYpQ4AaihGKX-0vZf77I{2+8_!tg`mbaLl!Y#_S;KKTDsLu_efgqu2r zZ~t{{YGE=mG&KJfFDV}R!K7<+k6Uu@aq?=A{S{~%~#c4Z)8ApdG_VjyO3avT*J*W2Iw&)@O7C+|vVWY=}ntbGiV~ z_=reSR>y8KcusQ|vuFdxWKD#4ZS`p#ZJO$HWgy^@Y@AhLd2?uL*C1kmtlnh+fF%QR zX)ld!L1XTraNv1<{W2i=3}y3Wy=-r(V=QaT`FJi|Ne=J1^;8f5v+k1tV@V15>xUHe zLuk!sz?(sTVrOjtP+R~=;A!U2CV4{iClF{n0CBATq&7ZFZ)j{~4we5fKGeX%0XuDZ z%JX$?#y)fgX#aF{a5^t9W_1sGdTDWN&n_`AF5!svC>U`85&wh`DEyp}MB6|DRrvq^ Y`2YX-|Npo6|Nr0sfaeil;pPGW0KjQ5tpET3 diff --git a/system/quests/download/q072-gc-e.bin b/system/quests/download/q072-gc-e.bin index 8bae770d95fc9c2e4d5bfcc448d38238c7b2cf81..b20e97cdecba74cc176e1957b2d1b702d0f342d7 100644 GIT binary patch delta 19 bcmX>Wek`2J{t6=l!-{C0KOZ)7?a>ARPJ##a delta 19 bcmX>Wek`2J{t6=l!-{C0KW{d2?a>ARPHqSC diff --git a/system/quests/events/q073-bb-e.bin b/system/quests/events/q073-bb-e.bin index a6b2f820fb6a817886d4c62b4fec227be4c7454d..5526d09b2d538e03cca3df52fba8775e4c43705c 100644 GIT binary patch literal 12465 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
w5gk5W(}_G!nD{m^k@bCI;s^9fua7z% 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 CabQ;f literal 12375 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 DolhQa diff --git a/system/quests/retrieval/q087-gc-j.bin b/system/quests/retrieval/q087-gc-j.bin index bdb2372411ea6086ad96745229d2b2458936c492..3050fc252482a3eb1135c6c5117f780f58c48359 100644 GIT binary patch delta 51 ycmbQwJC}FEDOSc)lkM4DCi}CQKrydm>5u>aA&8Hm;^hWl+@cDKh+TJoe|? Lk8DTA9}Eltv@R@0 diff --git a/system/quests/retrieval/q138-gc-s.bin b/system/quests/retrieval/q138-gc-s.bin index 71807c9ad4c5a795406ff6208fabc389f9cd490b..c753c004ca388666c7960428e9dd8e02c8b75133 100644 GIT binary patch delta 36 ucmV+<0Nekm&;j4i0T74O0RaEZF#RM9MEC#y`1Y|7$7cvI^v0(MzyJVR1`;&@ delta 110 zcmcbAo^j;`MuGM#j12$J82tUB=<@yl{~w<>3LH-l+VJJr@`k3ft9}4QV2B*fKA>)$ IQw%@=03DfF8~^|S