From 50e1b79b1ecfd22fee7caca25f2524111a111f4b Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 3 Oct 2022 21:44:30 -0700 Subject: [PATCH] reverse-engineer subcommands sent during game join --- src/CommandFormats.hh | 89 +++++++++++++++++++++++++++++++++++++++++-- src/Compression.cc | 42 ++++++++++++++++++++ src/Compression.hh | 2 + src/Main.cc | 23 ++++++----- 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 1a6ae41a..bd4b2b49 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -3224,12 +3224,93 @@ struct G_EnemyDropItemRequest_PC_V3_BB_6x60 : G_EnemyDropItemRequest_DC_6x60 { // 68: Telepipe/Ryuker // 69: Unknown (supported; game only) // 6A: Unknown (supported; game only; not valid on Episode 3) -// 6B: Sync enemy state (used while loading into game) -// 6C: Sync object state (used while loading into game) -// 6D: Sync item state (used while loading into game) + +// 6B: Sync enemy state (used while loading into game; same header format as 6E) +// 6C: Sync object state (used while loading into game; same header format as 6E) +// 6D: Sync item state (used while loading into game; same header format as 6E) // 6E: Sync flag state (used while loading into game) + +struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { + uint8_t subcommand; + parray unused; + le_uint32_t subcommand_size; + le_uint32_t unknown_a1; + le_uint32_t data_size; // Must be <= subcommand_size - 0x10 + // BC0-compressed data follows here (use bc0_decompress from Compression.hh) +}; + // 6F: Unknown (used while loading into game) -// 70: Unknown (used while loading into game) + +// 70: Sync player disp data and inventory (used while loading into game) +// Annoyingly, they didn't use the same format as the 65/67/68 commands here, +// and instead rearranged a bunch of things. +// TODO: Some missing fields should be easy to find in the future (e.g. when the +// sending player doesn't have 0 meseta, for example) + +struct G_Unknown_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ uint8_t subcommand; // == 0x70 + /* 0005 */ uint8_t basic_size; // == 0 + /* 0006 */ le_uint16_t unused; + /* 0008 */ le_uint32_t subcommand_size; + /* 000C */ parray unknown_a1; + // [1] and [3] in this array (and maybe [2] also) appear to be le_floats; + // they could be the player's current (x, y, z) coords + /* 0010 */ parray unknown_a2; + /* 002C */ parray unknown_a3; + /* 0034 */ parray, 5> unknown_a4; + /* 0070 */ le_uint32_t unknown_a5; + /* 0074 */ le_uint32_t player_tag; + /* 0078 */ le_uint32_t guild_card_number; + /* 007C */ parray unknown_a6; + /* 0084 */ struct { + parray unknown_a1; + parray unknown_a2; + } unknown_a7; + /* 00A0 */ le_uint32_t unknown_a8; + /* 00A4 */ parray unknown_a9; + /* 00B8 */ le_uint32_t unknown_a10; + /* 00BC */ le_uint32_t unknown_a11; + /* 00C0 */ parray technique_levels; // Last byte is uninitialized + /* 00D4 */ struct { + parray name; + uint64_t unknown_a2; // Same as unknown_a2 in PlayerDispDataDCPCV3, presumably + le_uint32_t name_color; + uint8_t extra_model; + parray unused; + le_uint32_t name_color_checksum; + uint8_t section_id; + uint8_t char_class; + uint8_t v2_flags; + uint8_t version; + le_uint32_t v1_flags; + le_uint16_t costume; + le_uint16_t skin; + le_uint16_t face; + le_uint16_t head; + le_uint16_t hair; + le_uint16_t hair_r; + le_uint16_t hair_g; + le_uint16_t hair_b; + le_uint32_t proportion_x; + le_uint32_t proportion_y; + } disp_part2; + /* 0124 */ struct { + PlayerStats stats; + parray unknown_a1; + le_uint32_t level; + le_uint32_t experience; + le_uint32_t meseta; + } disp_part1; + /* 0148 */ struct { + le_uint32_t num_items; + // Entries >= num_items in this array contain uninitialized data (usually + // the contents of a previous sync command) + parray items; + } inventory; + /* 0494 */ le_uint32_t unknown_a15; +}; + // 71: Unknown (used while loading into game) // 72: Unknown (used while loading into game) // 73: Invalid subcommand (but apparently valid on BB; function is unknown) diff --git a/src/Compression.cc b/src/Compression.cc index 91a23ab9..09d19419 100644 --- a/src/Compression.cc +++ b/src/Compression.cc @@ -8,6 +8,8 @@ #include +#include "Text.hh" + using namespace std; @@ -366,3 +368,43 @@ size_t prs_decompress_size(const string& data, size_t max_size) { } } } + + + +string bc0_decompress(const string& data) { + StringReader r(data); + StringWriter w; + + parray memo; + uint16_t memo_offset = 0x0FEE; + uint16_t control_stream_bits = 0x0000; + while (!r.eof()) { + control_stream_bits >>= 1; + if ((control_stream_bits & 0x100) == 0) { + control_stream_bits = 0xFF00 | r.get_u8(); + if (r.eof()) { + break; + } + } + if ((control_stream_bits & 1) == 0) { + uint8_t a1 = r.get_u8(); + if (r.eof()) { + break; + } + uint8_t a2 = r.get_u8(); + for (size_t z = 0; z <= (a2 & 0x0F) + 2; z++) { + uint8_t v = memo[((a1 | ((a2 << 4) & 0xF00)) + z) & 0x0FFF]; + w.put_u8(v); + memo[memo_offset] = v; + memo_offset = (memo_offset + 1) & 0x0FFF; + } + } else { + uint8_t v = r.get_u8(); + w.put_u8(v); + memo[memo_offset] = v; + memo_offset = (memo_offset + 1) & 0x0FFF; + } + } + + return move(w.str()); +} diff --git a/src/Compression.hh b/src/Compression.hh index 726600bc..9fedfec0 100644 --- a/src/Compression.hh +++ b/src/Compression.hh @@ -11,3 +11,5 @@ std::string prs_compress(const std::string& data); std::string prs_decompress(const std::string& data, size_t max_size = 0); size_t prs_decompress_size(const std::string& data, size_t max_size = 0); + +std::string bc0_decompress(const std::string& data); diff --git a/src/Main.cc b/src/Main.cc index 484228a9..69dd960f 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -233,8 +233,8 @@ The options are:\n\ --decompress-prs\n\ Compress or decompress data using the PRS algorithm. Both input-filename\n\ and output-filename may be specified.\n\ - --decompress-gjs [input-filename [output-filename]]\n\ - Decompress data using the GJS algorithm.Both input-filename and\n\ + --decompress-bc0 [input-filename [output-filename]]\n\ + Decompress data using the BC0 algorithm. Both input-filename and\n\ output-filename may be specified.\n\ --encrypt-data\n\ --decrypt-data\n\ @@ -309,7 +309,7 @@ enum class Behavior { RUN_SERVER = 0, COMPRESS_PRS, DECOMPRESS_PRS, - DECOMPRESS_GJS, + DECOMPRESS_BC0, ENCRYPT_DATA, DECRYPT_DATA, FIND_DECRYPTION_SEED, @@ -323,7 +323,7 @@ enum class Behavior { static bool behavior_takes_input_filename(Behavior b) { return (b == Behavior::COMPRESS_PRS) || (b == Behavior::DECOMPRESS_PRS) || - (b == Behavior::DECOMPRESS_GJS) || + (b == Behavior::DECOMPRESS_BC0) || (b == Behavior::ENCRYPT_DATA) || (b == Behavior::DECRYPT_DATA) || (b == Behavior::DECODE_QUEST_FILE) || @@ -335,7 +335,7 @@ static bool behavior_takes_input_filename(Behavior b) { static bool behavior_takes_output_filename(Behavior b) { return (b == Behavior::COMPRESS_PRS) || (b == Behavior::DECOMPRESS_PRS) || - (b == Behavior::DECOMPRESS_GJS) || + (b == Behavior::DECOMPRESS_BC0) || (b == Behavior::ENCRYPT_DATA) || (b == Behavior::DECRYPT_DATA) || (b == Behavior::DECODE_SJIS); @@ -374,8 +374,8 @@ int main(int argc, char** argv) { behavior = Behavior::COMPRESS_PRS; } else if (!strcmp(argv[x], "--decompress-prs")) { behavior = Behavior::DECOMPRESS_PRS; - } else if (!strcmp(argv[x], "--decompress-gjs")) { - behavior = Behavior::DECOMPRESS_PRS; + } else if (!strcmp(argv[x], "--decompress-bc0")) { + behavior = Behavior::DECOMPRESS_BC0; } else if (!strcmp(argv[x], "--encrypt-data")) { behavior = Behavior::ENCRYPT_DATA; } else if (!strcmp(argv[x], "--decrypt-data")) { @@ -473,21 +473,24 @@ int main(int argc, char** argv) { switch (behavior) { case Behavior::COMPRESS_PRS: case Behavior::DECOMPRESS_PRS: - case Behavior::DECOMPRESS_GJS: { + case Behavior::DECOMPRESS_BC0: { string data = read_input_data(); if (parse_data) { data = parse_data_string(data); } + size_t input_bytes = data.size(); if (behavior == Behavior::COMPRESS_PRS) { data = prs_compress(data); } else if (behavior == Behavior::DECOMPRESS_PRS) { data = prs_decompress(data); - } else if (behavior == Behavior::DECOMPRESS_GJS) { - data = gjs_decompress(data); + } else if (behavior == Behavior::DECOMPRESS_BC0) { + data = bc0_decompress(data); } else { throw logic_error("invalid behavior"); } + log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output", + input_bytes, input_bytes, data.size(), data.size()); write_output_data(data.data(), data.size()); break;