From fbdfdb085adb664ad32722a5a00bb580531cc840 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 28 Sep 2023 14:48:26 -0700 Subject: [PATCH] add learnings from Ep3 Trial Edition download quest --- README.md | 3 +- src/CommandFormats.hh | 4 -- src/Compression.cc | 39 +++++++---- src/Compression.hh | 12 ++-- src/Episode3/DataIndexes.cc | 133 ++++++++++++++++++++++++++++++++++-- src/Episode3/DataIndexes.hh | 34 +++++++-- src/Episode3/MapState.cc | 1 - src/Episode3/MapState.hh | 1 - src/Main.cc | 6 +- src/Quest.cc | 120 +++++++++++++++++++------------- src/Quest.hh | 6 +- 11 files changed, 269 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index f8f745f5..c46e2220 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It | GCI (with key) | .bin.gci and .dat.gci | Yes | decode-gci | | GCI (no key) | .bin.gci and .dat.gci | Decode (3) | decode-gci (3) | | GCI (Ep3) | .bin.gci or .mnm.gci | Yes | decode-gci | +| GCI (Ep3 Trial) | .bin.gci or .mnm.gci | Decode (3) | decode-gci (3) | | DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq | | DLQ (Ep3) | .bin.dlq or .mnm.dlq | Yes | decode-dlq | | QST (online) | .qst | Yes | decode-qst | @@ -142,7 +143,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It 3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.* 4. *Episode 3 online quests don't go in the system/quests directory; they instead go in the system/ep3/maps-free or system/ep3/maps-quest directories. If you want an Episode 3 quest to be available for both online play and for downloading, the file must exist in both system/quests and in one of the map directories in system/ep3.* -Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst. There are no encrypted Episode 3 GCI formats because the game doesn't encrypt quests saved to the memory card, unlike Episodes 1&2. +Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst. When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats. diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index a456a2f8..ad7ab447 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2831,7 +2831,6 @@ struct S_GameInformation_GC_Ep3_E1 { /* 0024 */ parray player_entries; /* 00E4 */ parray unknown_a3; /* 0104 */ Episode3::Rules rules; - /* 0114 */ parray unknown_a4; /* 0118 */ parray spectator_entries; } __packed__; @@ -2927,8 +2926,6 @@ struct S_TournamentGameDetails_GC_Ep3_E3 { /* 0024/034C */ ptext map_name; /* 0044/036C */ Episode3::Rules rules; - /* 0054/037C */ parray unknown_a1; - // This field is used only if the bracket pane is shown struct BracketEntry { le_uint16_t win_count = 0; @@ -6252,7 +6249,6 @@ struct G_SetPlayerSubstatus_GC_Ep3_6xB5x3C { struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D { G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; Episode3::Rules rules; - parray unknown_a1; struct Entry { uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM ptext player_name; diff --git a/src/Compression.cc b/src/Compression.cc index 92c68c59..b702e61e 100644 --- a/src/Compression.cc +++ b/src/Compression.cc @@ -794,7 +794,8 @@ string prs_compress_indexed(const string& data, ProgressCallback progress_fn) { return prs_compress_indexed(data.data(), data.size(), progress_fn); } -PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size) { +PRSDecompressResult prs_decompress_with_meta( + const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { // PRS is an LZ77-based compression algorithm. Compressed data is split into // two streams: a control stream and a data stream. The control stream is read // one bit at a time, and the data stream is read one byte at a time. The @@ -839,7 +840,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size // Control 1 = literal byte if (cr.read()) { if (max_output_size && w.size() == max_output_size) { - throw runtime_error("maximum output size exceeded"); + if (allow_unterminated) { + return {std::move(w.str()), r.where()}; + } else { + throw runtime_error("maximum output size exceeded"); + } } w.put_u8(r.get_u8()); @@ -882,7 +887,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size } for (size_t z = 0; z < count; z++) { if (max_output_size && w.size() == max_output_size) { - throw runtime_error("maximum output size exceeded"); + if (allow_unterminated) { + return {std::move(w.str()), r.where()}; + } else { + throw out_of_range("maximum output size exceeded"); + } } w.put_u8(w.str()[read_offset + z]); } @@ -892,21 +901,21 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size return {std::move(w.str()), r.where()}; } -PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size) { - return prs_decompress_with_meta(data.data(), data.size(), max_output_size); +PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size, bool allow_unterminated) { + return prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); } -string prs_decompress(const void* data, size_t size, size_t max_output_size) { - auto ret = prs_decompress_with_meta(data, size, max_output_size); +string prs_decompress(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { + auto ret = prs_decompress_with_meta(data, size, max_output_size, allow_unterminated); return std::move(ret.data); } -string prs_decompress(const string& data, size_t max_output_size) { - auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size); +string prs_decompress(const string& data, size_t max_output_size, bool allow_unterminated) { + auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); return std::move(ret.data); } -size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size) { +size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { size_t ret = 0; StringReader r(data, size); ControlStreamReader cr(r); @@ -943,15 +952,19 @@ size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size } if (max_output_size && ret > max_output_size) { - throw runtime_error("maximum output size exceeded"); + if (allow_unterminated) { + return max_output_size; + } else { + throw out_of_range("maximum output size exceeded"); + } } } return ret; } -size_t prs_decompress_size(const string& data, size_t max_output_size) { - return prs_decompress_size(data.data(), data.size(), max_output_size); +size_t prs_decompress_size(const string& data, size_t max_output_size, bool allow_unterminated) { + return prs_decompress_size(data.data(), data.size(), max_output_size, allow_unterminated); } void prs_disassemble(FILE* stream, const void* data, size_t size) { diff --git a/src/Compression.hh b/src/Compression.hh index 79c06029..4b34785c 100644 --- a/src/Compression.hh +++ b/src/Compression.hh @@ -184,15 +184,15 @@ struct PRSDecompressResult { std::string data; size_t input_bytes_used; }; -PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0); -PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0); -std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0); -std::string prs_decompress(const std::string& data, size_t max_output_size = 0); +PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); +std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); // Returns the decompressed size of PRS-compressed data, without actually // decompressing it. -size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0); -size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0); +size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); // Prints the command stream from a PRS-compressed buffer. void prs_disassemble(FILE* stream, const void* data, size_t size); diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 8a6bcf7e..37591ab7 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -1256,8 +1256,7 @@ void Rules::clear() { this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; this->disable_dice_boost = 0; this->def_dice_range = 0; - this->unused1 = 0; - this->unused2 = 0; + this->unused.clear(0); } string Rules::str() const { @@ -1396,6 +1395,40 @@ string Rules::str() const { return "Rules[" + join(tokens, ", ") + "]"; } +RulesTrial::RulesTrial(const Rules& r) + : overall_time_limit(r.overall_time_limit), + phase_time_limit(r.phase_time_limit), + allowed_cards(r.allowed_cards), + atk_dice_max(r.max_dice), + def_dice_max(r.max_dice), + disable_deck_shuffle(r.disable_deck_shuffle), + disable_deck_loop(r.disable_deck_loop), + char_hp(r.char_hp), + hp_type(r.hp_type), + no_assist_cards(r.no_assist_cards), + disable_dialogue(r.disable_dialogue), + dice_exchange_mode(r.dice_exchange_mode) {} + +RulesTrial::operator Rules() const { + Rules ret; + ret.overall_time_limit = this->overall_time_limit; + ret.phase_time_limit = this->phase_time_limit; + ret.allowed_cards = this->allowed_cards; + ret.min_dice = 1; + ret.max_dice = this->atk_dice_max; + ret.disable_deck_shuffle = this->disable_deck_shuffle; + ret.disable_deck_loop = this->disable_deck_loop; + ret.char_hp = this->char_hp; + ret.hp_type = this->hp_type; + ret.no_assist_cards = this->no_assist_cards; + ret.disable_dialogue = this->disable_dialogue; + ret.dice_exchange_mode = this->dice_exchange_mode; + ret.disable_dice_boost = 0; + ret.def_dice_range = 0x10 | (this->def_dice_max ? this->def_dice_max : 0x06); + ret.unused.clear(0); + return ret; +} + StateFlags::StateFlags() { this->clear(); } @@ -1518,9 +1551,6 @@ string MapDefinition::str(const CardIndex* card_index) const { " a5[0x70:0x74]=%02hhX %02hhX %02hhX %02hhX", this->unknown_a5[0x70], this->unknown_a5[0x71], this->unknown_a5[0x72], this->unknown_a5[0x73])); lines.emplace_back(" default_rules: " + this->default_rules.str()); - lines.emplace_back(string_printf( - " a6=%02hhX %02hhX %02hhX %02hhX", - this->unknown_a6[0], this->unknown_a6[1], this->unknown_a6[2], this->unknown_a6[3])); lines.emplace_back(" name: " + string(this->name)); lines.emplace_back(" location_name: " + string(this->location_name)); lines.emplace_back(" quest_name: " + string(this->quest_name)); @@ -1715,7 +1745,6 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) modification_tiles(map.modification_tiles), unknown_a5(map.unknown_a5), default_rules(map.default_rules), - unknown_a6(map.unknown_a6), name(map.name), location_name(map.location_name), quest_name(map.quest_name), @@ -1744,6 +1773,98 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) } } +MapDefinitionTrial::operator MapDefinition() const { + MapDefinition ret; + ret.unknown_a1 = this->unknown_a1; + ret.map_number = this->map_number; + ret.width = this->width; + ret.height = this->height; + ret.environment_number = this->environment_number; + ret.num_camera_zones = this->num_camera_zones; + ret.map_tiles = this->map_tiles; + ret.start_tile_definitions = this->start_tile_definitions; + ret.camera_zone_maps = this->camera_zone_maps; + ret.camera_zone_specs = this->camera_zone_specs; + ret.overview_specs = this->overview_specs; + ret.modification_tiles = this->modification_tiles; + ret.unknown_a5.clear(0xFF); + ret.unknown_a5 = this->unknown_a5; + ret.default_rules = this->default_rules; + ret.name = this->name; + ret.location_name = this->location_name; + ret.quest_name = this->quest_name; + ret.description = this->description; + ret.map_x = this->map_x; + ret.map_y = this->map_y; + ret.npc_decks = this->npc_decks; + ret.npc_ai_params = this->npc_ai_params; + ret.unknown_a7 = this->unknown_a7; + ret.npc_ai_params_entry_index = this->npc_ai_params_entry_index; + ret.before_message = this->before_message; + ret.after_message = this->after_message; + ret.dispatch_message = this->dispatch_message; + for (size_t z = 0; z < ret.dialogue_sets.size(); z++) { + ret.dialogue_sets[z].sub<8>(0) = this->dialogue_sets[z]; + for (size_t x = 8; x < ret.dialogue_sets[z].size(); x++) { + ret.dialogue_sets[z][x].unknown_a1 = 0xFFFF; + ret.dialogue_sets[z][x].unknown_a2 = 0xFFFF; + for (size_t w = 0; w < 4; w++) { + ret.dialogue_sets[z][x].strings[w].clear(0xFF); + ret.dialogue_sets[z][x].strings[w][0] = 0x00; + } + } + } + ret.reward_card_ids = this->reward_card_ids; + ret.win_level_override = this->win_level_override; + ret.loss_level_override = this->loss_level_override; + ret.field_offset_x = this->field_offset_x; + ret.field_offset_y = this->field_offset_y; + ret.map_category = this->map_category; + ret.cyber_block_type = this->cyber_block_type; + ret.unknown_a11 = this->unknown_a11; + ret.unavailable_sc_cards.clear(0xFFFF); + // The trial edition doesn't seem to have entry_states at all, so we have to + // guess and fill in the field appropriately here. + size_t num_npc_decks = 0; + for (size_t z = 0; z < ret.npc_decks.size(); z++) { + if (ret.npc_decks[z].name[0]) { + num_npc_decks++; + } + } + for (size_t z = 0; z < 4; z++) { + ret.entry_states[z].deck_type = 0xFF; + } + switch (num_npc_decks) { + case 0: // No NPCs; it's a free battle map + ret.entry_states[0].player_type = 0xFF; + ret.entry_states[1].player_type = 0xFF; + ret.entry_states[2].player_type = 0xFF; + ret.entry_states[3].player_type = 0xFF; + break; + case 1: // One NPC; assume it's a 1v1 quest (Player vs. COM) + ret.entry_states[0].player_type = 0x00; + ret.entry_states[1].player_type = 0x04; + ret.entry_states[2].player_type = 0x03; + ret.entry_states[3].player_type = 0x04; + break; + case 2: // Two NPCs; assume it's a 2v2 quest (Player+Player/COM vs. COM+COM) + ret.entry_states[0].player_type = 0x00; + ret.entry_states[1].player_type = 0x02; + ret.entry_states[2].player_type = 0x03; + ret.entry_states[3].player_type = 0x03; + break; + case 3: // Three NPCs; assume it's a 2v2 quest (Player+COM vs. COM+COM) + ret.entry_states[0].player_type = 0x00; + ret.entry_states[1].player_type = 0x03; + ret.entry_states[2].player_type = 0x03; + ret.entry_states[3].player_type = 0x03; + break; + default: // Should be impossible + throw logic_error("too many NPC decks in trial map definition"); + } + return ret; +} + bool Rules::check_invalid_fields() const { Rules t = *this; return t.check_and_reset_invalid_fields(); diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index b63a3d74..c626ecef 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -879,9 +879,8 @@ struct Rules { // NOTE: The following fields are unused in PSO's implementation, but newserv // uses them to implement extended rules. /* 0D */ uint8_t def_dice_range = 0; // High 4 bits = min, low 4 = max - /* 0E */ uint8_t unused1 = 0; - /* 0F */ uint8_t unused2 = 0; - /* 10 */ + /* 0E */ parray unused; + /* 14 */ // Annoyingly, this structure is a different size in Episode 3 Trial Edition. // This means that many command formats, as well as the map format, are @@ -906,6 +905,28 @@ struct Rules { std::string str() const; } __attribute__((packed)); +struct RulesTrial { + // The fields here have the same meaning as in the final version. The only + // difference is that Dice Boost does not exist in the trial version. + /* 00 */ uint8_t overall_time_limit = 0; + /* 01 */ uint8_t phase_time_limit = 0; + /* 02 */ AllowedCards allowed_cards = AllowedCards::ALL; + /* 03 */ uint8_t atk_dice_max = 1; + /* 04 */ uint8_t def_dice_max = 6; + /* 05 */ uint8_t disable_deck_shuffle = 0; + /* 06 */ uint8_t disable_deck_loop = 0; + /* 07 */ uint8_t char_hp = 15; + /* 08 */ HPType hp_type = HPType::DEFEAT_PLAYER; + /* 09 */ uint8_t no_assist_cards = 0; + /* 0A */ uint8_t disable_dialogue = 0; + /* 0B */ DiceExchangeMode dice_exchange_mode = DiceExchangeMode::HIGH_ATK; + /* 0C */ + + RulesTrial() = default; + explicit RulesTrial(const Rules&); + operator Rules() const; +} __attribute__((packed)); + struct StateFlags { /* 00 */ le_uint16_t turn_num; /* 02 */ BattlePhase battle_phase; @@ -1090,7 +1111,6 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests /* 1D68 */ parray unknown_a5; /* 1DDC */ Rules default_rules; - /* 1DEC */ parray unknown_a6; /* 1DF0 */ ptext name; /* 1E04 */ ptext location_name; @@ -1261,9 +1281,8 @@ struct MapDefinitionTrial { /* 1518 */ parray, 2> camera_zone_specs; /* 1AB8 */ parray, 3> overview_specs; /* 1C68 */ parray, 0x10> modification_tiles; - /* 1D68 */ parray unknown_a5; - /* 1DD4 */ Rules default_rules; - /* 1DE4 */ parray unknown_a6; + /* 1D68 */ parray unknown_a5; + /* 1DD4 */ RulesTrial default_rules; /* 1DE8 */ ptext name; /* 1DFC */ ptext location_name; /* 1E10 */ ptext quest_name; @@ -1292,6 +1311,7 @@ struct MapDefinitionTrial { /* 41A0 */ MapDefinitionTrial(const MapDefinition& map); + operator MapDefinition() const; } __attribute__((packed)); struct COMDeckDefinition { diff --git a/src/Episode3/MapState.cc b/src/Episode3/MapState.cc index d48646e2..5bc1e148 100644 --- a/src/Episode3/MapState.cc +++ b/src/Episode3/MapState.cc @@ -47,7 +47,6 @@ void MapAndRulesState::clear() { this->map_number = 0; this->unused4 = 0; this->rules.clear(); - this->unused5 = 0; } bool MapAndRulesState::loc_is_within_bounds(uint8_t x, uint8_t y) const { diff --git a/src/Episode3/MapState.hh b/src/Episode3/MapState.hh index 8ffcb3ee..1845f7cc 100644 --- a/src/Episode3/MapState.hh +++ b/src/Episode3/MapState.hh @@ -34,7 +34,6 @@ struct MapAndRulesState { le_uint32_t map_number; uint32_t unused4; Rules rules; - uint32_t unused5; MapAndRulesState(); void clear(); diff --git a/src/Main.cc b/src/Main.cc index c51887dd..c2b5227a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -756,7 +756,7 @@ int main(int argc, char** argv) { data = prs_compress(data, compression_level, progress_fn); } } else if ((behavior == Behavior::DECOMPRESS_PRS) || (behavior == Behavior::DECOMPRESS_PR2)) { - data = prs_decompress(data); + data = prs_decompress(data, bytes, (bytes != 0)); } else if (behavior == Behavior::COMPRESS_BC0) { if (compress_optimal) { data = bc0_compress_optimal(data.data(), data.size(), optimal_progress_fn); @@ -1323,11 +1323,11 @@ int main(int argc, char** argv) { string output_filename_base = input_filename; if (quest_file_type == Quest::FileFormat::BIN_DAT_GCI) { int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); - auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed); + auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed, skip_checksum); save_file(output_filename_base + ".dec", decoded); } else if (quest_file_type == Quest::FileFormat::BIN_DAT_VMS) { int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); - auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed); + auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed, skip_checksum); save_file(output_filename_base + ".dec", decoded); } else if (quest_file_type == Quest::FileFormat::BIN_DAT_DLQ) { auto decoded = Quest::decode_dlq_file(input_filename); diff --git a/src/Quest.cc b/src/Quest.cc index 123fec49..84c21f53 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -83,7 +83,8 @@ struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader { } __attribute__((packed)); template -string decrypt_download_quest_data_section(const void* data_section, size_t size, uint32_t seed) { +string decrypt_download_quest_data_section( + const void* data_section, size_t size, uint32_t seed, bool skip_checksum = false, bool is_ep3_trial = false) { string decrypted = decrypt_data_section(data_section, size, seed); size_t orig_size = decrypted.size(); @@ -99,41 +100,67 @@ string decrypt_download_quest_data_section(const void* data_section, size_t size round2_crypt.encrypt_t( decrypted.data() + 4, (decrypted.size() - 4)); - if (header->decompressed_size & 0xFFF00000) { - throw runtime_error(string_printf( - "decompressed_size too large (%08" PRIX32 ")", header->decompressed_size.load())); + if (is_ep3_trial) { + StringReader r(decrypted); + r.skip(16); + if (r.readx(15) != "SONICTEAM,SEGA.") { + throw runtime_error("Episode 3 GCI file is not a quest"); + } + r.skip(9); + + // Some Ep3 trial download quests don't have a stop opcode in the PRS + // stream; it seems the client just automatically stops when the correct + // amount of data has been produced. To handle this, we allow the PRS stream + // to be unterminated here. + size_t decompressed_size = prs_decompress_size( + r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true); + if (decompressed_size < sizeof(Episode3::MapDefinitionTrial)) { + throw runtime_error(string_printf( + "decompressed size (%zu) does not match expected size (%zu)", + decompressed_size, sizeof(Episode3::MapDefinitionTrial))); + } + return decrypted.substr(0x28); + + } else { + if (header->decompressed_size & 0xFFF00000) { + throw runtime_error(string_printf( + "decompressed_size too large (%08" PRIX32 ")", header->decompressed_size.load())); + } + + if (!skip_checksum) { + uint32_t expected_crc = header->checksum; + header->checksum = 0; + uint32_t actual_crc = crc32(decrypted.data(), orig_size); + header->checksum = expected_crc; + if (expected_crc != actual_crc && expected_crc != bswap32(actual_crc)) { + throw runtime_error(string_printf( + "incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32, + expected_crc, actual_crc)); + } + } + + // Unlike the above rounds, round 3 is always little-endian (it corresponds to + // the round of encryption done on the server before sending the file to the + // client in the first place) + PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT)); + decrypted.resize(orig_size); + + // Some download quest GCI files have decompressed_size fields that are 8 + // bytes smaller than the actual decompressed size of the data. They seem to + // work fine, so we accept both cases as correct. + size_t decompressed_size = prs_decompress_size( + decrypted.data() + sizeof(HeaderT), + decrypted.size() - sizeof(HeaderT)); + size_t expected_decompressed_size = header->decompressed_size.load(); + if ((decompressed_size != expected_decompressed_size) && + (decompressed_size != expected_decompressed_size - 8)) { + throw runtime_error(string_printf( + "decompressed size (%zu) does not match expected size (%zu)", + decompressed_size, expected_decompressed_size)); + } + + return decrypted.substr(sizeof(HeaderT)); } - - uint32_t expected_crc = header->checksum; - header->checksum = 0; - uint32_t actual_crc = crc32(decrypted.data(), orig_size); - header->checksum = expected_crc; - if (expected_crc != actual_crc && expected_crc != bswap32(actual_crc)) { - throw runtime_error(string_printf( - "incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32, - expected_crc, actual_crc)); - } - - // Unlike the above rounds, round 3 is always little-endian (it corresponds to - // the round of encryption done on the server before sending the file to the - // client in the first place) - PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT)); - decrypted.resize(orig_size); - - // Some download quest GCI files have decompressed_size fields that are 8 - // bytes smaller than the actual decompressed size of the data. They seem to - // work fine, so we accept both cases as correct. - size_t decompressed_size = prs_decompress_size( - decrypted.data() + sizeof(HeaderT), - decrypted.size() - sizeof(HeaderT)); - if ((decompressed_size != header->decompressed_size) && - (decompressed_size != header->decompressed_size - 8)) { - throw runtime_error(string_printf( - "decompressed size (%zu) does not match size in header (%" PRId32 ")", - decompressed_size, header->decompressed_size.load())); - } - - return decrypted.substr(sizeof(HeaderT)); } string decrypt_vms_v1_data_section(const void* data_section, size_t size) { @@ -160,16 +187,17 @@ string decrypt_vms_v1_data_section(const void* data_section, size_t size) { template string find_seed_and_decrypt_download_quest_data_section( - const void* data_section, size_t size, size_t num_threads) { + const void* data_section, size_t size, bool skip_checksum, bool is_ep3_trial, size_t num_threads) { mutex result_lock; string result; uint64_t result_seed = parallel_range([&](uint64_t seed, size_t) { try { - string ret = decrypt_download_quest_data_section(data_section, size, seed); + string ret = decrypt_download_quest_data_section( + data_section, size, seed, skip_checksum, is_ep3_trial); lock_guard g(result_lock); result = std::move(ret); return true; - } catch (const runtime_error&) { + } catch (const runtime_error& e) { return false; } }, @@ -474,7 +502,7 @@ shared_ptr Quest::dat_contents() const { } string Quest::decode_gci_file( - const string& filename, ssize_t find_seed_num_threads, int64_t known_seed) { + const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) { string data = load_file(filename); StringReader r(data); @@ -489,11 +517,11 @@ string Quest::decode_gci_file( if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) { if (known_seed >= 0) { return decrypt_download_quest_data_section( - r.getv(header.data_size), header.data_size, known_seed); + r.getv(header.data_size), header.data_size, known_seed, skip_checksum, false); } else if (header.embedded_seed != 0) { return decrypt_download_quest_data_section( - r.getv(header.data_size), header.data_size, header.embedded_seed); + r.getv(header.data_size), header.data_size, header.embedded_seed, skip_checksum, false); } else { if (find_seed_num_threads < 0) { @@ -503,7 +531,7 @@ string Quest::decode_gci_file( find_seed_num_threads = thread::hardware_concurrency(); } return find_seed_and_decrypt_download_quest_data_section( - r.getv(header.data_size), header.data_size, find_seed_num_threads); + r.getv(header.data_size), header.data_size, skip_checksum, false, find_seed_num_threads); } } else { // Unencrypted GCI format @@ -525,7 +553,7 @@ string Quest::decode_gci_file( if (header.is_trial()) { if (known_seed >= 0) { return decrypt_download_quest_data_section( - r.getv(header.data_size), header.data_size, known_seed); + r.getv(header.data_size), header.data_size, known_seed, true, true); } else { if (find_seed_num_threads < 0) { throw runtime_error("file is encrypted"); @@ -534,7 +562,7 @@ string Quest::decode_gci_file( find_seed_num_threads = thread::hardware_concurrency(); } return find_seed_and_decrypt_download_quest_data_section( - r.getv(header.data_size), header.data_size, find_seed_num_threads); + r.getv(header.data_size), header.data_size, true, true, find_seed_num_threads); } } else { @@ -575,7 +603,7 @@ string Quest::decode_gci_file( } string Quest::decode_vms_file( - const string& filename, ssize_t find_seed_num_threads, int64_t known_seed) { + const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) { string data = load_file(filename); StringReader r(data); @@ -603,7 +631,7 @@ string Quest::decode_vms_file( find_seed_num_threads = thread::hardware_concurrency(); } return find_seed_and_decrypt_download_quest_data_section( - data_section, header.data_size, find_seed_num_threads); + data_section, header.data_size, skip_checksum, 0, find_seed_num_threads); } } diff --git a/src/Quest.hh b/src/Quest.hh index d3b9ed9d..896d7c0f 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -93,11 +93,13 @@ public: static std::string decode_gci_file( const std::string& filename, ssize_t find_seed_num_threads = -1, - int64_t known_seed = -1); + int64_t known_seed = -1, + bool skip_checksum = false); static std::string decode_vms_file( const std::string& filename, ssize_t find_seed_num_threads = -1, - int64_t known_seed = -1); + int64_t known_seed = -1, + bool skip_checksum = false); static std::string decode_dlq_file(const std::string& filename); static std::string decode_dlq_data(const std::string& filename); static std::pair decode_qst_file(const std::string& filename);