From 63f6aff4eddb382b67095bfc9267a97e2d1d1d2c Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 26 Sep 2023 12:12:41 -0700 Subject: [PATCH] add decoder for Ep3 trial download quests --- src/Quest.cc | 78 +++++++++++++++++++++++++----------------- src/SaveFileFormats.cc | 6 +++- src/SaveFileFormats.hh | 1 + 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/Quest.cc b/src/Quest.cc index 3ab9bca7..123fec49 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -165,8 +165,7 @@ string find_seed_and_decrypt_download_quest_data_section( 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); lock_guard g(result_lock); result = std::move(ret); return true; @@ -522,36 +521,53 @@ string Quest::decode_gci_file( return compressed_data; } - } else if (header.game_id[2] == 'S') { // Episode 3 - // The first 0x10 bytes in the data segment appear to be unused. In most - // files I've seen, the last half of it (8 bytes) are duplicates of the - // first 8 bytes of the unscrambled, compressed data, though this is the - // result of an uninitialized memory bug when the client encodes the file - // and not an actual constraint on what should be in these 8 bytes. - r.skip(16); - // The game treats this field as a 16-byte string (including the \0). The 8 - // bytes after it appear to be completely unused. - if (r.readx(15) != "SONICTEAM,SEGA.") { - throw runtime_error("Episode 3 GCI file is not a quest"); + } else if (header.is_ep3()) { + if (header.is_trial()) { + if (known_seed >= 0) { + return decrypt_download_quest_data_section( + r.getv(header.data_size), header.data_size, known_seed); + } else { + if (find_seed_num_threads < 0) { + throw runtime_error("file is encrypted"); + } + if (find_seed_num_threads == 0) { + 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); + } + + } else { + // The first 0x10 bytes in the data segment appear to be unused. In most + // files I've seen, the last half of it (8 bytes) are duplicates of the + // first 8 bytes of the unscrambled, compressed data, though this is the + // result of an uninitialized memory bug when the client encodes the file + // and not an actual constraint on what should be in these 8 bytes. + r.skip(16); + // The game treats this field as a 16-byte string (including the \0). The 8 + // bytes after it appear to be completely unused. + if (r.readx(15) != "SONICTEAM,SEGA.") { + throw runtime_error("Episode 3 GCI file is not a quest"); + } + r.skip(9); + + data = r.readx(header.data_size - 40); + + // For some reason, Sega decided not to encrypt Episode 3 quest files in the + // same way as Episodes 1&2 quest files (see above). Instead, they just + // wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the + // remaining bytes completely unencrypted (but still compressed). + size_t unscramble_size = min(0x100, data.size()); + decrypt_trivial_gci_data(data.data(), unscramble_size, 0); + + size_t decompressed_size = prs_decompress_size(data); + if (decompressed_size != sizeof(Episode3::MapDefinition)) { + throw runtime_error(string_printf( + "decompressed quest is 0x%zX bytes; expected 0x%zX bytes", + decompressed_size, sizeof(Episode3::MapDefinition))); + } + return data; } - r.skip(9); - - data = r.readx(header.data_size - 40); - - // For some reason, Sega decided not to encrypt Episode 3 quest files in the - // same way as Episodes 1&2 quest files (see above). Instead, they just - // wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the - // remaining bytes completely unencrypted (but still compressed). - size_t unscramble_size = min(0x100, data.size()); - decrypt_trivial_gci_data(data.data(), unscramble_size, 0); - - size_t decompressed_size = prs_decompress_size(data); - if (decompressed_size != sizeof(Episode3::MapDefinition)) { - throw runtime_error(string_printf( - "decompressed quest is 0x%zX bytes; expected 0x%zX bytes", - decompressed_size, sizeof(Episode3::MapDefinition))); - } - return data; } else { throw runtime_error("unknown game name in GCI header"); diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 99c59a36..ad032ebb 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -87,7 +87,7 @@ void PSOGCIFileHeader::check() const { if (this->developer_id[0] != '8' || this->developer_id[1] != 'P') { throw runtime_error("GCI file is not for a Sega game"); } - if (this->game_id[0] != 'G') { + if ((this->game_id[0] != 'G') && (this->game_id[0] != 'D')) { throw runtime_error("GCI file is not for a GameCube game"); } if (this->game_id[1] != 'P') { @@ -106,6 +106,10 @@ bool PSOGCIFileHeader::is_ep3() const { return (this->game_id[2] == 'S'); } +bool PSOGCIFileHeader::is_trial() const { + return (this->game_id[0] == 'D'); +} + uint32_t compute_psogc_timestamp( uint16_t year, uint8_t month, diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 735cf131..02e00903 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -86,6 +86,7 @@ struct PSOGCIFileHeader { bool is_ep12() const; bool is_ep3() const; + bool is_trial() const; } __attribute__((packed)); struct PSOGCSystemFile {