From 0522b539c479286f399299b6adca554229a14d78 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 3 Nov 2024 21:24:48 -0800 Subject: [PATCH] describe DC save file formats; add decrypt/encrypt actions --- src/ChatCommands.cc | 4 +- src/DCSerialNumbers.cc | 129 +++++--- src/DCSerialNumbers.hh | 19 ++ src/Main.cc | 242 ++++++++++++-- src/Quest.cc | 4 +- src/ReceiveCommands.cc | 2 +- src/SaveFileFormats.cc | 16 +- src/SaveFileFormats.hh | 309 +++++++++++------- .../SetExtendedPlayerInfo.2OEF.patch.s | 2 +- .../SetExtendedPlayerInfo.2OJ5.patch.s | 2 +- .../SetExtendedPlayerInfo.2OJF.patch.s | 2 +- .../SetExtendedPlayerInfo.2OPF.patch.s | 2 +- tests/decrypt-vms-save.test.sh | 25 ++ tests/saves-vms/a1-2gc-expected.vmsd | Bin 0 -> 15360 bytes tests/saves-vms/a1-2gc.vms | Bin 0 -> 15360 bytes tests/saves-vms/a1-2gc.vmsd | Bin 0 -> 15360 bytes tests/saves-vms/d1-sys-expected.vmsd | Bin 0 -> 7680 bytes tests/saves-vms/d1-sys.vms | Bin 0 -> 7680 bytes tests/saves-vms/d1-sys.vmsd | Bin 0 -> 7680 bytes tests/saves-vms/e1-sys-expected.vmsd | Bin 0 -> 7680 bytes tests/saves-vms/e1-sys.vms | Bin 0 -> 7680 bytes tests/saves-vms/e1-sys.vmsd | Bin 0 -> 7680 bytes 22 files changed, 544 insertions(+), 214 deletions(-) create mode 100755 tests/decrypt-vms-save.test.sh create mode 100755 tests/saves-vms/a1-2gc-expected.vmsd create mode 100644 tests/saves-vms/a1-2gc.vms create mode 100755 tests/saves-vms/a1-2gc.vmsd create mode 100755 tests/saves-vms/d1-sys-expected.vmsd create mode 100644 tests/saves-vms/d1-sys.vms create mode 100755 tests/saves-vms/d1-sys.vmsd create mode 100755 tests/saves-vms/e1-sys-expected.vmsd create mode 100644 tests/saves-vms/e1-sys.vms create mode 100755 tests/saves-vms/e1-sys.vmsd diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 91d193e4..3ea11cac 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1728,8 +1728,8 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg }; if (c->version() == Version::DC_V2) { - auto dc_char = make_shared(c->character()->to_dc_v2()); - send_set_extended_player_info.operator()(c, dc_char); + auto dc_char = make_shared(c->character()->to_dc_v2()); + send_set_extended_player_info.operator()(c, dc_char); } else if (c->version() == Version::GC_NTE) { auto gc_char = make_shared(c->character()->to_gc_nte()); send_set_extended_player_info.operator()(c, gc_char); diff --git a/src/DCSerialNumbers.cc b/src/DCSerialNumbers.cc index 0c418f81..e8e7ed7f 100644 --- a/src/DCSerialNumbers.cc +++ b/src/DCSerialNumbers.cc @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -24,6 +25,7 @@ static const uint32_t primes1[] = { 0x313, 0x31D, 0x329, 0x32B, 0x335, 0x337, 0x33B, 0x33D, 0x347, 0x355, 0x359, 0x35B, 0x35F, 0x36D, 0x371, 0x373, 0x377, 0x38B, 0x38F, 0x397, 0x3A1, 0x3A9, 0x3AD, 0x3B3, 0x3B9, 0x3C7, 0x3CB, 0x3D1, 0x3D7, 0x3DF, 0x3E5}; + static const uint32_t primes2[] = { 0x3F1, 0x3F5, 0x3FB, 0x3FD, 0x407, 0x409, 0x40F, 0x419, 0x41B, 0x425, 0x427, 0x42D, 0x43F, 0x443, 0x445, 0x449, 0x44F, 0x455, 0x45D, 0x463, 0x469, 0x47F, @@ -135,6 +137,8 @@ static const uint32_t primes2[] = { 0x2627, 0x2629, 0x2635, 0x263B, 0x263F, 0x264B, 0x2653, 0x2659, 0x2665, 0x2669, 0x266F, 0x267B, 0x2681, 0x2683, 0x268F, 0x269B, 0x269F, 0x26AD, 0x26B3, 0x26C3, 0x26C9, 0x26CB, 0x26D5, 0x26DD, 0x26EF, 0x26F5}; +static constexpr size_t num_primes2 = sizeof(primes2) / sizeof(primes2[0]); + static const uint32_t primes3[] = { 0x2717, 0x2719, 0x2735, 0x2737, 0x274D, 0x2753, 0x2755, 0x275F, 0x276B, 0x276D, 0x2773, 0x2777, 0x277F, 0x2795, 0x279B, 0x279D, 0x27A7, 0x27AF, @@ -1107,15 +1111,19 @@ static const uint32_t primes3[] = { 0x18569, 0x1857B, 0x1857D, 0x18581, 0x18587, 0x18589, 0x18595, 0x185B1, 0x185B7, 0x185CB, 0x185D1, 0x185E1, 0x185E9, 0x185EF, 0x185F5, 0x185F9, 0x185FF, 0x18613, 0x1861F}; +static constexpr size_t num_primes3 = sizeof(primes3) / sizeof(primes3[0]); static bool check_prime3(uint64_t prime3) { static vector primes3_set; + static mutex primes3_init_mutex; if (primes3_set.empty()) { - size_t num_primes3 = sizeof(primes3) / sizeof(primes3[0]); - size_t primes3_set_size = primes3[num_primes3 - 1] - primes3[0] + 1; - primes3_set.resize(primes3_set_size, false); - for (size_t z = 0; z < num_primes3; z++) { - primes3_set[primes3[z] - primes3[0]] = true; + lock_guard g(primes3_init_mutex); + if (primes3_set.empty()) { + size_t primes3_set_size = primes3[num_primes3 - 1] - primes3[0] + 1; + primes3_set.resize(primes3_set_size, false); + for (size_t z = 0; z < num_primes3; z++) { + primes3_set[primes3[z] - primes3[0]] = true; + } } } uint64_t offset = prime3 - primes3[0]; @@ -1227,8 +1235,8 @@ bool dc_serial_number_is_valid_slow(const string& s, uint8_t domain, uint8_t sub } for (; offset1 < limit1; offset1++) { - for (size_t offset2 = 0; offset2 < sizeof(primes2) / sizeof(primes2[0]); offset2++) { - for (size_t offset3 = 0; offset3 < sizeof(primes3) / sizeof(primes3[0]); offset3++) { + for (size_t offset2 = 0; offset2 < num_primes2; offset2++) { + for (size_t offset3 = 0; offset3 < num_primes3; offset3++) { if (primes1[offset1] * primes2[offset2] * primes3[offset3] == serial_number) { return true; } @@ -1251,7 +1259,7 @@ bool decoded_dc_serial_number_is_valid_fast(uint32_t serial_number, uint8_t doma continue; } uint64_t sub1 = sub0 / primes1[offset1]; - for (size_t offset2 = 0; offset2 < sizeof(primes2) / sizeof(primes2[0]); offset2++) { + for (size_t offset2 = 0; offset2 < num_primes2; offset2++) { if (sub1 % primes2[offset2]) { continue; } @@ -1293,8 +1301,8 @@ string generate_dc_serial_number(uint8_t domain, uint8_t subdomain) { size_t det1 = (subdomain == 0xFF) ? phosg::random_object() : subdomain; size_t index1 = offset1 + (det1 % (limit1 - offset1)); - size_t index2 = phosg::random_object() % (sizeof(primes2) / sizeof(primes2[0])); - size_t index3 = phosg::random_object() % (sizeof(primes3) / sizeof(primes3[0])); + size_t index2 = phosg::random_object() % num_primes2; + size_t index3 = phosg::random_object() % num_primes3; uint32_t value = primes1[index1] * primes2[index2] * primes3[index3]; string s = phosg::string_printf("%08X", value); @@ -1306,54 +1314,79 @@ string generate_dc_serial_number(uint8_t domain, uint8_t subdomain) { } unordered_map generate_all_dc_serial_numbers(uint8_t domain, uint8_t subdomain) { - vector domains; - if (domain == 0xFF) { - domains.emplace_back(0x00); - domains.emplace_back(0x01); - domains.emplace_back(0x02); - } else { - domains.emplace_back(domain); + DCSerialNumberIterator iter; + + if (domain < 3) { + iter.domain = domain; + iter.end_domain = domain + 1; + } else if (domain != 0xFF) { + throw runtime_error("invalid domain"); } - vector subdomains; - if (subdomain == 0xFF) { - subdomains.emplace_back(0x00); - subdomains.emplace_back(0x01); - subdomains.emplace_back(0x02); - } else { - subdomains.emplace_back(subdomain); + if (subdomain < 3) { + iter.subdomain = subdomain; + iter.start_subdomain = subdomain; + iter.end_subdomain = subdomain + 1; + } else if (subdomain != 0xFF) { + throw runtime_error("invalid subdomain"); } + uint32_t serial_number; unordered_map ret; - for (uint8_t domain : domains) { - size_t offset1, limit1; - if (domain == 0) { - offset1 = 0x00; - limit1 = 0x03; - } else if (domain == 1) { - offset1 = 0x1E; - limit1 = 0x21; - } else if (domain == 2) { - offset1 = 0x3C; - limit1 = 0x3F; - } else { - throw runtime_error("invalid domain"); - } - - for (uint8_t subdomain : subdomains) { - size_t index1 = offset1 + (subdomain % (limit1 - offset1)); - for (size_t index2 = 0; index2 < sizeof(primes2) / sizeof(primes2[0]); index2++) { - for (size_t index3 = 0; index3 < sizeof(primes3) / sizeof(primes3[0]); index3++) { - uint32_t value = primes1[index1] * primes2[index2] * primes3[index3]; - ret[encode_dc_serial_number_int(value)].push_back(((domain << 2) & 3) | (subdomain & 3)); - } - fprintf(stderr, "... domain=%hhu subdomain=%hhu index2=%zu results=%zu (0x%zX)\n", domain, subdomain, index2, ret.size(), ret.size()); - } + while ((serial_number = iter.next()) != 0) { + ret[serial_number].push_back(((iter.domain << 2) & 3) | (iter.subdomain & 3)); + if (iter.index3 == 0) { + fprintf(stderr, "... (it) domain=%hhu subdomain=%hhu index2=%hu results=%zu (0x%zX)\n", iter.domain, iter.subdomain, iter.index2, ret.size(), ret.size()); } } return ret; } +uint32_t DCSerialNumberIterator::next() { + if (!this->started) { + this->started = true; + } else if (!this->complete) { + this->index3++; + if (this->index3 >= num_primes3) { + this->index3 = 0; + this->index2++; + if (this->index2 >= num_primes2) { + this->index2 = 0; + this->subdomain++; + if (this->subdomain >= this->end_subdomain) { + this->subdomain = this->start_subdomain; + this->domain++; + if (this->domain >= this->end_domain) { + this->serial_number = 0; + this->complete = true; + } + } + } + } + } + if (!this->complete) { + size_t index1 = (30 * this->domain) + (this->subdomain % 3); + return encode_dc_serial_number_int(primes1[index1] * primes2[this->index2] * primes3[this->index3]); + } else { + return 0; + } +} + +size_t DCSerialNumberIterator::total_count() const { + return (this->end_domain - this->start_domain) * (this->end_subdomain - this->start_subdomain) * num_primes2 * num_primes3; +} + +size_t DCSerialNumberIterator::progress() const { + size_t domains_done = this->domain - this->start_domain; + size_t subdomains_per_domain = this->end_subdomain - this->start_subdomain; + size_t subdomains_done = this->subdomain - this->start_subdomain; + return ( + (domains_done * subdomains_per_domain * num_primes2 * num_primes3) + + (subdomains_done * num_primes2 * num_primes3) + + (this->index2 * num_primes3) + + this->index3); +} + void dc_serial_number_speed_test(uint64_t seed) { uint32_t effective_seed = (seed & 0xFFFFFFFF00000000) ? phosg::random_object() : seed; fprintf(stderr, "Product speed test with seed=%08" PRIX32 "\n", effective_seed); diff --git a/src/DCSerialNumbers.hh b/src/DCSerialNumbers.hh index 07b94a47..599dea1f 100644 --- a/src/DCSerialNumbers.hh +++ b/src/DCSerialNumbers.hh @@ -20,6 +20,25 @@ bool decoded_dc_serial_number_is_valid_fast( std::string generate_dc_serial_number(uint8_t domain, uint8_t subdomain = 0xFF); std::unordered_map generate_all_dc_serial_numbers(uint8_t domain = 0xFF, uint8_t subdomain = 0xFF); +struct DCSerialNumberIterator { + bool started = false; + bool complete = false; + uint8_t domain = 0; + uint8_t start_domain = 0; + uint8_t end_domain = 3; + uint8_t subdomain = 0; + uint8_t start_subdomain = 0; + uint8_t end_subdomain = 3; + uint16_t index2 = 0; + uint16_t index3 = 0; + uint32_t serial_number = 0; + + uint32_t next(); + + size_t total_count() const; + size_t progress() const; +}; + void dc_serial_number_speed_test(uint64_t seed = 0xFFFFFFFFFFFFFFFF); struct EncryptedDCv2Executables { diff --git a/src/Main.cc b/src/Main.cc index ad277444..1aa04cad 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -610,8 +610,6 @@ Action a_parse_pc_v2_registry( serial_number, serial_number, access_data.c_str(), email_data.c_str()); }); -// ./newserv generate-pc-v2-registry --serial-number=386248990 --access-key=14575600 --email=mjem@wildblue.net - Action a_generate_pc_v2_registry( "generate-pc-v2-registry", "\ generate-pc-v2-registry [OUTPUT-FILENAME]\n\ @@ -674,6 +672,172 @@ Action a_decrypt_challenge_data( write_output_data(args, data.data(), data.size(), "dec"); }); +static void a_encrypt_decrypt_vms_save_fn(phosg::Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-vms-save"); + bool skip_checksum = args.get("skip-checksum"); + string serial_number_str = args.get("serial-number"); + int64_t override_round2_seed = args.get("round2-seed", -1, phosg::Arguments::IntFormat::HEX); + + int64_t round1_seed = serial_number_str.empty() ? -1 : stoul(serial_number_str, nullptr, 16); + + auto data = read_input_data(args); + phosg::StringReader r(data); + const auto& header = r.get(); + header.check(); + r.skip(header.icon_data_size()); + + size_t data_start_offset = r.where(); + + auto process_file = [&]() { + if (is_decrypt) { + const void* data_section = r.getv(header.data_size); + if (round1_seed < 0) { + size_t num_threads = args.get("threads", 0); + if (num_threads == 0) { + num_threads = thread::hardware_concurrency(); + } + + mutex output_lock; + if (UseIterator) { + DCSerialNumberIterator iter; + mutex iter_lock; + atomic seed_found = false; + auto thread_fn = [&]() -> void { + for (;;) { + uint32_t serial_number; + { + lock_guard g(iter_lock); + serial_number = iter.next(); + } + if (serial_number == 0) { + return; + } + try { + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, sizeof(StructT), serial_number, skip_checksum, override_round2_seed); + + seed_found = true; + { + lock_guard g(iter_lock); + iter.complete = true; + } + lock_guard g(output_lock); + fprintf(stderr, "\nFound serial number: %08" PRIX32 "\n", serial_number); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + + } catch (const runtime_error&) { + } + } + }; + + vector threads; + while (threads.size() < num_threads) { + threads.emplace_back(thread_fn); + } + for (;;) { + usleep(1000000); + lock_guard g(iter_lock); + size_t progress = iter.progress(); + size_t total_count = iter.total_count(); + float progress_percent = static_cast(progress * 100) / total_count; + fprintf(stderr, "... %zu/%zu (%g%%, domain %02hhX, subdomain %02hhX, index2 %04hX, index3 %04hX)\r", + progress, total_count, progress_percent, iter.domain, iter.subdomain, iter.index2, iter.index3); + if (iter.complete) { + break; + } + } + for (auto& th : threads) { + th.join(); + } + if (!seed_found) { + throw runtime_error("no seed found"); + } + + } else { + uint64_t seed = phosg::parallel_range_blocks([&](uint64_t serial_number, size_t) -> bool { + try { + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, sizeof(StructT), serial_number, skip_checksum, override_round2_seed); + + lock_guard g(output_lock); + fprintf(stderr, "\nFound serial number: %08" PRIX64 "\n", serial_number); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + return true; + + } catch (const runtime_error&) { + return false; + } + }, + 0, 0x100000000, 0x1000, num_threads); + if (seed >= 0x100000000) { + throw runtime_error("no seed found"); + } + } + + } else { + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, sizeof(StructT), round1_seed, skip_checksum, override_round2_seed); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + } + + } else { + const auto& s = r.get(); + 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"); + } + memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size()); + } + }; + + bool is_v2 = header.is_v2(); + if (!is_v2 && (header.data_size == sizeof(PSODCNTECharacterFile))) { + fprintf(stderr, "File type: DC NTE character\n"); + process_file.template operator()(); + } else if (!is_v2 && (header.data_size == sizeof(PSODCNTEGuildCardFile))) { + fprintf(stderr, "File type: DC NTE Guild Card list\n"); + throw runtime_error("DC NTE Guild Card files are not encrypted"); + } else if (!is_v2 && (header.data_size == sizeof(PSODC112000CharacterFile))) { + fprintf(stderr, "File type: DC 11/2000 character\n"); + process_file.template operator()(); + } else if (!is_v2 && (header.data_size == sizeof(PSODC112000GuildCardFile))) { + fprintf(stderr, "File type: DC 11/2000 Guild Card list\n"); + throw runtime_error("DC 11/2000 Guild Card files are not encrypted"); + } else if (!is_v2 && (header.data_size == sizeof(PSODCV1CharacterFile))) { + fprintf(stderr, "File type: DC v1 character\n"); + process_file.template operator()(); + } else if (is_v2 && (header.data_size == sizeof(PSODCV2CharacterFile))) { + fprintf(stderr, "File type: DC v2 character\n"); + process_file.template operator()(); + } else if (header.data_size == sizeof(PSODCV1V2GuildCardFile)) { + // There appears to be a copy/paste error here: the game uses the character + // file size when checksumming the Guild Card file, so we must do the same + if (!is_v2) { + fprintf(stderr, "File type: DC v1 Guild Card list\n"); + static_assert(sizeof(PSODCV1CharacterFile) <= sizeof(PSODCV1V2GuildCardFile::EncryptedSection)); + process_file.template operator()(); + } else { + fprintf(stderr, "File type: DC v2 Guild Card list\n"); + static_assert(sizeof(PSODCV2CharacterFile) <= sizeof(PSODCV1V2GuildCardFile::EncryptedSection)); + process_file.template operator()(); + } + } else { + throw runtime_error("unrecognized save type"); + } + + write_output_data(args, data.data(), data.size(), is_decrypt ? "vmsd" : "vms"); +} + +Action a_decrypt_vms_save("decrypt-vms-save", nullptr, a_encrypt_decrypt_vms_save_fn); +Action a_encrypt_vms_save("encrypt-vms-save", "\ + encrypt-gci-save --seed=SEED INPUT-FILENAME [OUTPUT-FILENAME]\n\ + decrypt-gci-save [--seed=SEED] INPUT-FILENAME [OUTPUT-FILENAME]\n\ + Encrypt or decrypt a character or Guild Card file in VMS format. If\n\ + encrypting, the checksum is also recomputed and stored in the encrypted\n\ + file. --seed is the encryption seed (serial number) specified as a 32-bit\n\ + hexadecimal value.\n", + a_encrypt_decrypt_vms_save_fn); + static void a_encrypt_decrypt_gci_save_fn(phosg::Arguments& args) { bool is_decrypt = (args.get(0) == "decrypt-gci-save"); bool skip_checksum = args.get("skip-checksum"); @@ -996,7 +1160,7 @@ Action a_salvage_gci( auto process_file = [&]() { vector> top_seeds_by_thread( num_threads ? num_threads : thread::hardware_concurrency()); - phosg::parallel_range( + phosg::parallel_range_blocks( [&](uint64_t seed, size_t thread_num) -> bool { size_t zero_count; if (round2) { @@ -1026,9 +1190,7 @@ Action a_salvage_gci( } return false; }, - 0, - 0x100000000, - num_threads); + 0, 0x100000000, 0x1000, num_threads); multimap top_seeds; for (const auto& thread_top_seeds : top_seeds_by_thread) { @@ -1108,7 +1270,7 @@ Action a_find_decryption_seed( return true; }; - uint64_t seed = phosg::parallel_range([&](uint64_t seed, size_t) -> bool { + uint64_t seed = phosg::parallel_range_blocks([&](uint64_t seed, size_t) -> bool { string be_decrypt_buf = ciphertext.substr(0, max_plaintext_size); string le_decrypt_buf = ciphertext.substr(0, max_plaintext_size); if (uses_v3_encryption(version)) { @@ -1137,7 +1299,7 @@ Action a_find_decryption_seed( } return false; }, - 0, 0x100000000, num_threads); + 0, 0x100000000, 0x1000, num_threads); if (seed < 0x100000000) { phosg::log_info("Found seed %08" PRIX64, seed); @@ -2500,7 +2662,7 @@ Action a_find_rare_enemy_seeds( return false; }; - phosg::parallel_range(thread_fn, 0, 0x100000000, num_threads, nullptr); + phosg::parallel_range_blocks(thread_fn, 0, 0x100000000, 0x1000, num_threads, nullptr); }); Action a_load_maps_test( @@ -2639,45 +2801,65 @@ Action a_generate_dc_serial_number( fprintf(stdout, "%s\n", serial_number.c_str()); }); Action a_generate_all_dc_serial_numbers( - "generate-all-dc-serial-numbers", "\ - generate-all-dc-serial-numbers\n\ - Generate all possible PSO DC serial numbers.\n", + "dc-serial-number-generator-test", nullptr, +[](phosg::Arguments& args) { size_t num_threads = args.get("threads", 0); - auto serial_numbers = generate_all_dc_serial_numbers(); - fprintf(stdout, "%zu (0x%zX) serial numbers found\n", serial_numbers.size(), serial_numbers.size()); - for (const auto& it : serial_numbers) { - fprintf(stdout, "Valid serial number: %08" PRIX32, it.first); - for (uint8_t where : it.second) { - fprintf(stdout, " (domain=%hhu, subdomain=%hhu)", - static_cast((where >> 2) & 3), - static_cast(where & 3)); + vector> serial_numbers; + serial_numbers.resize(9); + + DCSerialNumberIterator iter; + uint32_t serial_number; + size_t num_serial_numbers = 0; + while ((serial_number = iter.next()) != 0) { + serial_numbers[iter.domain * 3 + iter.subdomain].emplace(serial_number); + if (((++num_serial_numbers) % 0x10000) == 0) { + fprintf(stderr, "... %08zX (domain=%02hhX, subdomain=%02hhX, index2=%04hX, index3=%04hX) counts=[%zu, %zu, %zu, %zu, %zu, %zu, %zu, %zu, %zu]\n", + num_serial_numbers, iter.domain, iter.subdomain, iter.index2, iter.index3, + serial_numbers[0].size(), serial_numbers[1].size(), serial_numbers[2].size(), + serial_numbers[3].size(), serial_numbers[4].size(), serial_numbers[5].size(), + serial_numbers[6].size(), serial_numbers[7].size(), serial_numbers[8].size()); } - fputc('\n', stdout); } - atomic num_valid_serial_numbers = 0; + array, 9> found_counts = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + atomic num_mismatches = 0; mutex output_lock; auto thread_fn = [&](uint64_t serial_number, size_t) -> bool { for (uint8_t domain = 0; domain < 3; domain++) { for (uint8_t subdomain = 0; subdomain < 3; subdomain++) { - if (dc_serial_number_is_valid_fast(serial_number, domain, subdomain)) { - num_valid_serial_numbers++; + bool is_valid = dc_serial_number_is_valid_fast(serial_number, domain, subdomain); + bool was_iterated = serial_numbers[domain * 3 + subdomain].count(serial_number); + if (is_valid != was_iterated) { lock_guard g(output_lock); - fprintf(stdout, "Valid serial number: %08" PRIX64 " (domain=%hhu, subdomain=%hhu)\n", serial_number, domain, subdomain); + fprintf(stdout, "Mismatch at %08" PRIX64 " (domain=%hhu, subdomain=%hhu): is_valid=%s, was_iterated=%s\n", + serial_number, domain, subdomain, is_valid ? "true" : "false", was_iterated ? "true" : "false"); + } else if (is_valid && was_iterated) { + found_counts[domain * 3 + subdomain]++; } } } return false; }; auto progress_fn = [&](uint64_t, uint64_t, uint64_t current_value, uint64_t) -> void { - uint64_t num_found = num_valid_serial_numbers.load(); - fprintf(stderr, "... %08" PRIX64 " %" PRId64 " (0x%" PRIX64 ") found\r", - current_value, num_found, num_found); + fprintf(stderr, "... %08" PRIX64 " %" PRId64 " mismatches; counts: [%zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu, %zu/%zu]\r", current_value, num_mismatches.load(), + found_counts[0].load(), serial_numbers[0].size(), + found_counts[1].load(), serial_numbers[1].size(), + found_counts[2].load(), serial_numbers[2].size(), + found_counts[3].load(), serial_numbers[3].size(), + found_counts[4].load(), serial_numbers[4].size(), + found_counts[5].load(), serial_numbers[5].size(), + found_counts[6].load(), serial_numbers[6].size(), + found_counts[7].load(), serial_numbers[7].size(), + found_counts[8].load(), serial_numbers[8].size()); }; - phosg::parallel_range(thread_fn, 0, 0x100000000, num_threads, progress_fn); + phosg::parallel_range_blocks(thread_fn, 0, 0x100000000, 0x1000, num_threads, progress_fn); + + if (num_mismatches > 0) { + throw logic_error("mismatches occurred during test"); + } }); + Action a_inspect_dc_serial_number( "inspect-dc-serial-number", "\ inspect-dc-serial-number SERIAL-NUMBER\n\ @@ -2797,7 +2979,7 @@ Action a_replay_ep3_battle_commands( run_replay(base_seed, 0); } else { size_t num_threads = args.get("threads", 0); - phosg::parallel_range(run_replay, 0, 0x100000000, num_threads); + phosg::parallel_range_blocks(run_replay, 0, 0x100000000, 0x1000, num_threads); } }); diff --git a/src/Quest.cc b/src/Quest.cc index 87d0d32b..9d7f9255 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -166,7 +166,7 @@ string find_seed_and_decrypt_download_quest_data_section( 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 = phosg::parallel_range([&](uint64_t seed, size_t) { + uint64_t result_seed = phosg::parallel_range_blocks([&](uint64_t seed, size_t) { try { string ret = decrypt_download_quest_data_section( data_section, size, seed, skip_checksum, is_ep3_trial); @@ -177,7 +177,7 @@ string find_seed_and_decrypt_download_quest_data_section( return false; } }, - 0, 0x100000000, num_threads); + 0, 0x100000000, 0x1000, num_threads); if (!result.empty() && (result_seed < 0x100000000)) { static_game_data_log.info("Found seed %08" PRIX64, result_seed); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index aae3198b..59602e0f 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3416,7 +3416,7 @@ static void on_30(shared_ptr c, uint16_t, uint32_t, string& data) { shared_ptr bb_char; switch (c->version()) { case Version::DC_V2: - bb_char = PSOBBCharacterFile::create_from_dc_v2(check_size_t(data)); + bb_char = PSOBBCharacterFile::create_from_dc_v2(check_size_t(data)); break; case Version::GC_NTE: bb_char = PSOBBCharacterFile::create_from_gc_nte(check_size_t(data)); diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 2c3824c2..47265d94 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -143,6 +143,16 @@ bool PSOVMSFileHeader::checksum_correct() const { return (crc == this->crc); } +void PSOVMSFileHeader::check() const { + if (!this->checksum_correct()) { + throw runtime_error("VMS file unencrypted header checksum is incorrect"); + } +} + +bool PSOVMSFileHeader::is_v2() const { + return !memcmp(this->short_desc.data, "PSOV2", 5); +} + bool PSOGCIFileHeader::checksum_correct() const { uint32_t cs = phosg::crc32(&this->game_name, this->game_name.bytes()); cs = phosg::crc32(&this->embedded_seed, sizeof(this->embedded_seed), cs); @@ -543,7 +553,7 @@ shared_ptr PSOBBCharacterFile::create_from_preview( guild_card_number, language, preview.visual, preview.name.decode(language), level_table); } -shared_ptr PSOBBCharacterFile::create_from_dc_v2(const PSODCV2CharacterFile& dc) { +shared_ptr PSOBBCharacterFile::create_from_dc_v2(const PSODCV2CharacterFile::Character& dc) { auto ret = make_shared(); ret->inventory = dc.inventory; ret->inventory.decode_from_client(Version::DC_V2); @@ -792,10 +802,10 @@ void save_psochar( phosg::fwritex(f.get(), empty_membership); } -PSODCV2CharacterFile PSOBBCharacterFile::to_dc_v2() const { +PSODCV2CharacterFile::Character PSOBBCharacterFile::to_dc_v2() const { uint8_t language = this->inventory.language; - PSODCV2CharacterFile ret; + PSODCV2CharacterFile::Character ret; ret.inventory = this->inventory; // We don't need to do the v1-compatible encoding (hence it is OK to pass // nullptr here) but we do need to encode mag stats in the v2 format diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 51e7cdcb..c793d7d0 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -36,6 +36,12 @@ struct PSOVMSFileHeader { /* 0080 */ // parray, num_icons> icon; bool checksum_correct() const; + void check() const; + inline size_t icon_data_size() const { + return this->num_icons * 0x200; + } + + bool is_v2() const; } __packed_ws__(PSOVMSFileHeader, 0x80); struct PSOGCIFileHeader { @@ -292,118 +298,139 @@ struct PSOBBBaseSystemFile : PSOBBMinimalSystemFile { // Character files struct PSODCNTECharacterFile { - // See PSOGCCharacterFile::Character for descriptions of fields' meanings. - /* 0000:---- */ PlayerInventory inventory; - /* 034C:---- */ PlayerDispDataDCPCV3 disp; - /* 041C:0000 */ le_uint32_t validation_flags = 0; - /* 0420:0004 */ le_uint32_t creation_timestamp = 0; - /* 0424:0008 */ le_uint32_t signature = 0xBB40711D; - /* 0428:000C */ le_uint32_t play_time_seconds = 0; - /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; - /* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1; - /* 0432:0016 */ le_uint16_t inventory_erasure_count = 0; - /* 0434:0018 */ pstring ppp_username; - /* 0450:0034 */ pstring ppp_password; - // TODO: Figure out how quest flags work; it's obviously different from 0x80 - // bytes per difficulty like in v1. Is it just 2048 flags shared across all - // difficulties, instead of 1024 in each difficulty? - /* 0460:0044 */ parray quest_flags; - /* 0560:0144 */ le_uint16_t bank_meseta; - /* 0562:0146 */ le_uint16_t num_bank_items; - /* 0564:0148 */ parray bank_items; - /* 0A14:05F8 */ GuildCardDCNTE guild_card; - /* 0A8F:0673 */ uint8_t unknown_s1; // Probably actually unused - /* 0A90:0674 */ pstring v1_serial_number; - /* 0AA0:0684 */ pstring v1_access_key; - /* 0AB0:0694 */ -} __packed_ws__(PSODCNTECharacterFile, 0xAB0); + /* 0000 */ le_uint32_t checksum = 0; + struct Character { + // See PSOGCCharacterFile::Character for descriptions of fields' meanings. + /* 0000:---- */ PlayerInventory inventory; + /* 034C:---- */ PlayerDispDataDCPCV3 disp; + /* 041C:0000 */ le_uint32_t validation_flags = 0; + /* 0420:0004 */ le_uint32_t creation_timestamp = 0; + /* 0424:0008 */ le_uint32_t signature = 0xBB40711D; + /* 0428:000C */ le_uint32_t play_time_seconds = 0; + /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; + /* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1; + /* 0432:0016 */ le_uint16_t inventory_erasure_count = 0; + /* 0434:0018 */ pstring ppp_username; + /* 0450:0034 */ pstring ppp_password; + // TODO: Figure out how quest flags work; it's obviously different from 0x80 + // bytes per difficulty like in v1. Is it just 2048 flags shared across all + // difficulties, instead of 1024 in each difficulty? + /* 0460:0044 */ parray quest_flags; + /* 0560:0144 */ le_uint16_t bank_meseta; + /* 0562:0146 */ le_uint16_t num_bank_items; + /* 0564:0148 */ parray bank_items; + /* 0A14:05F8 */ GuildCardDCNTE guild_card; + /* 0A8F:0673 */ uint8_t unknown_s1; // Probably actually unused + /* 0A90:0674 */ pstring v1_serial_number; + /* 0AA0:0684 */ pstring v1_access_key; + /* 0AB0:0694 */ + } __packed_ws__(Character, 0xAB0); + /* 0004 */ Character character; + /* 0AB4 */ le_uint32_t round2_seed = 0; + /* 0AB8 */ +} __packed_ws__(PSODCNTECharacterFile, 0xAB8); struct PSODC112000CharacterFile { - // See PSOGCCharacterFile::Character for descriptions of fields' meanings. - /* 0000:---- */ PlayerInventory inventory; - /* 034C:---- */ PlayerDispDataDCPCV3 disp; - /* 041C:0000 */ le_uint32_t validation_flags = 0; - /* 0420:0004 */ le_uint32_t creation_timestamp = 0; - /* 0424:0008 */ le_uint32_t signature = 0xBB40711D; - /* 0428:000C */ le_uint32_t play_time_seconds = 0; - /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; - /* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1; - /* 0432:0016 */ le_uint16_t inventory_erasure_count = 0; - /* 0434:0018 */ pstring ppp_username; - /* 0450:0034 */ pstring ppp_password; - // TODO: Figure out how quest flags work; it's obviously different from 0x80 - // bytes per difficulty like in v1. Is it just 2048 flags shared across all - // difficulties, instead of 1024 in each difficulty? - /* 0460:0044 */ parray quest_flags; - /* 0560:0144 */ le_uint16_t bank_meseta; - /* 0562:0146 */ le_uint16_t num_bank_items; - /* 0564:0148 */ parray bank_items; - /* 0A14:05F8 */ GuildCardDC guild_card; - /* 0A91:0675 */ parray unknown_s1; // Probably actually unused - /* 0A94:0678 */ parray symbol_chats; - /* 0EB4:0A98 */ parray shortcuts; - /* 13B4:0F98 */ pstring v1_serial_number; - /* 13C4:0FA8 */ pstring v1_access_key; - /* 13D4:0FB8 */ -} __packed_ws__(PSODC112000CharacterFile, 0x13D4); + /* 0000 */ le_uint32_t checksum = 0; + struct Character { + // See PSOGCCharacterFile::Character for descriptions of fields' meanings. + /* 0000:---- */ PlayerInventory inventory; + /* 034C:---- */ PlayerDispDataDCPCV3 disp; + /* 041C:0000 */ le_uint32_t validation_flags = 0; + /* 0420:0004 */ le_uint32_t creation_timestamp = 0; + /* 0424:0008 */ le_uint32_t signature = 0xBB40711D; + /* 0428:000C */ le_uint32_t play_time_seconds = 0; + /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; + /* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1; + /* 0432:0016 */ le_uint16_t inventory_erasure_count = 0; + /* 0434:0018 */ pstring ppp_username; + /* 0450:0034 */ pstring ppp_password; + // TODO: Figure out how quest flags work; it's obviously different from 0x80 + // bytes per difficulty like in v1. Is it just 2048 flags shared across all + // difficulties, instead of 1024 in each difficulty? + /* 0460:0044 */ parray quest_flags; + /* 0560:0144 */ le_uint16_t bank_meseta; + /* 0562:0146 */ le_uint16_t num_bank_items; + /* 0564:0148 */ parray bank_items; + /* 0A14:05F8 */ GuildCardDC guild_card; + /* 0A91:0675 */ parray unknown_s1; // Probably actually unused + /* 0A94:0678 */ parray symbol_chats; + /* 0EB4:0A98 */ parray shortcuts; + /* 13B4:0F98 */ pstring v1_serial_number; + /* 13C4:0FA8 */ pstring v1_access_key; + /* 13D4:0FB8 */ + } __packed_ws__(Character, 0x13D4); + /* 0004 */ Character character; + /* 13D8 */ le_uint32_t round2_seed = 0; +} __packed_ws__(PSODC112000CharacterFile, 0x13DC); struct PSODCV1CharacterFile { - // See PSOGCCharacterFile::Character for descriptions of fields' meanings. - /* 0000:---- */ PlayerInventory inventory; - /* 034C:---- */ PlayerDispDataDCPCV3 disp; - /* 041C:0000 */ le_uint32_t validation_flags = 0; - /* 0420:0004 */ le_uint32_t creation_timestamp = 0; - /* 0424:0008 */ le_uint32_t signature = 0xA205B064; - /* 0428:000C */ le_uint32_t play_time_seconds = 0; - /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; - /* 0430:0014 */ le_uint32_t save_count = 1; - /* 0434:0018 */ pstring ppp_username; - /* 0450:0034 */ pstring ppp_password; - /* 0460:0044 */ QuestFlagsV1 quest_flags; - /* 05E0:01C4 */ PlayerBank60 bank; - /* 0B88:076C */ GuildCardDC guild_card; - /* 0C05:07E9 */ parray unknown_s1; // Probably actually unused - /* 0C08:07EC */ parray symbol_chats; - /* 1028:0C0C */ parray shortcuts; - /* 1528:110C */ pstring v1_serial_number; - /* 1538:111C */ pstring v1_access_key; - /* 1548:112C */ -} __packed_ws__(PSODCV1CharacterFile, 0x1548); + /* 0000 */ le_uint32_t checksum = 0; + struct Character { + // See PSOGCCharacterFile::Character for descriptions of fields' meanings. + /* 0000:---- */ PlayerInventory inventory; + /* 034C:---- */ PlayerDispDataDCPCV3 disp; + /* 041C:0000 */ le_uint32_t validation_flags = 0; + /* 0420:0004 */ le_uint32_t creation_timestamp = 0; + /* 0424:0008 */ le_uint32_t signature = 0xA205B064; + /* 0428:000C */ le_uint32_t play_time_seconds = 0; + /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; + /* 0430:0014 */ le_uint32_t save_count = 1; + /* 0434:0018 */ pstring ppp_username; + /* 0450:0034 */ pstring ppp_password; + /* 0460:0044 */ QuestFlagsV1 quest_flags; + /* 05E0:01C4 */ PlayerBank60 bank; + /* 0B88:076C */ GuildCardDC guild_card; + /* 0C05:07E9 */ parray unknown_s1; // Probably actually unused + /* 0C08:07EC */ parray symbol_chats; + /* 1028:0C0C */ parray shortcuts; + /* 1528:110C */ pstring v1_serial_number; + /* 1538:111C */ pstring v1_access_key; + /* 1548:112C */ + } __packed_ws__(Character, 0x1548); + /* 0004 */ Character character; + /* 154C */ le_uint32_t round2_seed = 0; +} __packed_ws__(PSODCV1CharacterFile, 0x1550); struct PSODCV2CharacterFile { - // See PSOGCCharacterFile::Character for descriptions of fields' meanings. - /* 0000:---- */ PlayerInventory inventory; - /* 034C:---- */ PlayerDispDataDCPCV3 disp; - /* 041C:0000 */ le_uint32_t validation_flags = 0; - /* 0420:0004 */ le_uint32_t creation_timestamp = 0; - /* 0424:0008 */ le_uint32_t signature = 0xA205B064; - /* 0428:000C */ le_uint32_t play_time_seconds = 0; - /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; - /* 0430:0014 */ le_uint32_t save_count = 1; - /* 0434:0018 */ pstring ppp_username; - /* 0450:0034 */ pstring ppp_password; - /* 0460:0044 */ QuestFlags quest_flags; - /* 0660:0244 */ PlayerBank60 bank; - /* 0C08:07EC */ GuildCardDC guild_card; - /* 0C85:0869 */ parray unknown_s1; // Probably actually unused - /* 0C88:086C */ parray symbol_chats; - /* 10A8:0C8C */ parray shortcuts; - /* 15A8:118C */ pstring v1_serial_number; - /* 15B8:119C */ pstring v1_access_key; - /* 15C8:11AC */ PlayerRecordsBattle battle_records; - /* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records; - /* 1680:1264 */ parray tech_menu_shortcut_entries; - // The Choice Search config is stored here as 32-bit integers, even though - // it's represented with 16-bit integers in the various commands that send it - // to and from the server. The order of the entries here is the same (that - // is, the first two of these ints are entries[0], the second two are - // entries[1], etc.). - /* 16A8:128C */ parray choice_search_config; - /* 16D0:12B4 */ parray unknown_a2; - /* 16D4:12B8 */ pstring v2_serial_number; - /* 16E4:12C8 */ pstring v2_access_key; - /* 16F4:12D8 */ -} __packed_ws__(PSODCV2CharacterFile, 0x16F4); + /* 0000 */ le_uint32_t checksum = 0; + struct Character { + // See PSOGCCharacterFile::Character for descriptions of fields' meanings. + /* 0000:---- */ PlayerInventory inventory; + /* 034C:---- */ PlayerDispDataDCPCV3 disp; + /* 041C:0000 */ le_uint32_t validation_flags = 0; + /* 0420:0004 */ le_uint32_t creation_timestamp = 0; + /* 0424:0008 */ le_uint32_t signature = 0xA205B064; + /* 0428:000C */ le_uint32_t play_time_seconds = 0; + /* 042C:0010 */ le_uint32_t option_flags = 0x00040058; + /* 0430:0014 */ le_uint32_t save_count = 1; + /* 0434:0018 */ pstring ppp_username; + /* 0450:0034 */ pstring ppp_password; + /* 0460:0044 */ QuestFlags quest_flags; + /* 0660:0244 */ PlayerBank60 bank; + /* 0C08:07EC */ GuildCardDC guild_card; + /* 0C85:0869 */ parray unknown_s1; // Probably actually unused + /* 0C88:086C */ parray symbol_chats; + /* 10A8:0C8C */ parray shortcuts; + /* 15A8:118C */ pstring v1_serial_number; + /* 15B8:119C */ pstring v1_access_key; + /* 15C8:11AC */ PlayerRecordsBattle battle_records; + /* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records; + /* 1680:1264 */ parray tech_menu_shortcut_entries; + // The Choice Search config is stored here as 32-bit integers, even though + // it's represented with 16-bit integers in the various commands that send it + // to and from the server. The order of the entries here is the same (that + // is, the first two of these ints are entries[0], the second two are + // entries[1], etc.). + /* 16A8:128C */ parray choice_search_config; + /* 16D0:12B4 */ parray unknown_a2; + /* 16D4:12B8 */ pstring v2_serial_number; + /* 16E4:12C8 */ pstring v2_access_key; + /* 16F4:12D8 */ + } __packed_ws__(Character, 0x16F4); + /* 0004 */ Character character; + /* 16F0 */ le_uint32_t round2_seed = 0; +} __packed_ws__(PSODCV2CharacterFile, 0x16FC); struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD // See PSOGCCharacterFile::Character for descriptions of fields' meanings. @@ -517,7 +544,7 @@ struct PSOGCCharacterFile { // ------zA BCDEFG-- HHHIIIJJ KLMNOPQR // z = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts). // This bit is unused by PSO GC. - // A = Keyboard controls (BB; 0 = on; 1 = off). Note that A is also used + // A = Keyboard controls (BB; 0 = on; 1 = off). This field is also used // by PSO GC, but its function is currently unknown. // G = Choice search setting (0 = enabled; 1 = disabled) // H = Player lobby labels (0 = name; 1 = name, language, and level; @@ -555,7 +582,7 @@ struct PSOGCCharacterFile { /* 2794:2378 */ be_uint32_t unknown_f8 = 0; /* 2798:237C */ } __packed_ws__(Character, 0x2798); - /* 00004 */ parray characters; + /* 00004 */ parray characters; // 0-3: main chars, 4-6: temps /* 1152C */ pstring serial_number; // As %08X (not decimal) /* 1153C */ pstring access_key; /* 1154C */ pstring password; @@ -760,13 +787,13 @@ struct PSOBBCharacterFile { uint8_t language, const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); - static std::shared_ptr create_from_dc_v2(const PSODCV2CharacterFile& dc); + static std::shared_ptr create_from_dc_v2(const PSODCV2CharacterFile::Character& dc); static std::shared_ptr create_from_gc_nte(const PSOGCNTECharacterFileCharacter& gc_nte); static std::shared_ptr create_from_gc(const PSOGCCharacterFile::Character& gc); static std::shared_ptr create_from_ep3(const PSOGCEp3CharacterFile::Character& ep3); static std::shared_ptr create_from_xb(const PSOXBCharacterFileCharacter& xb); - PSODCV2CharacterFile to_dc_v2() const; + PSODCV2CharacterFile::Character to_dc_v2() const; PSOGCNTECharacterFileCharacter to_gc_nte() const; PSOGCCharacterFile::Character to_gc() const; PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) const; @@ -811,11 +838,45 @@ void save_psochar( //////////////////////////////////////////////////////////////////////////////// // Guild Card files -struct PSODCV2GuildCardFile { - // TODO: Fill this out. - /* 0000 */ parray unknown_a1; - /* 330C */ -} __packed_ws__(PSODCV2GuildCardFile, 0x330C); +struct PSODCNTEGuildCardFile { + // Note: DC NTE does not encrypt the Guild Card file + struct Entry { + /* 0000 */ GuildCardDCNTE guild_card; + /* 007B */ uint8_t unknown_a1 = 0; // Probably actually unused + /* 007C */ + } __packed_ws__(Entry, 0x7C); + /* 0000 */ parray entries; + /* 3070 */ +} __packed_ws__(PSODCNTEGuildCardFile, 0x3070); + +struct PSODCGuildCardFileEntry { + /* 0000 */ GuildCardDC guild_card; + /* 007D */ parray unknown_a1 = 0; // Probably actually unused + /* 0080 */ +} __packed_ws__(PSODCGuildCardFileEntry, 0x80); + +struct PSODC112000GuildCardFile { + // Note: 11/2000 does not encrypt the Guild Card file + /* 0000 */ parray entries; + /* 3200 */ +} __packed_ws__(PSODC112000GuildCardFile, 0x3200); + +struct PSODCV1V2GuildCardFile { + struct EncryptedSection { + /* 0000 */ le_uint32_t checksum = 0; + /* 0004 */ parray entries; + /* 3204 */ le_int16_t music_volume = 0; + /* 3206 */ int8_t sound_volume = 0; + /* 3207 */ uint8_t language = 1; + /* 3208 */ le_uint32_t server_time_delta_frames = 540000; // 648000 on DCv1 + /* 320C */ le_uint32_t creation_timestamp = 0; + /* 3210 */ le_uint32_t round2_seed = 0; + /* 3214 */ + } __packed_ws__(EncryptedSection, 0x3214); + /* 0000 */ EncryptedSection encrypted_section; + /* 3214 */ parray event_flags; + /* 3314 */ +} __packed_ws__(PSODCV1V2GuildCardFile, 0x3314); struct PSOPCGuildCardFile { // PSO______GUD /* 0000 */ le_uint32_t checksum = 0; @@ -979,14 +1040,14 @@ std::string decrypt_fixed_size_data_section_s( size_t size, uint32_t round1_seed, bool skip_checksum = false, - uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) { + int64_t override_round2_seed = -1) { if (size < 2 * sizeof(U32T)) { throw std::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) + uint32_t round2_seed = (override_round2_seed >= 0) + ? override_round2_seed : reinterpret_cast*>(decrypted.data() + decrypted.size() - sizeof(U32T))->load(); PSOV2Encryption round2_crypt(round2_seed); if (BE) { @@ -1011,13 +1072,13 @@ std::string decrypt_fixed_size_data_section_s( return decrypted; } -template +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) { + int64_t override_round2_seed = -1) { std::string decrypted = decrypt_data_section(data_section, size, round1_seed); if (decrypted.size() < sizeof(StructT)) { @@ -1025,7 +1086,7 @@ StructT decrypt_fixed_size_data_section_t( } StructT ret = *reinterpret_cast(decrypted.data()); - PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load()); + PSOV2Encryption round2_crypt((override_round2_seed >= 0) ? override_round2_seed : ret.round2_seed.load()); if (BE) { round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); } else { @@ -1035,7 +1096,7 @@ StructT decrypt_fixed_size_data_section_t( if (!skip_checksum) { uint32_t expected_crc = ret.checksum; ret.checksum = 0; - uint32_t actual_crc = phosg::crc32(&ret, sizeof(ret)); + uint32_t actual_crc = phosg::crc32(&ret, ChecksumLength); ret.checksum = expected_crc; if (expected_crc != actual_crc) { throw std::runtime_error(phosg::string_printf( @@ -1070,12 +1131,12 @@ std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uin return encrypt_data_section(encrypted.data(), encrypted.size(), round1_seed); } -template +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 = phosg::random_object(); - encrypted.checksum = phosg::crc32(&encrypted, sizeof(encrypted)); + encrypted.checksum = phosg::crc32(&encrypted, ChecksumLength); PSOV2Encryption round2_crypt(encrypted.round2_seed); if (BE) { diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s index 9e463cc1..ea85fbe2 100644 --- a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s @@ -10,4 +10,4 @@ start: data: .data 0x8C4EC4E0 # char_file_part1 .data 0x8C4EC4E4 # char_file_part2 - # Server adds a PSODCV2CharacterFile here + # Server adds a PSODCV2CharacterFile::Character here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s index 9e463cc1..ea85fbe2 100644 --- a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s @@ -10,4 +10,4 @@ start: data: .data 0x8C4EC4E0 # char_file_part1 .data 0x8C4EC4E4 # char_file_part2 - # Server adds a PSODCV2CharacterFile here + # Server adds a PSODCV2CharacterFile::Character here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s index 5ffdd0d3..8369256b 100644 --- a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s @@ -10,4 +10,4 @@ start: data: .data 0x8C4E5F80 # char_file_part1 .data 0x8C4E5F84 # char_file_part2 - # Server adds a PSODCV2CharacterFile here + # Server adds a PSODCV2CharacterFile::Character here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s index 4b0bd131..cef817bf 100644 --- a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s @@ -10,4 +10,4 @@ start: data: .data 0x8C4DB9E0 # char_file_part1 .data 0x8C4DB9E4 # char_file_part2 - # Server adds a PSODCV2CharacterFile here + # Server adds a PSODCV2CharacterFile::Character here diff --git a/tests/decrypt-vms-save.test.sh b/tests/decrypt-vms-save.test.sh new file mode 100755 index 00000000..0501ea51 --- /dev/null +++ b/tests/decrypt-vms-save.test.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +EXECUTABLE="$1" +if [ -z "$EXECUTABLE" ]; then + EXECUTABLE="./newserv" +fi + +DIR="tests/saves-vms" + +echo "... decrypt v1 charfile" +$EXECUTABLE decrypt-vms-save $DIR/d1-sys.vms $DIR/d1-sys.vmsd --serial-number=77777777 +diff $DIR/d1-sys-expected.vmsd $DIR/d1-sys.vmsd + +echo "... decrypt v2 charfile" +$EXECUTABLE decrypt-vms-save $DIR/e1-sys.vms $DIR/e1-sys.vmsd --serial-number=DBA61FAC +diff $DIR/e1-sys-expected.vmsd $DIR/e1-sys.vmsd + +echo "... decrypt v1/v2 guildfile" +$EXECUTABLE decrypt-vms-save $DIR/a1-2gc.vms $DIR/a1-2gc.vmsd --serial-number=DBA61FAC +diff $DIR/a1-2gc-expected.vmsd $DIR/a1-2gc.vmsd + +echo "... clean up" +rm -f $DIR/d1-sys.vmsd $DIR/e1-sys.vmsd $DIR/a1-2gc.vmsd diff --git a/tests/saves-vms/a1-2gc-expected.vmsd b/tests/saves-vms/a1-2gc-expected.vmsd new file mode 100755 index 0000000000000000000000000000000000000000..2e3bdf227efe15c31582a6ddadc1f408b7a61fa1 GIT binary patch literal 15360 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 Ve-8Z5f&V%1KL`Hj!2d@F{vVM_{MG;f literal 0 HcmV?d00001 diff --git a/tests/saves-vms/a1-2gc.vmsd b/tests/saves-vms/a1-2gc.vmsd new file mode 100755 index 0000000000000000000000000000000000000000..2e3bdf227efe15c31582a6ddadc1f408b7a61fa1 GIT binary patch literal 15360 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 W|8zG0-T&_d{yTyHPT>D@0{;WKWr807 literal 0 HcmV?d00001 diff --git a/tests/saves-vms/d1-sys.vmsd b/tests/saves-vms/d1-sys.vmsd new file mode 100755 index 0000000000000000000000000000000000000000..5b05edc9228b05d79baf1a90a96face32b1f3b48 GIT binary patch literal 7680 zcmeHKe@qis9Dgkohssg}1c%^JoFEESLmsYA1CrnKiM{|y(+2X+Mh;Pp=u;UHE>I%+dcXs;1kyR1aNT)isY&F`Z~^7_A#$|_&(&uTkkWZJ@OH6c*Kwz2@%!m4>E=|2yTr5nbV+D|Jwe(XI zK~PkKV;2GW^*F6ofACq6z|<5R%zuIEdc)MZ)#*IYK&J~3-$v%!O-(qh|3pp@+UE$h zTAX9|Pt#{|f)QV%(JU;o7thxR3m_mL&Yd*sNF!uJex@2iwa$=#4YDCW3-X1+OcVV) zq6@GNNT(>RSgduglQskK1u>Hhx+(?I*!#u89yY>X!9`HE9=02&w*GF1rk9h2nNw$B8VTI z04DOzWG;N)MWP3xH|@TA_l8dr25^6ZC*4W3SrD`U2eZK;qwyISJc}}@g^LQTpzY*k z!s;UfJ)Z%0-{#QwbgDMq$C)sa!-BNc<#ol%jhj;31jeOWxpAYWsz${~vmxsM>j3Kj z>j3Kj>%hOz0rbBfD9I=(FRucxcl!icM-XN-<_b4`u1EXG&X zeex>uEAm6^Qykadel})UB^9Wyk_F-s}mUQxbE$F<6pU6Wzzr z#p5b4Wo2niozIX9q3%@p{e+Y!MMQEY#YX}m^mmM}-ugJYaNVl0iV`1H0Q#Zsx`0#; z(&dIna<~bRAmtH#V9uQxkH8Umem?8(9p|`rrVcaqs{YB`$JIpKhk@1gIP_2X@G$?` zcGdyb0oH;4s{`7{l@tEz+_t2Z4dw>*&70TQ*~l}*;f)-HaDP|oj`oUz^m5(M)t;+2 zr>FFi*9(v4MPsVK^NU(#hU<5_Ck%g45%0=t6P`E|mq;Ya&kk1a3%{`8J1qH$>T^Hc zOTK~Njsg52bCmuFX7+)b*La9D=GHu)169Q(zNP?EPhVeyDm}tL8VbT zpWDAb!7#Ud@9;GF;kKOVh9`K^w9sTK?)2u!m+N2xLRw_F0wIrqF{s;7fE}9d^fwVQ|136CSxa$ z#r%$On}d!^i)ks9`1R*ZRODX?N02X%OuGcR*J>5EnyXP?SO`x$oqk8VY_lRil~@u* zjuXY1%t;RU(~QirAAjky3bST&_3Prod&1oDZ1%GmxzkGcQqr3iW*M3Pyr1JDdpMSL z_NL2U;Cm=D;fKTFR$toR!keuKWq8H(`$r z>)aT2uYpp>2tPNbzJ_a~i!dSLQ1t`I>4iVA^4!+UUm$^UTL3bh|=RhsF<>5F`<*SZ6`1G)qMh68^CL7OT9 literal 0 HcmV?d00001 diff --git a/tests/saves-vms/e1-sys.vms b/tests/saves-vms/e1-sys.vms new file mode 100644 index 0000000000000000000000000000000000000000..b934fd840a35365fff6428277952018b4afef906 GIT binary patch literal 7680 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 J2mU`h@LzP~f(ift literal 0 HcmV?d00001 diff --git a/tests/saves-vms/e1-sys.vmsd b/tests/saves-vms/e1-sys.vmsd new file mode 100755 index 0000000000000000000000000000000000000000..2d2375d38e2c135ea4bdd205fbcd774b55885750 GIT binary patch literal 7680 zcmeHLZA@EL7=CY|Y?YyOK_F?6OPgukoC2u!m+N2xLRw_F0wIrqF{s;7fE}9d^fwVQ|136CSxa$ z#r%$On}d!^i)ks9`1R*ZRODX?N02X%OuGcR*J>5EnyXP?SO`x$oqk8VY_lRil~@u* zjuXY1%t;RU(~QirAAjky3bST&_3Prod&1oDZ1%GmxzkGcQqr3iW*M3Pyr1JDdpMSL z_NL2U;Cm=D;fKTFR$toR!keuKWq8H(`$r z>)aT2uYpp>2tPNbzJ_a~i!dSLQ1t`I>4iVA^4!+UUm$^UTL3bh|=RhsF<>5F`<*SZ6`1G)qMh68^CL7OT9 literal 0 HcmV?d00001