From 666464dd066c7a6773d653603db1eb7c75fa6300 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 15 Aug 2023 22:11:18 -0700 Subject: [PATCH] add PSO GC snapshot decoder --- README.md | 1 + src/Main.cc | 27 +++++++++++++++++++++++++++ src/SaveFileFormats.cc | 39 +++++++++++++++++++++++++++++++++++++++ src/SaveFileFormats.hh | 18 ++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/README.md b/README.md index 2ff31fc6..37f250cb 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,7 @@ newserv has many CLI options, which can be used to access functionality other th * Encrypt or decrypt data using Episode 3's trivial scheme (`encrypt-trivial-data`, `decrypt-trivial-data`) * Encrypt or decrypt data using the Challenge Mode text algorithm (`encrypt-challenge-data`, `decrypt-challenge-data`) * Encrypt or decrypt PSO GC save data (.gci files) (`encrypt-gci-save`, `decrypt-gci-save`) +* Convert a PSO GC or Episode 3 snapshot file to a BMP image (`decode-gci-snapshot`) * Find the likely round1 or round2 seed for a corrupt save file (`salvage-gci`) * Run a brute-force search for a decryption seed (`find-decryption-seed`) * Decode Shift-JIS text to UTF-16 (`decode-sjis`) diff --git a/src/Main.cc b/src/Main.cc index 14bf387a..b5d17948 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -273,6 +273,7 @@ enum class Behavior { DECRYPT_CHALLENGE_DATA, ENCRYPT_GCI_SAVE, DECRYPT_GCI_SAVE, + DECODE_GCI_SNAPSHOT, FIND_DECRYPTION_SEED, SALVAGE_GCI, DECODE_QUEST_FILE, @@ -314,6 +315,7 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::ENCRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::SALVAGE_GCI) || (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECODE_QUEST_FILE) || @@ -347,6 +349,7 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::ENCRYPT_GCI_SAVE) || + (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::ENCODE_QST) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || (b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || @@ -520,6 +523,8 @@ int main(int argc, char** argv) { behavior = Behavior::DECRYPT_GCI_SAVE; } else if (!strcmp(argv[x], "encrypt-gci-save")) { behavior = Behavior::ENCRYPT_GCI_SAVE; + } else if (!strcmp(argv[x], "decode-gci-snapshot")) { + behavior = Behavior::DECODE_GCI_SNAPSHOT; } else if (!strcmp(argv[x], "find-decryption-seed")) { behavior = Behavior::FIND_DECRYPTION_SEED; } else if (!strcmp(argv[x], "salvage-gci")) { @@ -639,6 +644,8 @@ int main(int argc, char** argv) { } else { filename += ".dec"; } + } else if (behavior == Behavior::DECODE_GCI_SNAPSHOT) { + filename += ".bmp"; } else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) { filename += ".txt"; } else if (behavior == Behavior::CONVERT_ITEMRT_REL_TO_JSON) { @@ -968,6 +975,26 @@ int main(int argc, char** argv) { break; } + case Behavior::DECODE_GCI_SNAPSHOT: { + auto data = read_input_data(); + StringReader r(data); + const auto& header = r.get(); + try { + header.check(); + } catch (const exception& e) { + log_warning("File header failed validation (%s)", e.what()); + } + const auto& file = r.get(); + if (!file.checksum_correct()) { + log_warning("File internal checksum is incorrect"); + } + + auto img = file.decode_image(); + string saved = img.save(Image::Format::WINDOWS_BITMAP); + write_output_data(saved.data(), saved.size()); + break; + } + case Behavior::SALVAGE_GCI: { uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF; if (system_filename) { diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 9531e61e..852a7769 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -115,3 +115,42 @@ string decrypt_gci_fixed_size_file_data_section_for_salvage( return decrypted; } + +bool PSOGCSnapshotFile::checksum_correct() const { + uint32_t crc = crc32("\0\0\0\0", 4); + crc = crc32(&this->width, sizeof(*this) - sizeof(this->checksum), crc); + return (crc == this->checksum); +} + +static uint32_t decode_rgb565(uint16_t c) { + // Input: rrrrrggg gggbbbbb + // Output: rrrrrrrr gggggggg bbbbbbbb aaaaaaaa + return ((c << 16) & 0xF8000000) | ((c << 11) & 0x07000000) | // R + ((c << 13) & 0x00FC0000) | ((c << 7) & 0x00030000) | // G + ((c << 11) & 0x0000F800) | ((c << 6) & 0x00000700) | // B + 0x000000FF; // A +} + +Image PSOGCSnapshotFile::decode_image() const { + if (this->width != 256) { + throw runtime_error("width is incorrect"); + } + if (this->height != 192) { + throw runtime_error("height is incorrect"); + } + + // 4x4 blocks of pixels + Image ret(this->width, this->height, false); + size_t offset = 0; + for (size_t y = 0; y < this->height; y += 4) { + for (size_t x = 0; x < this->width; x += 4) { + for (size_t yy = 0; yy < 4; yy++) { + for (size_t xx = 0; xx < 4; xx++) { + uint32_t color = decode_rgb565(this->pixels[offset++]); + ret.write_pixel(x + xx, y + yy, color); + } + } + } + } + return ret; +} diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 8d13f272..129b8010 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -279,6 +280,23 @@ struct PSOGCGuildCardFile { /* E28C */ } __attribute__((packed)); +struct PSOGCSnapshotFile { + /* 00000 */ be_uint32_t checksum; + /* 00004 */ be_uint16_t width; + /* 00006 */ be_uint16_t height; + /* 00008 */ parray pixels; + /* 18008 */ uint8_t unknown_a1; // Always 0x18? + /* 18009 */ uint8_t unknown_a2; + /* 1800A */ be_int16_t max_players; + /* 1800C */ parray players_present; + /* 1803C */ parray player_levels; + /* 1806C */ parray, 12> player_names; + /* 1818C */ + + bool checksum_correct() const; + Image decode_image() const; +} __attribute__((packed)); + template std::string decrypt_gci_or_vms_v2_data_section( const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) {