From cc702807618867e852db46fdb1ad0aa481f5f6b2 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 23 Sep 2023 17:01:37 -0700 Subject: [PATCH] add PC save file formats and encrypt/decrypt functions --- CMakeLists.txt | 5 +- TODO.md | 2 + src/Main.cc | 127 ++++++++++++- src/PSOEncryption.cc | 37 +++- src/PSOEncryption.hh | 2 + src/PlayerSubordinates.hh | 13 ++ src/Quest.cc | 20 +- src/SaveFileFormats.cc | 5 +- src/SaveFileFormats.hh | 208 ++++++++++++++++++--- tests/{vms => saves}/lionel-v1.dec | Bin tests/{vms => saves}/lionel-v1.vms | Bin tests/{vms => saves}/lionel-v2.dec | Bin tests/{vms => saves}/lionel-v2.vms | Bin tests/saves/pc_gud.bin | Bin 0 -> 31232 bytes tests/saves/pc_gud.dec | Bin 0 -> 31232 bytes tests/saves/pc_sys.bin | Bin 0 -> 970304 bytes tests/saves/pc_sys.dec | Bin 0 -> 970304 bytes tests/{gci => saves}/quest-ep3.dec | Bin tests/{gci => saves}/quest-ep3.gci | Bin tests/{gci => saves}/quest-unencrypted.dec | Bin tests/{gci => saves}/quest-unencrypted.gci | Bin tests/{gci => saves}/quest-with-key.dec | Bin tests/{gci => saves}/quest-with-key.gci | Bin tests/{gci => saves}/quest-without-key.dec | Bin tests/{gci => saves}/quest-without-key.gci | Bin tests/{gci => saves}/save-charfile.gcid | Bin tests/{gci => saves}/save-system.gci | Bin tests/test-decode-gci.sh | 40 ++-- tests/test-decode-pc.sh | 18 ++ tests/test-decode-vms.sh | 14 +- 30 files changed, 412 insertions(+), 79 deletions(-) rename tests/{vms => saves}/lionel-v1.dec (100%) rename tests/{vms => saves}/lionel-v1.vms (100%) rename tests/{vms => saves}/lionel-v2.dec (100%) rename tests/{vms => saves}/lionel-v2.vms (100%) create mode 100644 tests/saves/pc_gud.bin create mode 100755 tests/saves/pc_gud.dec create mode 100644 tests/saves/pc_sys.bin create mode 100755 tests/saves/pc_sys.dec rename tests/{gci => saves}/quest-ep3.dec (100%) rename tests/{gci => saves}/quest-ep3.gci (100%) rename tests/{gci => saves}/quest-unencrypted.dec (100%) rename tests/{gci => saves}/quest-unencrypted.gci (100%) rename tests/{gci => saves}/quest-with-key.dec (100%) rename tests/{gci => saves}/quest-with-key.gci (100%) rename tests/{gci => saves}/quest-without-key.dec (100%) rename tests/{gci => saves}/quest-without-key.gci (100%) rename tests/{gci => saves}/save-charfile.gcid (100%) rename tests/{gci => saves}/save-system.gci (100%) create mode 100755 tests/test-decode-pc.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 8916e209..c60315bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,11 +144,14 @@ add_test( NAME "decode-vms" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-vms.sh ${CMAKE_BINARY_DIR}/newserv) - add_test( NAME "decode-gci" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-gci.sh ${CMAKE_BINARY_DIR}/newserv) +add_test( + NAME "decode-pc" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-pc.sh ${CMAKE_BINARY_DIR}/newserv) # Installation configuration diff --git a/TODO.md b/TODO.md index 6cb0aca9..944f541c 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,8 @@ - Implement per-game logging - Add default values in all command structures (like we use for Episode 3 battle commands) - Check for RCE potential in 6x6B-6x6E commands +- Fix symbol chat header (including face_spec) across PC/GC boundary +- Check size of name field in GuildCardPC ## Episode 3 diff --git a/src/Main.cc b/src/Main.cc index 3145470b..bd5c67bb 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -271,10 +271,15 @@ enum class Behavior { DECRYPT_DATA, ENCRYPT_TRIVIAL_DATA, DECRYPT_TRIVIAL_DATA, + DECRYPT_REGISTRY_VALUE, ENCRYPT_CHALLENGE_DATA, DECRYPT_CHALLENGE_DATA, ENCRYPT_GCI_SAVE, DECRYPT_GCI_SAVE, + ENCRYPT_PC_SAVE, + DECRYPT_PC_SAVE, + ENCRYPT_SAVE_DATA, + DECRYPT_SAVE_DATA, DECODE_GCI_SNAPSHOT, ENCODE_GVM, FIND_DECRYPTION_SEED, @@ -315,13 +320,18 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::DECRYPT_REGISTRY_VALUE) || (b == Behavior::ENCRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_CHALLENGE_DATA) || + (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::ENCRYPT_PC_SAVE) || + (b == Behavior::DECRYPT_PC_SAVE) || + (b == Behavior::ENCRYPT_SAVE_DATA) || + (b == Behavior::DECRYPT_SAVE_DATA) || (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::ENCODE_GVM) || (b == Behavior::SALVAGE_GCI) || - (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::ENCODE_QST) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || @@ -349,10 +359,15 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::DECRYPT_REGISTRY_VALUE) || (b == Behavior::ENCRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_CHALLENGE_DATA) || - (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::ENCRYPT_GCI_SAVE) || + (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::ENCRYPT_PC_SAVE) || + (b == Behavior::DECRYPT_PC_SAVE) || + (b == Behavior::ENCRYPT_SAVE_DATA) || + (b == Behavior::DECRYPT_SAVE_DATA) || (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::ENCODE_GVM) || (b == Behavior::ENCODE_QST) || @@ -520,6 +535,8 @@ int main(int argc, char** argv) { behavior = Behavior::ENCRYPT_TRIVIAL_DATA; } else if (!strcmp(argv[x], "decrypt-trivial-data")) { behavior = Behavior::DECRYPT_TRIVIAL_DATA; + } else if (!strcmp(argv[x], "decrypt-registry-value")) { + behavior = Behavior::DECRYPT_REGISTRY_VALUE; } else if (!strcmp(argv[x], "encrypt-challenge-data")) { behavior = Behavior::ENCRYPT_CHALLENGE_DATA; } else if (!strcmp(argv[x], "decrypt-challenge-data")) { @@ -528,6 +545,14 @@ 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], "decrypt-pc-save")) { + behavior = Behavior::DECRYPT_PC_SAVE; + } else if (!strcmp(argv[x], "encrypt-pc-save")) { + behavior = Behavior::ENCRYPT_PC_SAVE; + } else if (!strcmp(argv[x], "decrypt-save-data")) { + behavior = Behavior::DECRYPT_SAVE_DATA; + } else if (!strcmp(argv[x], "encrypt-save-data")) { + behavior = Behavior::ENCRYPT_SAVE_DATA; } else if (!strcmp(argv[x], "decode-gci-snapshot")) { behavior = Behavior::DECODE_GCI_SNAPSHOT; } else if (!strcmp(argv[x], "encode-gvm")) { @@ -919,6 +944,13 @@ int main(int argc, char** argv) { break; } + case Behavior::DECRYPT_REGISTRY_VALUE: { + string data = read_input_data(); + string out_data = decrypt_v2_registry_value(data.data(), data.size()); + write_output_data(out_data.data(), out_data.size()); + break; + } + case Behavior::ENCRYPT_CHALLENGE_DATA: case Behavior::DECRYPT_CHALLENGE_DATA: { string data = read_input_data(); @@ -957,12 +989,12 @@ int main(int argc, char** argv) { auto process_file = [&]() { if (is_decrypt) { const void* data_section = r.getv(header.data_size); - auto decrypted = decrypt_gci_fixed_size_file_data_section( + auto decrypted = decrypt_fixed_size_data_section_t( data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); *reinterpret_cast(data.data() + data_start_offset) = decrypted; } else { const auto& s = r.get(); - auto encrypted = encrypt_gci_fixed_size_file_data_section( + auto encrypted = encrypt_fixed_size_data_section_t( s, round1_seed); if (data_start_offset + encrypted.size() > data.size()) { throw runtime_error("encrypted result exceeds file size"); @@ -997,6 +1029,89 @@ int main(int argc, char** argv) { break; } + case Behavior::ENCRYPT_PC_SAVE: + case Behavior::DECRYPT_PC_SAVE: { + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the serial number"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + bool is_decrypt = (behavior == Behavior::DECRYPT_PC_SAVE); + + auto data = read_input_data(); + if (data.size() == sizeof(PSOPCGuildCardFile)) { + if (is_decrypt) { + data = decrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed, skip_checksum, override_round2_seed); + } else { + data = encrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed); + } + data.resize((sizeof(PSOPCGuildCardFile) + 0x1FF) & (~0x1FF), '\0'); + } else if (data.size() == sizeof(PSOPCCharacterFile)) { + PSOPCCharacterFile* charfile = reinterpret_cast(data.data()); + if (is_decrypt) { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + try { + charfile->entries[z].character = decrypt_fixed_size_data_section_t( + &charfile->entries[z].character, sizeof(charfile->entries[z].character), round1_seed, skip_checksum, override_round2_seed); + } catch (const exception& e) { + fprintf(stderr, "warning: cannot decrypt character %zu: %s\n", z, e.what()); + } + } + } + } else { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + string encrypted = encrypt_fixed_size_data_section_t( + charfile->entries[z].character, round1_seed); + if (encrypted.size() != sizeof(PSOPCCharacterFile::CharacterEntry::Character)) { + throw logic_error("incorrect encrypted result size"); + } + charfile->entries[z].character = *reinterpret_cast(encrypted.data()); + } + } + } + } else if (data.size() == sizeof(PSOPCCreationTimeFile)) { + throw runtime_error("the PSO______FLS file is not encrypted; it is just random data"); + } else if (data.size() == sizeof(PSOPCSystemFile)) { + throw runtime_error("the PSO______COM file is not encrypted"); + } else { + throw runtime_error("unknown save file type"); + } + + write_output_data(data.data(), data.size()); + break; + } + + case Behavior::ENCRYPT_SAVE_DATA: + case Behavior::DECRYPT_SAVE_DATA: { + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the round1 seed"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + bool is_decrypt = (behavior == Behavior::DECRYPT_SAVE_DATA); + + auto data = read_input_data(); + StringReader r(data); + + string output_data; + size_t effective_size = bytes ? min(bytes, data.size()) : data.size(); + if (is_decrypt) { + output_data = big_endian + ? decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed) + : decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed); + } else { + output_data = big_endian + ? encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed) + : encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed); + } + write_output_data(output_data.data(), output_data.size()); + break; + } + case Behavior::DECODE_GCI_SNAPSHOT: { auto data = read_input_data(); StringReader r(data); @@ -1066,14 +1181,14 @@ int main(int argc, char** argv) { [&](uint64_t seed, size_t thread_num) -> bool { size_t zero_count; if (round2) { - string decrypted = decrypt_gci_fixed_size_file_data_section_for_salvage( + string decrypted = decrypt_gci_fixed_size_data_section_for_salvage( data_section, header.data_size, likely_round1_seed, seed, bytes); zero_count = count_zeroes( decrypted.data() + offset, decrypted.size() - offset, stride); } else { - auto decrypted = decrypt_gci_fixed_size_file_data_section( + auto decrypted = decrypt_fixed_size_data_section_t( data_section, header.data_size, seed, diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index d6cc2b03..f2444be3 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -40,36 +40,44 @@ template void PSOLFGEncryption::encrypt_t(void* vdata, size_t size, bool advance) { using U32T = typename std::conditional::type; - if (size & 3) { - throw invalid_argument("size must be a multiple of 4"); - } if (!advance && (size != 4)) { throw logic_error("cannot peek-encrypt/decrypt with size > 4"); } - size >>= 2; + size_t uint32_count = size >> 2; + size_t extra_bytes = size & 3; U32T* data = reinterpret_cast(vdata); - for (size_t x = 0; x < size; x++) { + for (size_t x = 0; x < uint32_count; x++) { data[x] ^= this->next(advance); } + if (extra_bytes) { + U32T last = 0; + memcpy(&last, &data[uint32_count], extra_bytes); + last ^= this->next(advance); + memcpy(&data[uint32_count], &last, extra_bytes); + } } template void PSOLFGEncryption::encrypt_minus_t(void* vdata, size_t size, bool advance) { using U32T = typename std::conditional::type; - if (size & 3) { - throw invalid_argument("size must be a multiple of 4"); - } if (!advance && (size != 4)) { throw logic_error("cannot peek-encrypt/decrypt with size > 4"); } - size >>= 2; + size_t uint32_count = size >> 2; + size_t extra_bytes = size & 3; U32T* data = reinterpret_cast(vdata); - for (size_t x = 0; x < size; x++) { + for (size_t x = 0; x < uint32_count; x++) { data[x] = this->next(advance) - data[x]; } + if (extra_bytes) { + U32T last = 0; + memcpy(&last, &data[uint32_count], extra_bytes); + last = this->next(advance) - last; + memcpy(&data[uint32_count], &last, extra_bytes); + } } void PSOLFGEncryption::encrypt(void* vdata, size_t size, bool advance) { @@ -975,3 +983,12 @@ std::u16string decrypt_challenge_rank_text(const std::u16string& data) { std::u16string encrypt_challenge_rank_text(const std::u16string& data) { return encrypt_challenge_rank_text(data.data(), data.size()); } + +string decrypt_v2_registry_value(const void* data, size_t size) { + string ret(reinterpret_cast(data), size); + PSOV2Encryption crypt(0x66); + for (size_t z = 0; z < size; z++) { + ret[z] ^= (crypt.next() & 0x7F); + } + return ret; +} diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index 66141f89..fcadc9be 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -272,3 +272,5 @@ template std::u16string encrypt_challenge_rank_text(const ptext& data) { return encrypt_challenge_rank_text(data.data(), data.size()); } + +std::string decrypt_v2_registry_value(const void* data, size_t size); diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index ad7f01ed..f3f45726 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -174,6 +174,19 @@ struct PlayerDispDataBB { void apply_dressing_room(const PlayerDispDataBBPreview&); } __attribute__((packed)); +struct GuildCardPC { + /* 00 */ le_uint32_t player_tag = 0; + /* 04 */ le_uint32_t guild_card_number = 0; + // TODO: Is the length of the name field correct here? + /* 08 */ ptext name; + /* 38 */ ptext description; + /* EC */ uint8_t present = 0; + /* ED */ uint8_t language = 0; + /* EE */ uint8_t section_id = 0; + /* EF */ uint8_t char_class = 0; + /* F0 */ +} __attribute__((packed)); + // TODO: Is this the same for XB as it is for GC? (This struct is based on the // GC format) struct GuildCardV3 { diff --git a/src/Quest.cc b/src/Quest.cc index 10187f17..7da2270b 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -83,10 +83,8 @@ struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader { } __attribute__((packed)); template -string decrypt_gci_or_vms_v2_download_quest_data_section( - const void* data_section, size_t size, uint32_t seed) { - string decrypted = decrypt_gci_or_vms_v2_data_section( - data_section, size, seed); +string decrypt_download_quest_data_section(const void* data_section, size_t size, uint32_t seed) { + string decrypted = decrypt_data_section(data_section, size, seed); size_t orig_size = decrypted.size(); decrypted.resize((decrypted.size() + 3) & (~3)); @@ -161,13 +159,13 @@ string decrypt_vms_v1_data_section(const void* data_section, size_t size) { } template -string find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( +string find_seed_and_decrypt_download_quest_data_section( const void* data_section, size_t size, size_t num_threads) { mutex result_lock; string result; uint64_t result_seed = parallel_range([&](uint64_t seed, size_t) { try { - string ret = decrypt_gci_or_vms_v2_download_quest_data_section( + string ret = decrypt_download_quest_data_section( data_section, size, seed); lock_guard g(result_lock); result = std::move(ret); @@ -530,11 +528,11 @@ string Quest::decode_gci_file( // fields, so assume it's encrypted if any of them are nonzero. if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) { if (known_seed >= 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, known_seed); } else if (header.embedded_seed != 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, header.embedded_seed); } else { @@ -544,7 +542,7 @@ string Quest::decode_gci_file( if (find_seed_num_threads == 0) { find_seed_num_threads = thread::hardware_concurrency(); } - return find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( + return find_seed_and_decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, find_seed_num_threads); } @@ -617,7 +615,7 @@ string Quest::decode_vms_file( } if (known_seed >= 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( data_section, header.data_size, known_seed); } else { @@ -627,7 +625,7 @@ string Quest::decode_vms_file( if (find_seed_num_threads == 0) { find_seed_num_threads = thread::hardware_concurrency(); } - return find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( + return find_seed_and_decrypt_download_quest_data_section( data_section, header.data_size, find_seed_num_threads); } } diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 64f074d9..19d9bd50 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -101,14 +101,13 @@ uint32_t compute_psogc_timestamp( return second + (minute + (hour + (res_day * 24)) * 60) * 60; } -string decrypt_gci_fixed_size_file_data_section_for_salvage( +string decrypt_gci_fixed_size_data_section_for_salvage( const void* data_section, size_t size, uint32_t round1_seed, uint64_t round2_seed, size_t max_decrypt_bytes) { - string decrypted = decrypt_gci_or_vms_v2_data_section( - data_section, size, round1_seed, max_decrypt_bytes); + string decrypted = decrypt_data_section(data_section, size, round1_seed, max_decrypt_bytes); PSOV2Encryption round2_crypt(round2_seed); round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size()); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 8bf430a0..3a937ce9 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -117,6 +117,27 @@ struct PSOGCSaveFileSymbolChatEntry { /* 58 */ } __attribute__((packed)); +struct PSOPCSaveFileSymbolChatEntry { + /* 00 */ le_uint32_t present; + /* 04 */ ptext name; + /* 34 */ uint8_t face_spec; + /* 35 */ uint8_t flags; + /* 36 */ be_uint16_t unused; + struct CornerObject { + uint8_t type; + uint8_t flags_color; + } __attribute__((packed)); + /* 38 */ parray corner_objects; + struct FacePart { + uint8_t type; + uint8_t x; + uint8_t y; + uint8_t flags; + } __attribute__((packed)); + /* 40 */ parray face_parts; + /* 70 */ +} __attribute__((packed)); + struct PSOGCSaveFileChatShortcutEntry { /* 00 */ be_uint32_t present_type; /* 04 */ parray definition; @@ -298,8 +319,7 @@ struct PSOGCSnapshotFile { } __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) { +std::string decrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) { if (max_decrypt_bytes == 0) { max_decrypt_bytes = size; } else { @@ -322,8 +342,7 @@ std::string decrypt_gci_or_vms_v2_data_section( } template -std::string encrypt_gci_or_vms_v2_data_section( - const void* data_section, size_t size, uint32_t round1_seed) { +std::string encrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed) { std::string encrypted(reinterpret_cast(data_section), size); encrypted.resize((encrypted.size() + 3) & (~3)); @@ -338,23 +357,66 @@ std::string encrypt_gci_or_vms_v2_data_section( return ret; } -template -StructT decrypt_gci_fixed_size_file_data_section( +template +std::string decrypt_fixed_size_data_section_s( const void* data_section, size_t size, uint32_t round1_seed, bool skip_checksum = false, uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) { - std::string decrypted = decrypt_gci_or_vms_v2_data_section( - data_section, size, round1_seed); + using U32T = std::conditional_t; + if (size < 2 * sizeof(U32T)) { + throw runtime_error("data size is too small"); + } + std::string decrypted = decrypt_data_section(data_section, size, round1_seed); + + uint32_t round2_seed = override_round2_seed < 0x100000000 + ? static_cast(override_round2_seed) + : reinterpret_cast(decrypted.data() + decrypted.size() - sizeof(U32T))->load(); + PSOV2Encryption round2_crypt(round2_seed); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size() - sizeof(U32T)); + } else { + round2_crypt.encrypt(decrypted.data(), decrypted.size() - sizeof(U32T)); + } + + if (!skip_checksum) { + U32T& checksum = *reinterpret_cast(decrypted.data()); + uint32_t expected_crc = checksum; + checksum = 0; + uint32_t actual_crc = crc32(decrypted.data(), decrypted.size()); + checksum = expected_crc; + if (expected_crc != actual_crc) { + throw std::runtime_error(string_printf( + "incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32, + expected_crc, actual_crc)); + } + } + + return decrypted; +} + +template +StructT decrypt_fixed_size_data_section_t( + const void* data_section, + size_t size, + uint32_t round1_seed, + bool skip_checksum = false, + uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) { + + std::string decrypted = decrypt_data_section(data_section, size, round1_seed); if (decrypted.size() < sizeof(StructT)) { throw std::runtime_error("file too small for structure"); } StructT ret = *reinterpret_cast(decrypted.data()); PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load()); - round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); + } else { + round2_crypt.encrypt(&ret, offsetof(StructT, round2_seed)); + } if (!skip_checksum) { uint32_t expected_crc = ret.checksum; @@ -371,28 +433,55 @@ StructT decrypt_gci_fixed_size_file_data_section( return ret; } -std::string decrypt_gci_fixed_size_file_data_section_for_salvage( - const void* data_section, - size_t size, - uint32_t round1_seed, - uint64_t round2_seed, - size_t max_decrypt_bytes); +template +std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uint32_t round1_seed) { + using U32T = std::conditional_t; -template -std::string encrypt_gci_fixed_size_file_data_section( - const StructT& s, uint32_t round1_seed) { + if (size < 2 * sizeof(U32T)) { + throw runtime_error("data size is too small"); + } + + uint32_t round2_seed = random_object(); + + string encrypted(reinterpret_cast(data), size); + *reinterpret_cast(encrypted.data()) = 0; + *reinterpret_cast(encrypted.data() + encrypted.size() - sizeof(U32T)) = round2_seed; + *reinterpret_cast(encrypted.data()) = crc32(encrypted.data(), encrypted.size()); + + PSOV2Encryption round2_crypt(round2_seed); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(encrypted.data(), encrypted.size()); + } else { + round2_crypt.encrypt(encrypted.data(), encrypted.size()); + } + + return encrypt_data_section(encrypted.data(), encrypted.size(), round1_seed); +} + +template +std::string encrypt_fixed_size_data_section_t(const StructT& s, uint32_t round1_seed) { StructT encrypted = s; encrypted.checksum = 0; encrypted.round2_seed = random_object(); encrypted.checksum = crc32(&encrypted, sizeof(encrypted)); PSOV2Encryption round2_crypt(encrypted.round2_seed); - round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed)); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed)); + } else { + round2_crypt.encrypt(&encrypted, offsetof(StructT, round2_seed)); + } - return encrypt_gci_or_vms_v2_data_section( - &encrypted, sizeof(StructT), round1_seed); + return encrypt_data_section(&encrypted, sizeof(StructT), round1_seed); } +std::string decrypt_gci_fixed_size_data_section_for_salvage( + const void* data_section, + size_t size, + uint32_t round1_seed, + uint64_t round2_seed, + size_t max_decrypt_bytes); + uint32_t compute_psogc_timestamp( uint16_t year, uint8_t month, @@ -400,3 +489,80 @@ uint32_t compute_psogc_timestamp( uint8_t hour, uint8_t minute, uint8_t second); + +struct PSOPCCreationTimeFile { // PSO______FLS + // The game creates this file if necessary and fills it with random data. + // Most of the random data appears to be a decoy; only one field is used. + // As in other PSO versions, creation_timestamp is used as an encryption key + // for the other save files, but only if the serial number isn't set in the + // Windows registry. + /* 0000 */ parray unused1; + /* 0624 */ le_uint32_t creation_timestamp; + /* 0628 */ parray unused2; + /* 1400 */ +} __attribute__((packed)); + +struct PSOPCSystemFile { // PSO_____COM + /* 0000 */ le_uint32_t checksum; + // Most of these fields are guesses based on the format used in GC and the + // assumption that Sega didn't change much between versions. + /* 0004 */ le_int16_t music_volume; + /* 0006 */ int8_t sound_volume; + /* 0007 */ uint8_t language; + /* 0008 */ le_uint32_t unknown_a3; + /* 000C */ parray unknown_a4; // Last one is always 0x1234? + /* 002C */ parray event_flags; + /* 012C */ le_uint32_t round1_seed; + /* 0130 */ parray end_padding; + /* 0200 */ +} __attribute__((packed)); + +struct PSOPCGuildCardFile { // PSO______GUD + /* 0000 */ le_uint32_t checksum; + // TODO: Figure out the PC guild card format. + /* 0004 */ parray unknown_a1; + /* 7984 */ le_uint32_t creation_timestamp; + /* 7988 */ le_uint32_t round2_seed; + /* 798C */ parray end_padding; + /* 7A00 */ +} __attribute__((packed)); + +struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD + /* 00000 */ le_uint32_t signature; // 'CAEN' (stored as 4E 45 41 43) + /* 00004 */ le_uint32_t extra_headers; // 1 + /* 00008 */ le_uint32_t num_entries; // 0x80 + /* 0000C */ le_uint32_t entry_size; // 0x1D54 (actual entry size is +0x40) + /* 00010 */ parray unknown_a1; + struct CharacterEntry { + /* 0000 */ le_uint32_t present; // 1 if character present, 0 if empty + struct Character { + /* 0000 */ le_uint32_t checksum; + /* 0004 */ PlayerInventory inventory; + /* 0350 */ PlayerDispDataDCPCV3 disp; + /* 0420 */ be_uint32_t unknown_a1; + /* 0424 */ be_uint32_t creation_timestamp; + /* 0428 */ be_uint32_t signature; // == 0x6C5D889E? + /* 042C */ be_uint32_t play_time_seconds; + /* 0430 */ be_uint32_t option_flags; // TODO: document bits in this field + /* 0434 */ be_uint32_t save_count; + // TODO: Figure out what this is. On GC, this is where the bank data goes. + /* 0438 */ parray unknown_a2; + /* 0C0C */ GuildCardPC guild_card; + /* 0CFC */ parray symbol_chats; + // TODO: Figure out what this is. On GC, this is where chat shortcuts and + // challenge/battle records go. + /* 123C */ parray unknown_a3; + /* 1CDC */ parray tech_menu_shortcut_entries; + /* 1D04 */ parray unknown_a4; + /* 1D30 */ ptext serial_number; // As %08X (not decimal) + /* 1D40 */ ptext access_key; // As decimal + /* 1D50 */ le_uint32_t round2_seed; + /* 1D54 */ + } __attribute__((packed)); + /* 0004 */ Character character; + /* 1D58 */ parray unused; + /* 1D94 */ + } __attribute__((packed)); + /* 00440 */ parray entries; + /* ECE40 */ +} __attribute__((packed)); diff --git a/tests/vms/lionel-v1.dec b/tests/saves/lionel-v1.dec similarity index 100% rename from tests/vms/lionel-v1.dec rename to tests/saves/lionel-v1.dec diff --git a/tests/vms/lionel-v1.vms b/tests/saves/lionel-v1.vms similarity index 100% rename from tests/vms/lionel-v1.vms rename to tests/saves/lionel-v1.vms diff --git a/tests/vms/lionel-v2.dec b/tests/saves/lionel-v2.dec similarity index 100% rename from tests/vms/lionel-v2.dec rename to tests/saves/lionel-v2.dec diff --git a/tests/vms/lionel-v2.vms b/tests/saves/lionel-v2.vms similarity index 100% rename from tests/vms/lionel-v2.vms rename to tests/saves/lionel-v2.vms diff --git a/tests/saves/pc_gud.bin b/tests/saves/pc_gud.bin new file mode 100644 index 0000000000000000000000000000000000000000..71f5d65847de640f6db649112b8a61eb89a3de6c GIT binary patch literal 31232 zcmV(vKxwaJfF`(Y$4 zx6VuMsJ_;;QCz};+B`pR8N6k}FtSVmBB$<17SJ5HGAcQID6drZGkroZU~Y*h!rJ8P zUEtOXiw=^TS_>YX+S;0MwgfSXp@+Ua>ZXTDo<;Y;+}Gh+4u9tcJ_1#8k{gdx!k! z5J+)k7A*?!`QpVcBvR0UPRLvUj_TATXt|7@oT0uOZdPTYbrzhm9!ejc7;FY-Zqh6E zGiWWAdysN{n8m~71_xFi%h1-7urWrUV0Kupe#G;UOS-o~#}_CLv54ATgmeRO4X)7~c zwQ#O3Ep~77Nfc`Ya#oLrJ{9l8 zYktNcV6j;gvu0p$Rthe)?K1SCl!Pjm*j`$$?iDoBMSx^1S1ss$|pef^W6l-nfx0Usd1+ z07(3&&E0T}xd3)uLSyxGa#k?v1|0aHnSBp?DtJ{AKf8AR)pjFF1c*Bjh`+nCig;;stagrQwa%^?U>x{F$)WZKEIG3J4p{Ui z8NIq%<2Oi9L!Q;c+M6}8E!Cn|U4R`5`QO8AQ0YUXBzq-|ZfjYaU} zhQG|{#!_c7N9+`6-WICm?>cSAk~TE4dAYWbSL9ZJuTD!7YMlz?0vIxCf6(qp#Q0Cw ztU+cHG{`3!9b#-0hM+S&0=E|jUEI84Cx+I%B4 z_BSnfdfw9R=YD~RU^ZK}@>!?kO8b@e)`V1h!?AF$GO85V^E32k1jXH0DkQFYBAd(B z-URTBw2BuaC<4I6#2~ol;_sl*)>rcjPf+(KK39w?E%1b9fn*py|66Ax;KoXDT(7xY z#-hAtR$3OPm*o)G5?&jG7&bL+eEO{XvQpj}EhzZfOPBGKdCUg<-6tA1E`j?u6z4)X zY|LihK8L~N%#6_We&-i&TJBG%`HtDTrJ97$+qD-Wc5G5n2f5E2W@i>`zF09waEmdB z)@YQX=Dlptm_|h5eYna7wYumt|C{Y5W`8Im3RcY`LhPCG z3y!=9;b6WPO1K^4Di5`*nGkaFP@Cf;CH?)l$qw-r4`*S4s<+7 zV-!=|GOm@{HJ`L z%rKt&9M$h{YxZpv-A*nm9-W=cL-#WHLZ=1F*KB0a=+yM^a-6LTg)lcj6jNXsxc;() zT$b#O8EM(w7$1xCJ9X?@_%fg{^J$SD;6iO}kK$-N&k{(iS=fQYo|kQIWnTEkzQyxd z+}IjWR1B%!jslFzs)pqkIo&gqV&LEe()`GvA{9pN`6j&h|15v_=;(4wf8}AP%BS#N z`#>^!U^ij2cKmoE21=c7xuF1!WiBgdR&?McC28xh#<=kQvahjH+m7fO{5a;qIZ6WV z^|7fPnt(JZu4S+c`4{MFW`M^jH-+eAwf?hPDua@qOtxABdZfvoI!1uPyHTt<5CSa6 zh<3hU1XjSFKajbxYuW_xl9kfjKAx4sg{~f^YEELHR1)SeQn;aF1YLx+6i-`O#Lm|4 zMF#Iy{%sD<7DYSK^zf{v)-Bo-M{->g7Qa?Rv)^#yJtqYk+KD583D9(D=>Lr+a#)E< zY~?iRUD$QYND5OiU%2ar@a|HQIzx*01{;>rjetO&Nq9A88R(8Z^1@$Am|{e5y<4*I ze3!bv@-3Ervq&^fv-@0sz!s$NoTw3}cT|H#4~Zf%s_qALa(Eg-2qGj6_WCR>Ag*R} z2x{uaOpzeKtKIazdaz0w;D4+Bmz-ZViB9lGt*{=^C*fo%$45F9)$>6?Fn9V8>TU2LLN9f#QZXQN`-YQnBDhh@ZyI1Y>x?Cj4{8$-K z=6{ii7g&k_>bA)E0Xcco^M4;e^_X)Hj-b=t7RlB8;*C783R&1c)`T z&`~V%fG=uVxED7kKf0yEI{${8gl4ED#yLgF5}QOWp%$V5L08vur1XRmbo-t4;At#` zoTN!$hh*R|UbHx}cX}{74n66HV9Hcb;XnXbwb;_utr!|m{u|Is+-pLLGAMqC_~Kkx zrKa(O{)m@?LA+cBPy8n0X4%Q&!L2`}Ol6oEw{C}~rg~}>$@LuCpvrz_Og`qZrmC)r zh81?&!>E_tKr0Bf`QNRhNLDx_NECqbvDQH9$h=)h=Z6-UutqvD(#tcufr{B$0rEBm zTK~1-w`>F_Lh-|&J-}!_3`tP>dZerpUz_28E{Ebgz7DFiI*z(YI_p$oURESWzp2q| zVh;|Y7s;0eBcdi!^74eh2hyaCu@;~q%t4Aa+vbpmVz@&jJdF2Q#L9AD&WqrRM$C|@ z&9WA5etHfPb*YGfjKmLq+x%#})bWSJ>`G$m>-qd)m?I)^K60uurujXlU+@6%R&!=J(u`#fapVu1u1Pq|(ZKg6j^#u+I2yKcx}}xpTRk7MMk=r;lp-9vT?(w{Y}|5RU1yLf}Fo^VeEOOFdG;P=_K#pQc9jYQ{PlSTh#u}~;gkzn(mi(&y+8C%ScG7c7`bAq#_Iv8OcnLhY zl_*w3z%D-Sufs6S0j6{woV+c!{CgBV z__KEw2JOJ@9&4t}{nc9RXqHl(ZXe9Z{OZ!wc+VDVR;Ea9+}2c#5jNW$IP;Lpc+X6c z?7ayT#G5{X?P$pRFe%>b;^;6r#pX*4H0+?L@TjEx z+P7u`ZAYdCM7Yz41kO|eDP=T8-2a^H7f){Rd=BWhCyEI+r1sktK`I}gWTqm28E!{a zjFGs}XwPN7+|yh1saKRh9++m)Io_-%CESWd2A-hN(Lq7St{1AX9U8@}a**21Be9Ld zucCg;%G^Z>NN;pfe!}S>zv8%dKtw@*I-%VN-OiN=raGc&9#Wk@lNN4wR9&-eYbJQy ze|9|V?0|&uDEEi*F7kd(D$CMTx=^kjy;U%}=~o63oI~}TAXglG;%Jxa^l)xN7QJG`9fO{7B4- zoigPA$H6)D`0F~h$U#vUt&9xYDZ!|L7bY0hiIu4Sp6!|PEimz6NQ44%PAhJ5Ol`!$ zg2up!P~jY#cH>}jVHKWz9I#=Ugj$cR)*?u-l2%~AE1~J$j(qhatK>;CRA~f6e~&x_ z)cX|C;v&=Cy2R`Oy{yvZ0RW}&JaYeOmbIQeZIeb$y$D9MHnO~XYv-RdLO;A z=RY(o*aY+(9HOh@n%EP{tapqpdWOcx-#!v0Z7`)lt4{Uoh;RoVK3gof>nEO!ReJW| zh^6;XDc|tDTr)skP8xFPzQn5OnTe=xmo39`dS>8KNqZ4GVGtubI=;dTzj9uf)5V!* z?l$n=C!qdjdR$<{@>~u0T5NOTf`V`leiG?1Yx0K+YFcdEAvHK#Zdi*hVehc$=;^@@ z&1<&Tmf4N^pz{JSjhTjihI-tmPfzLE4a%^5ObJX3z%@49(Jpo!Ph3mQ94~_umLcfV z5;&j;=@OStxFH9Te{%Jj0_L@YD3o-lO6z8X{kML;Nc z;cftJ7Of$&oV4W9G1)l^E9Z2bh$?Lk%tUdP;Hz&vG-~$BPJ?FRnc1GD=~XfkDiq@i zZgVob8%DGY)|yI!7V168DBlLN^nQ4Il%`k$givx=^4>6fOw}N?agAC)E6P&()o^}g zuL&79*z+FH17F5=qP%e3pGG2-1}qYV8YbPd-=eEnK4naC)l-1nf;%dqR#}h;(ow9mQeG^n7S^H2){=wpk}eou7*~MCBwM*>aBR81@3_sDvD@4LC;*@(Ho!05Ghv*1 zO7{$h8eme?bUVRL8+BR2oVEs_WCHR~E3g&k%HUR~T2VaEbRvd|DCM50l24iMsOna_ zsW*N6XsQ+1EVjiSDRG(Dy87xs3H&7&o*=%6kcaRFus}RSdyxEKuAq9#ES!qL54LD0 zii&btyX`tg_M@4(8D*dm7ygJBFD=`rV^kSNT9pB9rI+-;m4+xORymYB2o2y?pD2U(mKNjqv0EV@*`FqL=X3&i=Xei4D ze*OYt9N~bj(5wzpPVUDXyiJKoXJ^hl%sW&$db}CRYywKg+`^y4;?UXpjqKuDsPy(0 z;)WVhE>XH>y;j#CR;3w0j#5A1u8i6W$rgFA-)UY@{9u?gQ4j|IX0$igBBKrp&ZXwz zNLYC^(t+8iM`zQd61MD)I~Za=8iX1|B&Omyy#1N_1bz;N$Y+&o$Ow|AcovYH#Czsu zM-cKP6rxU`+6|ope~gAq$qGZVA5pywMpiUK4K1%W$Zc7~jL2Y>k063CDm>CJphbSw z&Kjiyvd)y4ivBTq?DB+4RLC!PsMCrBbSIsC3JuVya3ZEl;dMxOt~jO;`OzB6+L#W{ z+ODd!>yBFbB{)C3X=iz1OYyacfI0Z>!2#4r_4P6g9S*gyuTz5fmM&oowD!`ZH(|O&=%xx}MRloDJ9LT0S0 z6xn|P*MJT?TQ(jWraPNbJcglD1_XK=u|a>ENny4I)T_Dj{Db0d40xKn4)Z>{% zNLGfQc;4hfO6U}}{i4c!J6g((uJb6~9)#G!Bp4K4S=+L50(uI#{#2J7^elgt!0EH@ z6z;~vAgq}00XTYD&c?H^%yS`0Nv}_^;F1u7vtO8M6X-aY2tJ?+f`1GU)cka9d_I(8q_MZG zW?UU8GYSVm4LJ)2n} z&RVqkqQfw8;^^d&#&ik`Z;ml9Mno|INT4nCeu zP$ue>J~F%SXt}D?64_~{rSjn5$e&zOw@GhO21r;YwjNnx(&THkyj-Sgt%Nnu+Wf7Y zq`PBF;!pyjJ`Ro_SrkC5YJKV~a_l~UWz7aSj&Vd(u3S(p^x|MM2Ktxdlcmt;0#Q*) zJk=)}-h6Zfp>BxR!%c89VpjzAldJx>Wcz}Air5?z(p@cfdhYnMwu|zw6ZwSU#un*A zjU!=F#tM>B{Q`C#iejyZ>Z(8s!&<`J zS&TV?B>$~E=frajJ0{S;S{Qm0oe28I0sx1AYdOp$=z>P;X6NG9kLbHdicWu%6VsFg zA!ua#YB)jabI{Jh3$89kOwNSSD*O+~D-I$Ha!#SKzY0*hD?E>sfF&V!ZuadwvYsb` zduKfzw_>FXduMEL(eL1RK|_-n7BMI6?E&<-tQg}T?v}3+6ajcs_7pf#@Dy?jDggUJ zEIJ5;Q$bo)ws`~#ZaDJ&MMVNGVq6|0#*7CuG*Dd(vsAVM+J? zPZqMI)lAth7vWH2QD&-REqPqw#pTHwjPjbmwcYDg92pfV`@xUVsX99 zmWwffGN!iLLp(jcCE>*E?(>{QVlU^JkG^>E{TBlg3?!sk`(!d4UK}vQ!5-A3z2G!7 zYFCIVzl+?wBSryjTz1@FM_H_9A6~&v{1R4x5Hr3%_- zqdBy`d$qGvQf;X9^N754**XjqdKszSC}&>)LpJN>d?K!( zXnbutI&_DWyMjhRb8Y~C%a%AYb`%@fzVLNL%75*3CRO4Z?#Aa;fC`QT>|dvl{UhjV z8Uc^?ugten{OX8U#NTI3-q7N=?)DHeFDSE6)EM-Y^tM_)580W03W(ljm0>uU{k_G2kTqZa%_$|l*saAKq3hI zMisOhx*3@X{_U32lsB?lcEnEGX0g$pm6bI^fS^eY^jNB(STvdh0~^+uN^YwbKjEGz zRO(5rYYR+&hYZ z+6Ls6v1rVXJlBUNR?>cpc62#52o76T$d03Q5HFWppoXGWgMUs>FpQSY;nbqK19=*B zz5_O8CFu>IqWTzYUJqElSd#8dPYCVrkY=;7EGlF++e#9Fj5Y$^$tpv7VU>aA4!vm6 zZ<)VTlXMi!bqF9Xslbh7i1L?|1np8M$fB$!UBrj#nt4-MadsM!93`yxR11NkLy(yY zy8O$`(8-Wqn8=dwqS}VBQzHJn_~fs2;jk}!2LnwsSWtkjqdsYl<2k-EYkUXvYT>G` z1BPTIP{cO)X285b_?*PKEo2v7?8Je~zK=@>s<0A`!0*cs=oCScV1(5_@AB5;`;!d$MF^{Oj!lZ{}7 zc%2~^ey?!d@8S&F+-b~sYTCcjRX6Qo-z}Yxa^lYB{ywT=NU$ za}Y@6BMmlpgO@m3@>-L<1j?cUT?`@srZz_e*eg;cK=aTXWU5A5SOh!q+x5Ph3NI!~xc93JlG>C`W4WEc`0b2~kkUbyxBx zY<|s0!4y;#Cu1tTEKSc4;h*;0a{NGgJEs{x11`+EF<5?PmcURMV4)z7gq2#2dA@AW zf9ffuJQN}dYIGFTY>Q^uoS<}#-BAq5E|^jX2=&CT__I4&rDwKI5wY41)`ED*KAM~> z3V^Zz_;>dD)~|ce8z!q&%}kc5c(>7#1KqTzHtK9G7Dci36>cPSt|!W>GBm2Db%!T=CqfuB1FFV?i&- z$n1MB<`lts3_h5G|3FUv%9x2H(r!rj_?n`dx%YA4UdnLXyt23dpfEHvxkc(=K+AM} z_EO}CFBQiuCw6)j(&#;k0$&|6QOBQBTqKwTJWa|r3S7!Y4fkq6q{*j(PFlv>?m4`q zoce`CU|MA*3u}5ygvQ0fe4jZ=n%HuZhN%$WU4X_ zfFv40@95s~F|ijYwuvTkIpsc;Z`?Drvw}b%T%nBn$-XNybOQ1E4Mgrq9%j3wX3lGA zn4%>T-j(*hjJ5?t)Y@lG`e^j>osz!D;=ZQ}=%O%4eNgCP|6Yc9DdGj!N{$kqWaX~T zURwD5Ail~YDXO_EfPiAwUGcwF3o>agj<>{{1mEO)L>gk(CvWKx<&^jqT4x05=Qs=p zfGW(P+^JB)D(GWY8&5n2LZnvDkNdJM>-6gN!xFBAzWJ1S&S9bnTQu6C@ zF4>XX-$cF=V7X>KoDno1pzLonAeoYo2S^Ue?%s;-oNR!w)hq5p4nSh9ys(t6CNvB$ z*BE;UAiPe;(puk`z#;U4MoFWq<%g0J4I#x{f{K3XW3LoQY06ykh9f3No7BKUM zZzlpGD8e|lRc&X9XUPAEf*5SZb&_k#r^dfB`p`>jgeVgmlkl>jkjN5gc+bvO27+L<7qHjZcD`;5LFbGZuLesrNRi3GDfUds6`0OX*NJ?wxheh%O{CA zc-*K)QSg-W1L?NVkF-j^Q>^aqI0qJUwl1t+kPPgyzYyvR7V*rnVtbpGR33^Mk0QLj z%g(-%x-=%+75H<{Vq7m6BFT_X_Wj)G%3QmkTz(or5TSFXXb}sy6Ti`gDrYfu-sC(Q$7mA?#9}iX24Kj48O_iG5 z99U-tXYq{PW596vFg>*NvboA-RMW`&AfyTnx zf`BH+;lOKQHt3v}c2hPb;I;Y^_-4=DH#dG}^#aa7)$jla{yBq!F7RV4uGJ<`n74dH zh8ks{_0RCe(8wRfCvVK;bZMfqdQFgwVizL)&<5RY2wwip9j_bO=0w@njNwzx z#FV-S(zK|4fcRx@ekzXo)Y87x=UJ+0;Fs0tc)(_n4{Fvp%!%A#3Oq-dS>}Y$GTlvS zuo2O2U#RJ;8khry$W#N1>_4DGK^;==WJ&@fudM=grjQwit`a!HQIi7go4@)By>84z zxtzo4*FZN*mRCN^kh>$c*uUo{ilWgD$NQ96E@&8>tn9g$0GtW@2iB1}rpBnkq@R^K zZYD_;yMbtaEuKEQvZkyYpRYGS3_IMcv{8)h0L*?JlX6kIA!U5ZRLQIE%!o@;06bCJ zK-T3YNxis$T>ZdNeK|oZaz)6Q$;b5lWL-e|2I!FpR*wBt-OQmlb_kScd;vDU4B&8% z*9Kx?q)faRymTy{zw)arb76?TB6}h5_}CI6XU7Hxpj(WTFMGW+D|%yN7;^UEn(Vmk zY?%DL$o=4AW+WXK*i>It^<4l zmp?wsfV{CEEz#Vx2z=wLIgypJ+?+hfoN;y~$j&YhZK5vqx{Th`QB{c&bfT+2{PUdp zK2=zDuy|x5bv67-B!d8(s3iW6p{%?v?|2`w`R9$e7r_n6?%J}bPl(cQQSgr0QjW63 zXw>MJRBp-O0{R)#8BU0zptqWsWDqTt`esXH%=@OUROX@)ewWFunEDKsV>hG)Zmb!4 z8=PQ1wB!hkq0{i*Z06V2riEVS*SfB*Af=Pe;M%hC(sB@oWRu4SVGQ2EhfI3uHuzQe z?#hte76}T5(@%36H2G%YBT=JjzdRC)2xHMt(xDePXgMa}mb3ab3f8ehHHmQS!Ld4SgD;+|1 zqOcIvKT1G=G<a$pZX*r^iDd}o z9g}m;;Orsd->K%+Yp5n2oSZ+c@}#L+)1{|jf~K_2JXrV<<=j9j!C7N_4Egf2of zF2MC0`%U;n+z4XBV_T%Bt!TFNl;D7($(eK&$@#b#NFsP8BzGEtHXoHQ#Wvc;_S6#9 zjlk1lpiFKkN44P2u3(A3qH;@W)MQDJ2YLY~mkxF6lp6CRpbsL5ozC;@HaJKCitK!k zNlFvqM+UG75gR@#uVY%>Fx z_cy3JWD~Rj7LPK~t^nrfG^OGKPgw{)P+W8~77jabEhTlDepEfg^^hnnt{u7zPHqhjPb0azq|Rfc)dv%lOE; zkR-bv^WL1uykJy}1ccb;-~dqI4!-FXt5(sq{J?p1nppv%AH&Q#goav%w)Q3amv~Yn zX^cR7REJKd7SyU`JG56=v@cHa*4;I-D#d|l|6rPmOTN^;eo=~4GVb6TMvxQ%^7nbT z$bcMIW^Cy4956-Blwky)A7Xr+3jY3DpGPy8w8%Wrj&NR+oIq*@bL0-B7N`6q@dGm8 z)luhlEaOC|P^{=A*l+T04%9cN$$D%$3JQub|A&9~C|FMRN)wvL$9>)$W-bd2Co7Dc z5==^nY|Kja&KR*Hx0WOd(AHjT*>yU;p7C=r%quvF{%4e6}*EbTC? zde?`r<~&*)EW<@)Ewx!%1^Vp~z+%5x@!AXcXTtPWSrY z=)s#B0QlB6kZEf-EBu4?+@>h+H)@ZWC}jM0CA=2kY~|jjY$MeEg8XwP(HH;+F0}qb zJPOjzJOCub(jM~KR8G(W-fidGYi?a(?81JE z)!=+A+q(b9GNk(VZKvGeSFeJi;f8l`F!BRwp8?R#W`abO%mv*K-_^N7a?)J})X^w} zA__ZP{3M#FBvRt`?>ddBT;TK<(L(6G0yh<^!VGfCe0d%I z;!1R!W-IXZXHhk9n0`DpD7tSUTlZ*&G zi=BwQNIaNhEQ9I3BPAGAWiF^NHi3dd$SM;l;)~d5rhHoe zq(SK63H4he?$X*=5HfH~a2t$UKpwm3DwZHTa=pYtja`9l02O_(aC|b7Jf)oOw2UK^ zT=y<;(PDj0Zc-GJRsH2nGR(4AG_f*k&SW*J*AFJl)4WsiX=IkG*bBrsblWUsR}cbp zj-Unz_AefqPr^zKFTRKi^F7Y()NE`8ftkoXrbV_tqQV=>Cgj737Lsr>t(7LaW%GX= zvLL}zbxVCov^X~Sa86cWNfopz`fS^al<10kyz)9cnOpRtqU_6vQpD3}e8NFSREtas z&aj@!%*cbE2TZh;1A_sukY3-uz_i0|akD1W0WR@CoY?zf48-i}RGEU-MmJ+25fpM|@NI2-6I0d9Ej{GLgt#41%C4vThW zCWzTDuv#wDDqn~sR?0&Bt`!bWMr-?mGz(;KAiOY!%JEzp6vQAK=pFKiJTBxs)K~80 zjq{BL1~NlDgvM$vR^P_ZI?`%9BsXeUDI`g~Krdd6wsI+zd%~i_kxaZdDF7FjtZ)P5 zWnW{bC1Nx#`%3bFlhtD0_X58@r6U)(l+z{PADf(fNYb($%r-{cZFBelaV?kHsA1d8*TXcwxh{&$HWq;~R`l=U00nK& zvP?GzC{JQJ_I^OvvmkcP#8ZGo#8X~elqd6QF+<4G^`QC;=zY47C-ja$2#IJW1jfD_ zQ<(0rZlqPYuj8Kl6*yji&68fdG&WgrR#j)RL=8WteO|mA6C@xhmYaSqV5rhTKm(=E zWXACRC$L(?-2VsgMlXTxVL%|q{(++T#;XuLU z(BlfcAoPHVLM8?TMe$mQEAz-uq30x^yaVl4Y1;5bHi8~e(5^HKz~tLlx(-ZYO=i`2zgCg_ozBK5v!($8p4C_>Hrd*^4%AUg#Jj$?(q{9 zn8LtIhft z?_&xAlXkVqHPoJ1TZ1#>phxxF8t1Wn%g62TuaX(qqda39vooF4b zP#O*`hlab(;va(aL_{MF>?gkj@~XWiepWdIP+)y0tS3VeH&)t)3nJj-_iW6AMx+0& z@Xb9=@kbwaB&6s#W$rI0X)l8Ce?`7;I2zYgzB)*eX5TY4aZVucnHM1b4jwQB?2AC> z31<1FKEw(<`T{CPB~&wffJX6!5!`aNLUEbDsFGza91lFe(~`Hlz)t8UX%2ZK+K9X+ za{{hgO5-ThJI}i4Q?w$+AMvQ4i<0M>K%5jT+(HUeqR(YdJUbaUjA;JU>uhEz`l%}9 z9W=y_cA$TW@zklX*yX?>2bsSsWG<*5vxc@w5?IvCvbxmbWfq5v;M|C&dngBjFdGqa z5}KD+DT&}R*z*Wxwl>w>;rto^X^d5W2ghx#e=A`uBryPZi*UUNM1E&> zH*jJAj1MqqKXiLA;45Wb_w3FZdvY+IZ2t3>B%`w3Ls?sGo=CxU2?zQb5s+%#*DOKJX1j%%ByZUDQvY zTsK~nq)1a_Z)C#`{B{NW!cCz)dW?qyFGuA&iI$|v+$U{VNpX7R=7A#M@ofV9VVEoAZ5A!PgYCh)# zl3L6X7aml6Lrrp@h?Zjv9d_#>C?6g(XOi#|WP{Ibgf5@*c)lx49{b*8*)-b{^`@yl~j>F!@#X!x7|F4y59agz>gop7AY*z_J9 zE`HXW4v|vmfF?!|ByKi434p=-{kRG~0V-Br;C70RWlo__JXAXUDI<<3e}J4oOh3(q zm=vfV7l6z+8yG0+hN>HW2m@(>JWsAao@rtX&W?Jmnt9 zvAeWTnhXyCEVuGf5ApVVzKS>)mSh3iww{SaVe@KrUDAuH`1sJe%i7*VQexmva#{yo7ycp3!w0jjd4~e5_!F&vQL@c0|bulyO)yku!IR&VPs_t z+<-D^jMy+Cdr0=SroxwYyOEc9_W$~-+9;=BW|2&eg1D`zq%lOKVw}3GAm~72gv~uC zRI<8D?TTS;c=EPlc6%8R_3$Ek3Z#dlAbfp~%#1EbX|;H82=LeKaS z@OEGV5PMWk@)FRfQnZE)pvl}_gEaiTWVYdw?ruKBz&?8KEE??#=cGRF;A=4-^4^Yf zEY|co?`rN454HWf{yObhX!G16$#`hOG$gr*oxA5VG_!QyoC8Wn!dDTHp7Vi@f&l+& z8Tth1=1|TaxYDd*UBC`}>iTOL^}JshyX|EBbQB)I+}Y<2xaaUJitdPMpn;>D@k*Sm z1mk;2;7S%W?qq!iSn6%M`$AIU_H6+)zf0I&^gOsmCW%%!yKqYU0s=CE(7qH5Z$C6O zi8D;B;x5f(6eJq_7QTk^_{5|dh)}n7iYSx@Z~oB6im1zAdDTo>axf6EQsCCUImOXe z>fD&sI8(krDx?E<#k-l2N{o(7)>0a2v{v9Ov;5thGjNn0o;;ya>*cN8rKDJz z`awq8Fg!->;#BS(0sH+`4DBG;YNFoSKW(gb@Pj-czv{S3tZX*_R+GLKF4E7sFUO3+ zXlCQSE+Jd31$xm$SyGUP6TBLVg5Daiu9F5h(a%7-K#Q*G$5hY1jUc8hCaW@V=sz8o zo!mzJUmag7-?5{QBzg-D7!PSq!#Iq9wH7kakY%XhhPosCUlA&bsQ_5))XYJ-(3BVrVA3&0&V}am1IJell6K$rrIr@zH~d=jLFDPr5DCV80sR}3|r?eMdP0AgAgDHIe|i4-h#+FKo8=Kn}im$ zIusii9E}?Kpx;_(M3Gg(PdO>n&9cUkc5Nhh@D9p+&WW@yt{QG(qI-TFJE|FLyo|>e zDlYb_{=JeasN{y#)2HRBIx-_!`2HpGeZ5~^lm2ND$G zf(Hg^fpe>tuBs(Npvm3NX0qFKT6{QbK!vIT>=@RDe2PFz9GVLg=vwxZoEawUf;()N z%p}b(j9OAz5kE-&|Cp#wleQA|yIhk&&_XgwcC4Kp)wucwY_5#DB=!^7E<^D+LA2~vice^arz+k!@9{0k z6a1zrI~cvppC*4B0`@W2s6WweJ(aG?ewcCW z%)Y1QyJ8}s6=>@eZidSVMBG-C#h3+H6UZ;oR^t~k25Yh12cT@v4YCNPv4s;hPB16`UKG+C-R zXzHD!r7K%uJRoTLY9 z-Pljn!GVpsgcK^SW2Dh`tRDaXz0<*>y3kuwq7d;XvOW|T{YiB9>XaY__^sF!-SAK0MpgJuRGt>>3T6k|u#5+%V=0$PxzjG72nG0I~3K#Xj^Rw~~zR zrkKQgNAxJ4FsE@|JI$@sNog!TuLOMpCNM`@wY@m#q|=n!JT}bzAVs5{!sEf0%#PIv zqzRKeGg`gurYsd^QFMBMO-%^Yp%8AkD9zV%`;M&YFsFk8;7mmOlCI$s7Od5u^2Tup zV2l-Nw6bP+ZXVcde&>On*q87mukG{3+v(zGcY6fg$Wr1f4D#Iayj5(h+6Aw?EAGO3 ztjzLvwat{X}XgAx^K{x3jpO2!e;okjYJ@|xLRiAx-kJPZW}$T)gPBEbZnld{vNA4R>sr_NSaC^0+z`5ENL+hnb?MrYtZb6L{TJW&U-16v{W$0S ze4f`)_tBQKUJ77u(296xo?`ptUbo={4Kr2dH3%cR zjm^6`LT(ZD^bYREw11DXP_J)Zv>ZuqZrV<|oGG_B&*yw+<@9`LyD8Cprj?FC&|t@Z zw-g?JaW>x{jXBsAY7;4TD!58mZB0GVi`cksR`wC%8pg1|&2%Od95W~)T(TwrT@YE| zckjGgVXs;qCTI}siNZ~bF&`D=9p70^h zTGkDXt5b$>Y6u6laukxwtK60mYMF*8Popw>b4{x=?~(u;c{@sb!Lm#iXhwN5 z?8WQmNW3=HhoskFLpZpTOcLDdoNl)YRA7JDH4sFf48Yvf)cW+!F2@FYd6O$FL0ffE zNq)REL2{hrQX(o76G9zso^$CyV1c_H{kL)gO*Y>DWj5#Qeb-n!lqz6!b|gGv2(iBQ z;4sw%K21gpq2hlNKiGl-Cf`-XUn+BponQsWieP1ga?Y^W>dOZt=+`S0NcB$RVgt$sTEsAZ5>B!2P%omKJmw)#$m(rR30sW}n}c&|?K0ox`|p>| zjht&9*z0X)@qBDxD|GFSZ2x5B03malSYo!zzW;cQoaE+=)o*C#*M%-ZnVf}>EY zG%l&aElV)=IdBtAQ*gLxCsx1DKJ=Yxt!t^6w<9Clm@;@K3@&LEO--v|F?l;4y*Gt} z`MZ9yWN|ifLPZUo>+-6cEWv9n<_?6p6&|fXzOw(({5Pc;`lww5dShA|$0_K=bjiz4 zM8_;J{-+*sz%UNEx5S(5X`a3LVLRl^NL;{9cHP=NZ_#5`0-M4kgIw>H48yBOQyjl~ z0gyi%aTAx0Chkh-x}$4)r8D8E4+{D>693W!(_@QOwB7!@rK$k+D@qCDokYV^Zpoi2 zpZkuIS<{s6gxGakY!_F!Blj@9$xj9&>^7?0<=6#YSWmt5gQI*)c5l2&dw1OjfgM)8 z#L$z^EM|ZzVWSlb6v&lZ`AbW~?DsN79XSWRQoZaESJY_27$9FvHt57xwWpiRJr*9= zo9K=Mi8D1_KjN+u=Ja-`Ub-cX4b|%2*_QnP{Q_65W>d{%YkMj*)4@rTTBAk0<1V0# zhDCD+@VtaZIptijjRCk<+m{sHzhNfBE1=m$BL%t~Z`~Pu51EA&zXghgbCmLFQmXP| z8SKnh1IKvF?`#@0{=6aJ>9DCN#$#(t?7eTtF|!`#|E6Xyk@pu6_NVCPc?4>&S*&?o z?DVyBcE^mf+Dc@~luZx-KyE&050m;+Vt73GL`!Fst6n%R)W6?ei#9OQdk+Tft8qv;M|Z|S(3My`=N%jTC7ElDp+H!Bi0!z^vz ze8$V2dmV0I$s{)IMUqE-o89AR3muzIN`-}dEI_?NNiShLjqYz3MZsLc`kpTj_bsSg z<#qnh%wss6F^1>dxnLPO{BCVTFq-yz@M!R%KE>cGdkXiZo#yxQS%V@t(27*uU-MA(A;>ANjtQY+i&R;drB4*0b1M?ttq~07_hr2rh`B50 z0)5oSqbic=eXtd3XDgj0DFGo&Zqxm)Q>GXpl+@bvlJnMqE>N5=Nrx*_*!t(*&!3Mj zkp%w^@@mI1a;ouScYDvOWLWxM(3Rr`Zku^^Wc>Z_JQzgNZs)*x#1nBaQbhYQV#C1B zbuK(c3~fnfITw0Vp<_ZN&=)zoWzay9TN8_h^7G1mn8s2@isMZewQmRuC~n)UTPqnM zekjs(H}liO30#t-kC-#TlU!FZy?k4 z4dI98vvuUPLrL@Rbxe&{*&WaezjuuG{%HZYQAFMdPVTa|u zqs~=sKCPS2V_P#)f{UH{Sf^{@TG7RghDnv0b&JvefQApR>ZgnBPyDgzxixZioajo1 z$FYvV1G%%UaE1+4msBw_VZip|1GzZs6O)HA3R(@tPRoA~q3qUD6Qp2`B~{DV5q0aw zbjMtC?Bz^vYHmm%Bn;Q`>8FmO!x9CEGSLNzJXzSOp;QRYA&3}Q4uEh_gP$t82f@F8 zCt~Cs1+BCo)2Vf-i$K2GquMVX#8gn3v(F+?v(yY&%=n zh1ltho3#*w1Js*GTq;Q+s4%zOZ-+O=l)~3z;wOMtL&crCsXx~v)(TGhD%Es5sVB9U zbu9ZBj2R&C`p8Y_!7>Ma*p0&OtIr&7>$!U&Yq_=34oRh!zkhI$MfpZr{e94Bj{+ zbM~7kql=7|88h6gR4Qm$e-2h9NK*}z_3IHyTX%!b9%@Kf?6JH0nS-UcDJl%|VPmIXB+LAODQs%c`@pAdXf97S zH9teP<)G*v#|5C@eW#{a^{umqY~EhAGBlhXo~p_kyKX~Jfrk;m7e{3(%BHGqd+!u2 z4!*@PaXi0UpAV-}*k7?VWJp7|{9h62Bwn7P_5Lz;t#z;prey?iI>RZVdTs7Xlu+$$ z`_G`I_n4gi`>~if_6k7dt9%B|#KIE2L2l{HIv)b7KzH=`A!4#|`CY#e{W*or zSKEa9O7n89>y>|9YhS5>O`Jg@_bAFb5_vf2k+FYAf)^~2CCGjv<+$CCXk-V1T?G9I zrtWN;&#!rzU203Hpt_Z|Qn>5w6XkFMp`I`H$JVD%$fucCt%pYK{O7X9e^PXAN5$v- zu*llb&a&003dRKFGnNAGO%Jt_k^Lo-8j45sd(H-8za6;;M;t!%;kG-zQrCEhS%{BdYJ$x$++^6%FC_P9Qt*o-ZIF0JM ztO*m>h)^$RZgZQX0o}yYc1ZCReBW;tV7Yu&q%BQ}4?9~s2Iu%C20}|?XWZ*wbUqLY zyDLmp6but~=sc3k=D*iWl5?I&+5A%+$jL-LgOI{(7RT&F#B3T%MrvidipE(kG?fl) zG>o*<9F*b}Yk&A)6Hsg7Po-qB?mOtvLSgpo4yoDlw%Zj~H%U+1)dM2T)V)C6-EW{0 zW3lSgu3$_9W|d}ry%kc@0$}q5cSdKBig61`C?e zDyhIhx|)l;ZP*!iJ34T*y+3~iQMhtFRo(^-#QE9? zh4XfA($!1nD4&-ETs`%zJIw7puYM=qXLy{2z@$yak$!s^+`K0&7)0N; z)%^0{_IUC4-;LXR2^F0maQ$C0*K{mT7AGVtJ|#0^6zC3;yiY#S>|N378C^*^8!hB5 zd9Iql*z6bTGRaJHJiaooyB{5@Lzb+x?CWLC@_eT_gStFv#EDxCT4H@t4$8Gq7`m$) zs%Z_|6G5iBz93;HA8`&IF@sAkA(-AAU4m?LQM&dij4~(Xwl1^rxAju>6tXe;2G8h< z)Og~p6rYwDOTfuoKMvo`wQ^cPC)^1~IW!uJ6T2RK(<8aNnY|-+Wo3R?eRY8eFy*I7 zn?Pd-p|kv^CUY&f^bh!*c0&|5=}A?RGgxBIyli9e8JmrZEd2adqqjtCnO8^8X4{_? zid%#H1Eg~lhTPT~9dKW?H5w_=5&A9bz{3C7kvY~xXGC0i{z7zopBYi_WFULFFz+bL zPF4o%J8^bF*7SPHs#>06k`pJ?M0#B0$mo+PQbwgDbOk!O&>e(|w04C6i`!*6JGO6e zZVdS&A<0v- z*Oo|-ZA=luv?iRdLze&CaVhs}%gW-(Zv7(8o8yKjC}t~|*@ycFX>Lx)1Qk;rJ>`;T zl{7KR!d!3$2@K**2g>c~o>MrBo{HrG7e91z2^JrjK!TZmFxf2TQD1NQE;EXFsL`BEQM~`Zfl{}!gYVd0)h6* zV4uT^f1!53A#M+R8{%JiO6FSSyI2>gQ*LKp2{YW^ajj*{D#8XdN7epAAb)IH0V}1> z?pM$=p7&*FCn;e}Ec|;V(^o5In4;T>JSGo5sGd)&K4D7lDJU(ueiJnC4w&eK8s~{B z;(Z6256r0?cb8{L8X{z1{I0i9(&*$33^s;ysw?dEY3# zNo)8lk1ti}=$djyPwbY5(wR)p>({QyWbh__XUoI5OK}e?u^hhg#^#~Ab}Ry`o;!Nj zi|2z3vz)t3y7t<_OSvI3#;%Bz4GrZ-l@;7M>O+Ynxy@T)i-GPL>R-rc!*O$`t6z16 zc@cj7FgaA;X`j9EaQ&*%*?NBQ%ZEeUr1V?wzH7~+SD-~?vrn88dL{InqI{e_6M$U+ z4G8hD%3`iEMHD5I!z*!6M&*kRq%0h8l#==N(h8PNGd+Ue{JV_3yI^@@O(MIe{3Fu}EjI+LG>670Bj??XSI zv30SE3G2S4u3qm5YQWIfRjiBb5f}h76jNY7QMG)}r}`6{{gN9&fOAF0v8!t{Q%D^4 z6`NwUz^}zRhRS;HE?<+n^3;W)-lOl7twasu4e=lK{H%RtL|%aOvoK2l>u`vaz9z{& z0lPs5FG&&;d(~o{dBgvRuc41+gsI#ng)xi>c9igyz1;CRBzi%N*gdJ};e$cJ4hRPuIlt|qAAWQIBbPim5WI1YSzHaU%B`5o;G z>hlTc4Nd{y{VJ`@)_WS*D#20m>|>d+cpd1ejmn{(>U#mY;n$z5CpWEWHY~+=-&EOG zTt8-ESaZwg1s?R{FYQ2WM%4h5TPF3n8qZthyl@uDptAA%3Dmwz}dl@ zL!7(bWorGJ95*gPs{wt#!CEq)RsrEe_|u4Dv932JGR4^Lnz=Gt!kk_Ft3?0W!T08R zr1j>8lBqgM5TUWk=4TMJ%lwI5px|jK?SI&TGW{!sNlW(OTnV%cv=7vrnHsw9l9kT( zX4b{%K(OTNSXPi-sA^m2RPK(zK=){-i^tZv@RMx`%s1AZtukb147x^<0}j` zw6OGgizL(nUNc~f^|7bm;{HJ=hn3|~xc$Ikyn405l_!@P!~Gv<5_blXvs_lA)?c>Z zQkBj*!$0m-<4}mNsUSCHvxiT^ke|Qqm*&d^i@3JMT zM0=cgnEE0`V37%6P?)14%gjYQMc6;sXk1%EahLE&W$?3-0-I6j0%O+Catjk4$5Gjg zpT5>Qqt$P>DNF-e$}0N_`l>q!LLi`u0=8^6Lj{YJ!M1lj~fI;Y;{_0eD?nDZTJ?Nx;3Uia#l7zr$Ti9t_+ zVIdCfbnyY`JpdPe<83YHsn^OE7lTgV@{(S$ZH5;_N}J)46p{ZdAGM-m z&|Yj;{6(q1`M>ek%86MO=Wd#&2Ms|<#T(yus2&oW%Jo{*XDFGh$ zpF{zikeMHOlcFnXtm`V5xtDkaQ`dA^ z=1K%3pH!JTqBUkiqVkQCM2?A@Hiato|)nQ%K&3W51BQK9j$$!nz!ji&!8E=t1ztIk~6n>hn4>jR#(46k*(V*5eY z_cag+5z$%AV8{k=t?45E)N2NYwOh=otWibDPR4164WfbK)DDw_()2q+zf^%CChJl6 zKDdXQks-|aGQ$#x@6g` z;6F9xJ}mYqkS z)VhHXs6!7enS9gJjHw1QT)whBwt?+tF~h(jk|Y$NuLVhYRVpp4i!OE!u44feXy(9|pJ?soD{q*DDO zi9f?XsZc`a^GAEh9JuMPOvNkTRc@6~IB6*9GJ%`y)bEvuMiYbiuHlBDG7YC_%S)+Lnbb?i^!bgvbG{dCD zf%Cbxt(95g&-iY#x;{ggt-VL*+?wvZ-d7IBeZc;q80}Ortq7+z;7(b#m zu*wCZItZRlppi9sWf40?NK4!?jU^Jem&)?Ir4iC zZV(}HbGd&KJp!?ZOLx<8E$B&}YHXheUSQ`Zwf}^kmp;(TZ+{q>kqF`%gtcFnRYyFf z4t@5zwJDpgubjk3yO}A{&3&ek6zT%fFiB&1dY7}gyQ}fHt_xxQxv1w)*_z1%a_9G5 zIYTmjsCPIk-i7!(2duBIM@7~!T-ESjx{$ph7K;O|ot+J&XJ>$tL`B1E34C+AT+Ioi zdSWQjIES0#R028; z4PvpuS7qvH`()+W1E5L?%eFJs>yeqU-b5)sr;^L+q;HD%cNBpQexQF>5PQ_|>ZLzn znHp4dxi7OqMju&0*LKX|aP-Az`*IZQPu6diy(33o4e9|WT-^V16+))0uwbyWN>f6A zzL<&IRciif-e8e+x2q3r3P(Fu@RNa*^ftaT@pb-j&2mFy^#04 zUX0I`<0TJcrS-tWM;AS{U%_^XDSVvhEr}n%fo;l!3-$Ee5_h6fj_8@RSKscZUTs^`i||Fi|SldFqDy3TGJYTx_^Sxz7k4V^k? z(R2Rt5Ri^jLCwWu1MTM%48j*&;Q^WA4@plM(}mS*wuIuDM)q=0%o8mUM8xl@#%%5=z74C^CVYWA`lnv#M3dl zMlODDFv(rhdp!nlBDNd}ed8V^nBeMlBin$I?w2{^er~1omvHi+( z=#9YDVeLbY9&kpvu;nvxMvQJ;76Om@qqOHs^Gq6QEp?r?ftr;01kdDS`o-YCm*j0$ z9cU7uO5D*Vbl4r0U9XkHbt5F0D;8;-&EYDaq-@#!<^Fn3ksw{{_U0DgXlnUszwwgd zz9vEHFMgKM99SqScO{M6J@MVm=Xr+lRQH)SQcQ|Nh%5Ep?gC%pAW={lA?4$_E_Caz zEh>gBzlsOYAlZ&_6)f2k(Z(xZ>|D6cUC5tn=Vy1~k_avLr@NlxWqis6FmwxD3cFGA zsc($d?Z_63O+_(=OQGr}`3mV(^?&8JW8D&quGLiDRzJ#OBt)yT9Cz79ru>7W=Tqf> zEewyjXTTyA{mnmS1HWwIpI_?$zmVC;Lqs(sK(4g*rufla`7kZtI{0t4cEtXgNB%pz zA4q?BdZ;9)hEE#<=;sgCyL^v&;Y61~x9GoT{gGmpW z0*a;BvnN-oK7rIm@!6vq8l0KGZwP!q)v9tVMy?GPZ zaZ5YuI=4h4-=8IpaySBYLtZm(R^(vrWC0@>QwG6M;i^CKvF9P?LL+W+3oV`nY>Orfb~pv3rb)^^9U{J~7(0 zpV=?x-i;rebVaPNjG&arYG{8QBrjng=d?pS_gu4awHlz4FQ}GTF+l;r?k9Cf+>ucp z`00+X{m}DFzW3XnJvZm!+mh4Izes;s(4LYkyfCx;x;F((r3JrF7^WoFggD9W5&A4$ z9_A}P_RxdUd$=E^&Y~c>g|OZYZ2B%s`AOD|!q3aZALRvLdCaiLMxCfPV%j!(&Z)JIffsEn`aZE@$rv+-RRicNp}H zPE>@rG%e>v2P0gfN+za~4e3&bolZV^b@2~>v-jLv%SmT<|nL1sHw&shOoitsL_~jp*V&_9e>{dh>^{S1x z?|9uUwWWh}QAx34QO7awegQDBb?BQYljs111I+&NGpa88mP~uZyq^%3N}Z0PF+7ub ztx=8S1AM$j%nwe1Z+{dW(L^zk1?P^6U6Kt5TNyxRt`TPQ?|AHA%i#<<%R(vRkG~l7 z&oFm&6iJ1du(BXz_NHH8m<~5mBZ$-NY1g<-9Gwrw^w5A3YS8~Mq2G>f^k|)1I09?^ z3RVX{DPND>G_K&2pKvlv?&%CLA&hW0g)ydRhbBn>E{vK3^Jx#wO|2p8zghNYb8Pem z5n+6LE(yNT_D?k4J!h}s)e`f`M57fYBFJ-SvQBuvwN<}|{uGGdU)+nl$!wo0$>3A5 z(6^8|Q&v%%iw<7PtW715oTgTa1wN?SIR`lq)_{nJuDV?#AF>%q_+-a??ertv=4>z; zz4s6OmpDhlzf|aa1K%55=+#Wq#G7gBPm*X!HjO2 za)H?g!#2l%fv*d`K&@LQhWl6Oz@M3TYmAwpoX$23t(}Z^M1ME(`YRhfT$oRh^aJqo zfj2St`j}~IA*IKS5_S5H@>tuCisoA zu_|9QweWgh0J_JRVDlwIG+xd8x@V;Ry8}^-v2eFTy z5$-BD6a0jEKU!Xv** z$_r48y+@6?x?s>hZZb{$xa%n}!xSnvJS;Uj@JfC%L1J%vmDIV7O{8Kq?#kD1 zse0v~cVRv(~+%Jl&0cOW6u&*OpY&`!Z?aW&EG6Cud;~OXdy4QZ_C?J?T|Y2TeGb z`_5=-U}}m>1-$Dp_l5sGyRR%3H8zMv_W#b3o^(V9y*w=4|IKVxfU;MT_4I~;Em>R zt;tlZ@(o>h@X{vo{EzG1tX=lVvzuv`cb<8-wl8y4syq}%ifC3HogFkk4^=!dsHJxF zFTDEkD|#=bO4zXI4x^3ffO{rH1bc~UUB4)e3UzAy3w}6X%xC;G+lGmga~Zk{l5yFuMPw;p@MJ z8f$U-LFPu<|CIB;M@OG}P?U-DGd|J8mIbT1J^E%x-mSaNG74lPIX_dytq|XV&9>iz zx^3Ey!iD|yE4%Y)1)Xnue%b*=SlZJoLwz~KTqm8(J`t%nNi0SPW#&5apOGj$!! zHC=>W%N2SrtiePsxeuJyw3I#{{yJxlz#zoLUI(@CZIiE&mDb-A{nW^K;g}8hKo+CV zR8G;PIWj$c`X;5<;}^j*%Ad1It#(74^04!xh(H%X-wwoQHkrGX?oibcczlN8Z=;X5ih~9SD+NXH+)6nyaW}jMb%DNmqP{-hN^=GJOm^_}JA^3y= zd;Vx((G({FKPwq~Z)aJlSKEq&PT+WWFI{8W@d`eTVRQQ4z2++co=YeBNO*_+EhPsq zSyWdcrqNMF689HcydryXl=0;Eul+d`h@@rfHvx@_HEU4xaFFeaFl^#fDmdq;7jZH! zqK}|Rv`I>Q*ls|_MsR0)2(lHp|7zKo`l}p}rOInY`p~T`x32s$p?$r-@Y+N5I$}lk zAI5phZB3y`nx2(;m$sBCr|NYQvd1!CoR1|p_~iO;E0r}7c!9Y2Y^b4vD{qFdLP0S@ zvnq{H;}z+_yDUc$mU3}(9Mnpx>*Y)ne86PGZuFDr*=7WtVyKg0>3@IbjJh=Y8-7}J zG(b4O=b0Fm%odbS*>A1lJdsXjs@GGCM9 z`KH9<%s#3=+e5$rqnyFqR@G4UAlY2zKNc(KYg>s;iR_sf#jq*s4%q4^9t5(@SAYS>RF)aH)RZ}aBy4T^6YIBA9Nl45?#^9jz+d>KYnGFK+?9) zt=x>x?hCU4g!qb)>A&#ght)YN|AIWGmoCMo2jraVEZWB%w{Z-9ykLQ$Q7|_0FsAgL zsbCj)@y>nKw*Ho^i18W&oEm*ZJIyT-kXdX^q}Lq+k+RNYsC zl3}AbVoK-6QI{V6>8`WkOx9pZmNL0RL`}!dtAzPg%gN98mzur79w+^dnU95)Z6!v1 zicsYXGR)*gu^J@!oce=lPu8+GYaOd>@U-G z)g)Y6ZBiRwEZ7HYCKQVVFlF8iAnUxVnzKGQ0?j#R$%mly=V0$#XEv|Ha6IFLkOaNS~-We2BUJum%2Z%^OtecsuhP7 zuMairDD4srwGz4gcMH<}1%l^9ypWkqwU5DehW%%hDoicH56gl*sZaO1E5}W7g~s+N zN&VNklc4{;zH1dZBc4(DR+#4e2fwvd=r8%DNApUgU98bjf89EPy^--H@n9Fg->z%- z+fGS%n#@($x>bh>N7eGk#NAw6=^%WJ5=Y1Na$a4I(fR1k^;^d7-YsB$BPO`VLdIV4 zPU#XpH{;`<(txY>;fNV}D-eEhiL`bjdKLV{ts`y&EwY?6u+n0wCs#9w}ag3kSvB?5hKVLa(6&#j&UZ2fb6>4l-jV$>>_&j8H z*_j;$@JasWBrRzXTH7)3#ovg&u;iJLfxTR&$)Q;2y3K;>ep}3RQWJQ$aO}^L>!dY5 za_F+4YvrYNfs1|ANa?~-C-8T-rby3og_BUIoPm?GW-qOqA{}i+SIfz(Q1w!Uz5)DR zm$J)1&b$+90-|TPF14`dC?(8rBF3xsx!Y=bb=ami>*gozwH~(Pi(EB!J!-Uf{fToGo7l4`Q7-rHReGn<95&svfpR^PBWrJsB z0db+&pQyejnuLmNH8%{rs(O8za~lpEZT6;qWn$tgPXAakX9`kbenVBB&Ue;u?l!o+ zJ2%J*2Y#6Q8ia7-mDRkTB}kIJCJ+31b;StN33ug_8r?1<7$bOHm6exUUFjbraX&2f z{H4<{;Tg7b1>ymBpLqp%(HLhuI~RfR?_X?)i%Qc;k60lOpUwI=xE1}aE}GiH&>vMc z&b2wbQst&u%ae2LrZ!G>vVcOP8f0oAsCuZyPh0F6*G)~F#Iw74?ztZpyvr-1V|VK} zKK(~Jmv@Oe2<+c|>O*h_7<06h<2^6F;%8CZqE&fa6RJuGm^K7?S1W5ZL1hAw~^+S77i)JDs@D z8=xF(mQvnlY8#y`?rrf&h*6Ei5N z?+}Fn1@~Ta&yQD~M zJl~ryg61R#`q$XVd-?{p%)V;0R(-)<#L!e^5MwOG@-T|c^?|1IqG&?sRsr3(&&L;l z+K=M?mtJ#$jV$Rl#2GA#&pHFOKY6LQK6_WI`RF76n+iwSe>(sNGlMw|LKqH9KNg7Owk8Q#T&qwm#lF z>*zQa^H}<+GIL)a;QK)xq{4$@8qX&}Q{>#hq>}Pox4&nYWE(Wu>#hl&T)crXxpDN1 ze!ggB>~Cw*Z5*J#_deH_G8c8JyWe1SB947W_l(XXVZU+h^`oK?e#_m@V>zxP51Tfj zUgZm&$^GhEjQo^$Yjv7l1*!pA{NWVEO$&k&;}Mx22ewq);IriSUhTIVsG?!?VrQ=6 WPxjID^2>j>0eY_Rl`tJK)ri>(Vu$_Eet literal 0 HcmV?d00001 diff --git a/tests/saves/pc_sys.bin b/tests/saves/pc_sys.bin new file mode 100644 index 0000000000000000000000000000000000000000..9470b4206b47ee504f26745d2997dbc8169f29b1 GIT binary patch literal 970304 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 zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHgY(+(X7000002=ceyV1h!(fB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 bV8DO@0|pEjFkrxd0RsjM7%*VKfPonZbcq7r literal 0 HcmV?d00001 diff --git a/tests/saves/pc_sys.dec b/tests/saves/pc_sys.dec new file mode 100755 index 0000000000000000000000000000000000000000..30f8a6817d2bd52408398d0eac6340c62f40baae GIT binary patch literal 970304 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 tests/gci/save-charfile.gcid.hex -hexdump -vC tests/gci/save-charfile-redec.gcid > tests/gci/save-charfile-redec.gcid.hex +echo "... re-encrypt saves/save-charfile.gci" +$EXECUTABLE encrypt-gci-save --sys=tests/saves/save-system.gci tests/saves/save-charfile.gcid tests/saves/save-charfile.gci +$EXECUTABLE decrypt-gci-save --sys=tests/saves/save-system.gci tests/saves/save-charfile.gci tests/saves/save-charfile-redec.gcid +hexdump -vC tests/saves/save-charfile.gcid > tests/saves/save-charfile.gcid.hex +hexdump -vC tests/saves/save-charfile-redec.gcid > tests/saves/save-charfile-redec.gcid.hex # There should be differences on two lines: the checksum and the round2 seed -NUM_DIFF_LINES=$(diff -y --suppress-common-lines tests/gci/save-charfile.gcid.hex tests/gci/save-charfile-redec.gcid.hex | wc -l) +NUM_DIFF_LINES=$(diff -y --suppress-common-lines tests/saves/save-charfile.gcid.hex tests/saves/save-charfile-redec.gcid.hex | wc -l) if [[ $NUM_DIFF_LINES -ne 2 ]]; then - diff -U3 tests/gci/save-charfile.gcid.hex tests/gci/save-charfile-redec.gcid.hex + diff -U3 tests/saves/save-charfile.gcid.hex tests/saves/save-charfile-redec.gcid.hex exit 1 fi echo "... clean up" -rm tests/gci/*.gci.dec tests/gci/save-charfile.gci tests/gci/save-charfile-redec.gcid tests/gci/*.hex +rm tests/saves/*.gci.dec tests/saves/save-charfile.gci tests/saves/save-charfile-redec.gcid tests/saves/*.gcid.hex diff --git a/tests/test-decode-pc.sh b/tests/test-decode-pc.sh new file mode 100755 index 00000000..58da3617 --- /dev/null +++ b/tests/test-decode-pc.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +EXECUTABLE="$1" +if [ "$EXECUTABLE" == "" ]; then + EXECUTABLE="./newserv" +fi + +echo "... decrypt saves/pc_gud.bin" +$EXECUTABLE decrypt-pc-save tests/saves/pc_gud.bin --seed=1705B11E +diff tests/saves/pc_gud.dec tests/saves/pc_gud.bin.dec +echo "... decrypt saves/pc_sys.bin" +$EXECUTABLE decrypt-pc-save tests/saves/pc_sys.bin --seed=1705B11E +diff tests/saves/pc_sys.dec tests/saves/pc_sys.bin.dec + +echo "... clean up" +rm tests/saves/pc_*.bin.dec diff --git a/tests/test-decode-vms.sh b/tests/test-decode-vms.sh index 2b95891b..27c9394e 100755 --- a/tests/test-decode-vms.sh +++ b/tests/test-decode-vms.sh @@ -7,12 +7,12 @@ if [ "$EXECUTABLE" == "" ]; then EXECUTABLE="./newserv" fi -echo "... decode vms/lionel-v1.vms" -$EXECUTABLE decode-vms tests/vms/lionel-v1.vms -diff tests/vms/lionel-v1.dec tests/vms/lionel-v1.vms.dec -echo "... decode vms/lionel-v2.vms" -$EXECUTABLE decode-vms tests/vms/lionel-v2.vms --seed=D0231610 -diff tests/vms/lionel-v2.dec tests/vms/lionel-v2.vms.dec +echo "... decode saves/lionel-v1.vms" +$EXECUTABLE decode-vms tests/saves/lionel-v1.vms +diff tests/saves/lionel-v1.dec tests/saves/lionel-v1.vms.dec +echo "... decode saves/lionel-v2.vms" +$EXECUTABLE decode-vms tests/saves/lionel-v2.vms --seed=D0231610 +diff tests/saves/lionel-v2.dec tests/saves/lionel-v2.vms.dec echo "... clean up" -rm tests/vms/*.vms.dec +rm tests/saves/*.vms.dec