diff --git a/README.md b/README.md index 8f80f6a6..6c38eea6 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,9 @@ Current known issues / missing features / things to do: - Implement private and overflow lobbies. - Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.) - Encapsulate BB server-side random state and make replays deterministic. -- Make a looser form of item tracking that can be used on non-BB versions when quests replace player inventories, like in battle and challenge modes. +- Implement character and inventory replacement for battle and challenge modes. +- Implement the C5 (battle/challenge records) command. +- Implement choice search. - Episode 3 bugs - Fix behavior when joining a spectator team after the beginning of a battle. - Disconnecting during a match turns you into a COM if there are other humans in the match, even if the match is part of a tournament. This may be incorrect behavior for tournaments. diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index af6c266b..c95c0b90 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1098,6 +1098,101 @@ struct C_OpenFileConfirmation_44_A6 { // the client crashes! Essentially, it reflects the saved state of the player's // character rather than the live state. +struct PlayerRecordsEntry_DC { + /* 00 */ le_uint32_t client_id; + /* 04 */ PlayerRecordsDC_Challenge challenge; + /* A4 */ PlayerRecords_Battle battle; + /* BC */ +} __packed__; + +struct PlayerRecordsEntry_PC { + /* 00 */ le_uint32_t client_id; + /* 04 */ PlayerRecordsPC_Challenge challenge; + /* DC */ PlayerRecords_Battle battle; + /* F4 */ +} __packed__; + +struct PlayerRecordsEntry_V3 { + /* 0000 */ le_uint32_t client_id; + /* 0004 */ PlayerRecordsV3_Challenge challenge; + /* 0104 */ PlayerRecords_Battle battle; + /* 011C */ +} __packed__; + +struct PlayerRecordsEntry_BB { + /* 0000 */ le_uint32_t client_id; + /* 0004 */ PlayerRecordsBB_Challenge challenge; + /* 0144 */ PlayerRecords_Battle battle; + /* 015C */ +} __packed__; + +struct C_CharacterData_DCv1_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ +} __attribute__((packed)); + +struct C_CharacterData_DCv2_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_DC records; + /* 04D8 */ ChoiceSearchConfig choice_search_config; + /* 04F0 */ +} __attribute__((packed)); + +struct C_CharacterData_PC_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_PC records; + /* 0510 */ ChoiceSearchConfig choice_search_config; + /* 0528 */ parray blocked_senders; + /* 05A0 */ le_uint32_t auto_reply_enabled; + // The auto-reply message can be up to 0x200 characters. If it's shorter than + // that, the client truncates the command after the first null value (rounded + // up to the next 4-byte boundary). + /* 05A4 */ char16_t auto_reply[0]; +} __attribute__((packed)); + +struct C_CharacterData_V3_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_V3 records; + /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0550 */ ptext info_board; + /* 05FC */ parray blocked_senders; + /* 0674 */ le_uint32_t auto_reply_enabled; + // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, + // the client truncates the command after the first zero byte (rounded up to + // the next 4-byte boundary). + /* 0678 */ char auto_reply[0]; +} __attribute__((packed)); + +struct C_CharacterData_GC_Ep3_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_V3 records; + /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0550 */ ptext info_board; + /* 05FC */ parray blocked_senders; + /* 0674 */ le_uint32_t auto_reply_enabled; + /* 0678 */ ptext auto_reply; + /* 0724 */ Episode3::PlayerConfig ep3_config; + /* 2A74 */ +} __attribute__((packed)); + +struct C_CharacterData_BB_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataBB disp; + /* 04DC */ PlayerRecordsEntry_BB records; + /* 0638 */ ChoiceSearchConfig choice_search_config; + /* 0650 */ ptext info_board; + /* 07A8 */ parray blocked_senders; + /* 0820 */ le_uint32_t auto_reply_enabled; + // Like on V3, the client truncates the command if the auto reply message is + // shorter than 0x200 bytes. + /* 082C */ char16_t auto_reply[0]; +} __attribute__((packed)); + // 62: Target command // Internal name: SndPsoData2 // When a client sends this command, the server should forward it to the player @@ -2370,17 +2465,12 @@ struct S_ChoiceSearchResultEntry_V3_C4 { parray unused2; } __packed__; -// C5 (S->C): Challenge rank update (DCv2 and later versions) +// C5 (S->C): Player records update (DCv2 and later versions) // Internal name: RcvChallengeData -// header.flag = entry count +// Command is a list of PlayerRecordsEntry structures; header.flag specifies +// the entry count. // The server sends this command when a player joins a lobby to update the // challenge mode records of all the present players. -// Entry format is PlayerChallengeDataV3 or PlayerChallengeDataBB. -// newserv currently doesn't send this command at all because the V3 and -// BB formats aren't fully documented. -// TODO: Figure out where the text is in those formats, write appropriate -// conversion functions, and implement the command. Don't forget to overwrite -// the client_id field in each entry before sending. // C6 (C->S): Set blocked senders list (V3/BB) // The command always contains the same number of entries, even if the entries @@ -2974,10 +3064,35 @@ struct C_CreateSpectatorTeam_GC_Ep3_E7 { // E7 (S->C): Unknown (Episode 3) // Same format as E2 command. -// E7: Save or load full player data (BB) -// See export_bb_player_data() in Player.cc for format. +// E7: Save or load full character data (BB) // TODO: Verify full breakdown from send_E7 in BB disassembly. +struct SC_SyncCharacterSaveFile_BB_00E7 { + /* 0000 */ PlayerInventory inventory; // From player data + /* 034C */ PlayerDispDataBB disp; // From player data + /* 04DC */ le_uint32_t unknown_a1; + /* 04E0 */ le_uint32_t creation_timestamp; + /* 04E4 */ le_uint32_t signature; // == 0xA205B064 (see SaveFileFormats.hh) + /* 04E8 */ le_uint32_t play_time_seconds; + /* 04EC */ le_uint32_t option_flags; // account + /* 04F0 */ parray quest_data1; // player + /* 06F8 */ PlayerBank bank; // player + /* 19C0 */ GuildCardBB guild_card; + /* 1AC8 */ le_uint32_t unknown_a3; + /* 1ACC */ parray symbol_chats; // account + /* 1FAC */ parray shortcuts; // account + /* 29EC */ ptext auto_reply; // player + /* 2B44 */ ptext info_board; // player + /* 2C9C */ PlayerRecords_Battle battle_records; + /* 2CB4 */ parray unknown_a4; + /* 2CB8 */ PlayerRecordsBB_Challenge challenge_records; + /* 2DF8 */ parray tech_menu_config; // player + /* 2E20 */ parray unknown_a6; + /* 2E4C */ parray quest_data2; // player + /* 2EA4 */ KeyAndTeamConfigBB key_config; // account + /* 3994 */ +} __attribute__((packed)); + // E8 (S->C): Join spectator team (Episode 3) // header.flag = player count (including spectators) diff --git a/src/Main.cc b/src/Main.cc index f2117411..907305e4 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -144,6 +144,9 @@ The actions are:\n\ same way, but if it is not given, newserv will try all possible basis\n\ values and return the one that results in the greatest number of zero bytes\n\ in the output.\n\ + encrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encrypt or decrypt data using the challenge mode trivial algorithm.\n\ encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ Encrypt or decrypt a character or Guild Card file in GCI format. If\n\ @@ -252,6 +255,8 @@ enum class Behavior { DECRYPT_DATA, ENCRYPT_TRIVIAL_DATA, DECRYPT_TRIVIAL_DATA, + ENCRYPT_CHALLENGE_DATA, + DECRYPT_CHALLENGE_DATA, ENCRYPT_GCI_SAVE, DECRYPT_GCI_SAVE, FIND_DECRYPTION_SEED, @@ -289,6 +294,8 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::ENCRYPT_CHALLENGE_DATA) || + (b == Behavior::DECRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::SALVAGE_GCI) || (b == Behavior::ENCRYPT_GCI_SAVE) || @@ -318,6 +325,8 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::ENCRYPT_CHALLENGE_DATA) || + (b == Behavior::DECRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || @@ -471,6 +480,10 @@ 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], "encrypt-challenge-data")) { + behavior = Behavior::ENCRYPT_CHALLENGE_DATA; + } else if (!strcmp(argv[x], "decrypt-challenge-data")) { + behavior = Behavior::DECRYPT_CHALLENGE_DATA; } else if (!strcmp(argv[x], "decrypt-gci-save")) { behavior = Behavior::DECRYPT_GCI_SAVE; } else if (!strcmp(argv[x], "encrypt-gci-save")) { @@ -813,6 +826,17 @@ int main(int argc, char** argv) { break; } + case Behavior::ENCRYPT_CHALLENGE_DATA: + case Behavior::DECRYPT_CHALLENGE_DATA: { + string data = read_input_data(); + const uint8_t* u8data = reinterpret_cast(data.data()); + string result = (behavior == Behavior::DECRYPT_CHALLENGE_DATA) + ? decrypt_challenge_rank_text(u8data, data.size()) + : encrypt_challenge_rank_text(u8data, data.size()); + write_output_data(result.data(), result.size()); + break; + } + case Behavior::ENCRYPT_GCI_SAVE: case Behavior::DECRYPT_GCI_SAVE: { uint32_t round1_seed; diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index 7211dafb..9111532c 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -885,3 +885,44 @@ void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis) { bytes[z] ^= key; } } + +template +StringT crypt_challenge_rank_text(const void* src, size_t size) { + if (size == 0) { + return StringT(); + } + + StringT ret; + StringReader r(src, size); + typename StringT::value_type prev = 0; + while (!r.eof()) { + typename StringT::value_type ch = r.get(); + if (ch == 0) { + break; + } + if (ret.empty()) { + ret.push_back(ch ^ 0x7F); + } else { + ret.push_back(IsEncrypt ? ((ch - prev) ^ 0x7F) : ((ch ^ 0x7F) + ret.back())); + } + prev = ch; + } + + return ret; +} + +string encrypt_challenge_rank_text(const uint8_t* src, size_t size) { + return crypt_challenge_rank_text(src, size); +} + +string decrypt_challenge_rank_text(const uint8_t* src, size_t size) { + return crypt_challenge_rank_text(src, size); +} + +u16string encrypt_challenge_rank_text(const char16_t* src, size_t size) { + return crypt_challenge_rank_text(src, size); +} + +u16string decrypt_challenge_rank_text(const char16_t* src, size_t size) { + return crypt_challenge_rank_text(src, size); +} diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index e324dba5..62f92998 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -240,3 +240,8 @@ private: }; void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis); + +std::string decrypt_challenge_rank_text(const uint8_t* data, size_t size); +std::string encrypt_challenge_rank_text(const uint8_t* data, size_t size); +std::u16string decrypt_challenge_rank_text(const le_uint16_t* data, size_t size); +std::u16string encrypt_challenge_rank_text(const le_uint16_t* data, size_t size); diff --git a/src/Player.cc b/src/Player.cc index 9a851979..6a6c644e 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -368,82 +368,6 @@ void ClientGameData::save_player_data() { } } -void ClientGameData::import_player(const PSOPlayerDataDCPC& pd) { - auto player = this->player(); - player->inventory = pd.inventory; - player->disp = pd.disp.to_bb(); - // TODO: Add these fields to the command structure so we can parse them - // info_board = pd.info_board; - // blocked_senders = pd.blocked_senders; - // auto_reply = pd.auto_reply; -} - -void ClientGameData::import_player(const PSOPlayerDataV3& gc, bool is_gc) { - auto account = this->account(); - auto player = this->player(); - player->inventory = gc.inventory; - if (is_gc) { - for (size_t z = 0; z < 30; z++) { - player->inventory.items[z].data.bswap_data2_if_mag(); - } - } - player->disp = gc.disp.to_bb(); - player->info_board = gc.info_board; - account->blocked_senders = gc.blocked_senders; - if (gc.auto_reply_enabled) { - player->auto_reply = gc.auto_reply; - } else { - player->auto_reply.clear(0); - } -} - -void ClientGameData::import_player(const PSOPlayerDataBB& bb) { - auto account = this->account(); - auto player = this->player(); - // Note: we don't copy the inventory and disp here because we already have - // them (we sent the player data to the client in the first place) - player->info_board = bb.info_board; - account->blocked_senders = bb.blocked_senders; - if (bb.auto_reply_enabled) { - player->auto_reply = bb.auto_reply; - } else { - player->auto_reply.clear(0); - } -} - -PlayerBB ClientGameData::export_player_bb() { - auto account = this->account(); - auto player = this->player(); - - PlayerBB ret; - ret.inventory = player->inventory; - ret.disp = player->disp; - ret.unknown.clear(0); - ret.option_flags = account->option_flags; - ret.quest_data1 = player->quest_data1; - ret.bank = player->bank; - ret.guild_card_number = this->guild_card_number; - ret.name = player->disp.name; - ret.team_name = account->team_name; - ret.guild_card_description = player->guild_card_description; - ret.reserved1 = 0; - ret.reserved2 = 0; - ret.section_id = player->disp.visual.section_id; - ret.char_class = player->disp.visual.char_class; - ret.unknown3 = 0; - ret.symbol_chats = account->symbol_chats; - ret.shortcuts = account->shortcuts; - ret.auto_reply = player->auto_reply; - ret.info_board = player->info_board; - ret.unknown5.clear(0); - ret.challenge_data = player->challenge_data; - ret.tech_menu_config = player->tech_menu_config; - ret.unknown6.clear(0); - ret.quest_data2 = player->quest_data2; - ret.key_config = account->key_config; - return ret; -} - void PlayerLobbyDataPC::clear() { this->player_tag = 0; this->guild_card = 0; diff --git a/src/Player.hh b/src/Player.hh index 10931b92..23050cd1 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -182,13 +182,14 @@ struct PlayerDispDataBBPreview { // BB player appearance and stats data struct PlayerDispDataBB { - PlayerStats stats; - PlayerVisualConfig visual; - ptext name; - le_uint32_t play_time; - uint32_t unknown_a3; - parray config; - parray technique_levels; + /* 0000 */ PlayerStats stats; + /* 0024 */ PlayerVisualConfig visual; + /* 0074 */ ptext name; + /* 008C */ le_uint32_t play_time; + /* 0090 */ uint32_t unknown_a3; + /* 0094 */ parray config; + /* 017C */ parray technique_levels; + /* 0190 */ PlayerDispDataBB() noexcept; @@ -217,14 +218,15 @@ struct GuildCardV3 { // BB guild card format struct GuildCardBB { - le_uint32_t guild_card_number; - ptext name; - ptext team_name; - ptext description; - uint8_t present; // should be 1 if guild card entry exists - uint8_t language; - uint8_t section_id; - uint8_t char_class; + /* 0000 */ le_uint32_t guild_card_number; + /* 0004 */ ptext name; + /* 0034 */ ptext team_name; + /* 0054 */ ptext description; + /* 0104 */ uint8_t present; // should be 1 if guild card entry exists + /* 0105 */ uint8_t language; + /* 0106 */ uint8_t section_id; + /* 0107 */ uint8_t char_class; + /* 0108 */ GuildCardBB() noexcept; void clear(); @@ -324,39 +326,84 @@ struct PlayerLobbyDataBB { void clear(); } __attribute__((packed)); -struct PlayerChallengeDataV3 { - le_uint32_t client_id; - struct { - le_uint16_t unknown_a1; - parray unknown_a2; // Possibly unused - parray unknown_a3; - struct { - parray unknown_a1; - le_uint16_t unknown_a2; - parray unknown_a3; - parray unknown_a4; - parray unknown_a5; - } __attribute__((packed)) unknown_a4; // 0x50 bytes - struct { - parray unknown_a1; - parray unknown_a2; - } __attribute__((packed)) unknown_a5; // 0x10 bytes - struct UnknownPair { - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; - } __attribute__((packed)); - parray unknown_a6; // 0x18 bytes - parray unknown_a7; - } __attribute__((packed)) unknown_a1; // 0x100 bytes - // On Episode 3, unknown_a2[0] is win count, [1] is loss count, and [4] is - // disconnect count - parray unknown_a2; - parray unknown_a3; -} __attribute__((packed)); // 0x11C bytes +template +struct PlayerRecordsDCPC_Challenge { + using CharT = typename std::conditional::type; + using CharBinT = typename std::conditional::type; -struct PlayerChallengeDataBB { - le_uint32_t client_id; - parray unknown_a1; + /* 00 */ le_uint16_t title_color; + /* 02 */ parray unknown_a0; + /* 04 */ parray rank_title; // Encrypted; see decrypt_challenge_rank_text + /* 10 */ parray unknown_a1; // TODO: This might be online or offline times + /* 34 */ le_uint16_t unknown_a2; + /* 36 */ le_uint16_t grave_deaths; + /* 38 */ parray grave_coords_time; + /* 4C */ ptext grave_team; + /* 60 */ ptext grave_message; + /* 78 */ parray times_ep1; // TODO: This might be offline times + /* 9C */ parray unknown_a3; + /* A0 */ +} __attribute__((packed)); + +struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge { +} __attribute__((packed)); +struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge { +} __attribute__((packed)); + +template +struct PlayerRecordsV3_Challenge { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + + // Offsets are (1) relative to start of C5 entry, and (2) relative to start + // of save file structure + /* 0000:001C */ U16T title_color; // XRGB1555 + /* 0002:001E */ parray unknown_a2; // Probably actually unused + /* 0004:0020 */ parray times_ep1_online; + /* 0028:0044 */ parray times_ep2_online; + /* 003C:0058 */ parray times_ep1_offline; + /* 0060:007C */ parray unknown_a3; + /* 0064:0080 */ U16T grave_deaths; + /* 0066:0082 */ parray unknown_a4; // Probably actually unused + /* 0068:0084 */ parray grave_coords_time; + /* 007C:0098 */ ptext grave_team; + /* 0090:00AC */ ptext grave_message; + /* 00B0:00CC */ parray unknown_a5; + /* 00B4:00D0 */ parray unknown_a6; + /* 00D8:00F4 */ parray rank_title; // Encrypted; see decrypt_challenge_rank_text + /* 00E4:0100 */ parray unknown_a7; + /* 0100:011C */ +} __attribute__((packed)); + +struct PlayerRecordsBB_Challenge { + // TODO: Figure out the rest of this structure. Probably it's very similar to + // the V3 structure, but it's a bit larger due to various text fields. + /* 0000 */ le_uint16_t title_color; // XRGB1555 + /* 0002 */ parray unknown_a2; // Probably actually unused + /* 0004 */ parray times_ep1; + /* 0028 */ parray times_ep2; + /* 003C */ parray times_ep1_offline; + /* 0060 */ parray unknown_a3; + /* 0064 */ le_uint16_t grave_deaths; + /* 0066 */ parray unknown_a4; // Probably actually unused + /* 0068 */ parray grave_coords_time; + /* 007C */ ptext grave_team; + /* 00A4 */ ptext grave_message; + /* 00E4 */ parray unknown_a5; + /* 00E8 */ parray unknown_a6; + /* 010C */ parray rank_title; // Encrypted; see decrypt_challenge_rank_text + /* 0124 */ parray unknown_a7; + /* 0140 */ +} __attribute__((packed)); + +template +struct PlayerRecords_Battle { + using U16T = typename std::conditional::type; + // On Episode 3, battle_place_counts[0] is win count and [1] is loss count + /* 00 */ parray place_counts; + /* 08 */ U16T disconnect_count; + /* 0A */ parray unknown_a1; + /* 18 */ } __attribute__((packed)); template @@ -370,82 +417,12 @@ struct ChoiceSearchConfig { parray entries; } __attribute__((packed)); -struct PSOPlayerDataDCPC { // For command 61 - PlayerInventory inventory; - PlayerDispDataDCPCV3 disp; -} __attribute__((packed)); - -struct PSOPlayerDataV3 { // For command 61 - PlayerInventory inventory; - PlayerDispDataDCPCV3 disp; - PlayerChallengeDataV3 challenge_data; - ChoiceSearchConfig choice_search_config; - ptext info_board; - parray blocked_senders; - le_uint32_t auto_reply_enabled; - // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, - // the client truncates the command after the first zero byte (rounded up to - // the next 4-byte boundary). - char auto_reply[0]; -} __attribute__((packed)); - -struct PSOPlayerDataGCEp3 { // For command 61 - PlayerInventory inventory; - PlayerDispDataDCPCV3 disp; - PlayerChallengeDataV3 challenge_data; - ChoiceSearchConfig choice_search_config; - ptext info_board; - parray blocked_senders; - le_uint32_t auto_reply_enabled; - ptext auto_reply; - Episode3::PlayerConfig ep3_config; -} __attribute__((packed)); - -struct PSOPlayerDataBB { // For command 61 - PlayerInventory inventory; - PlayerDispDataBB disp; - PlayerChallengeDataBB challenge_data; - ChoiceSearchConfig choice_search_config; - ptext info_board; - parray blocked_senders; - le_uint32_t auto_reply_enabled; - char16_t auto_reply[0]; -} __attribute__((packed)); - -struct PlayerBB { // Used in 00E7 command - PlayerInventory inventory; // 0000-034C; player - PlayerDispDataBB disp; // 034C-04DC; player - parray unknown; // 04DC-04EC; not saved - le_uint32_t option_flags; // 04EC-04F0; account - parray quest_data1; // 04F0-06F8; player - PlayerBank bank; // 06F8-19C0; player - le_uint32_t guild_card_number; // 19C0-19C4; player - ptext name; // 19C4-19F4; player - ptext team_name; // 19F4-1A14; player - ptext guild_card_description; // 1A14-1AC4; player - uint8_t reserved1; // 1AC4-1AC5; player - uint8_t reserved2; // 1AC5-1AC6; player - uint8_t section_id; // 1AC6-1AC7; player - uint8_t char_class; // 1AC7-1AC8; player - le_uint32_t unknown3; // 1AC8-1ACC; not saved - parray symbol_chats; // 1ACC-1FAC; account - parray shortcuts; // 1FAC-29EC; account - ptext auto_reply; // 29EC-2B44; player - ptext info_board; // 2B44-2C9C; player - parray unknown5; // 2C9C-2CB8; not saved - parray challenge_data; // 2CB8-2DF8; player - parray tech_menu_config; // 2DF8-2E20; player - parray unknown6; // 2E20-2E4C; not saved - parray quest_data2; // 2E4C-2EA4; player - KeyAndTeamConfigBB key_config; // 2EA4-3994; account -} __attribute__((packed)); - struct SavedPlayerDataBB { // .nsc file format ptext signature; PlayerDispDataBBPreview preview; ptext auto_reply; PlayerBank bank; - parray challenge_data; + PlayerRecordsBB_Challenge challenge_records; PlayerDispDataBB disp; ptext guild_card_description; ptext info_board; @@ -453,6 +430,9 @@ struct SavedPlayerDataBB { // .nsc file format parray quest_data1; parray quest_data2; parray tech_menu_config; + // TODO: We don't save battle records in this structure, which is wrong. + // Make a new save file format that doesn't have the super-long signature, + // and also can save battle records. void add_item(const PlayerInventoryItem& item); PlayerInventoryItem remove_item( @@ -524,13 +504,6 @@ public: void load_player_data(); // Note: This function is not const because it updates the player's play time. void save_player_data(); - - void import_player(const PSOPlayerDataDCPC& pd); - void import_player(const PSOPlayerDataV3& pd, bool is_gc); - void import_player(const PSOPlayerDataBB& pd); - // Note: this function is not const because it can cause player and account - // data to be loaded - PlayerBB export_player_bb(); }; uint32_t compute_guild_card_checksum(const void* data, size_t size); diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 76d9a938..fa8e2035 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -1017,7 +1017,7 @@ static HandlerResult C_GXB_61(shared_ptr, // return MODIFIED if so. if (session.version == GameVersion::BB) { - auto& pd = check_size_t(data, 0xFFFF); + auto& pd = check_size_t(data, 0xFFFF); if (session.options.enable_chat_filter) { add_color_inplace(pd.info_board.data(), pd.info_board.size()); } @@ -1034,9 +1034,9 @@ static HandlerResult C_GXB_61(shared_ptr, } } else { - PSOPlayerDataV3* pd; + C_CharacterData_V3_61_98* pd; if (flag == 4) { // Episode 3 - auto& ep3_pd = check_size_t(data); + auto& ep3_pd = check_size_t(data); if (ep3_pd.ep3_config.is_encrypted) { decrypt_trivial_gci_data( &ep3_pd.ep3_config.card_counts, @@ -1046,9 +1046,9 @@ static HandlerResult C_GXB_61(shared_ptr, ep3_pd.ep3_config.basis = 0; modified = true; } - pd = reinterpret_cast(&ep3_pd); + pd = reinterpret_cast(&ep3_pd); } else { - pd = &check_size_t(data, 0xFFFF); + pd = &check_size_t(data, 0xFFFF); } if (session.options.enable_chat_filter) { add_color_inplace(pd->info_board.data(), pd->info_board.size()); diff --git a/src/QuestScript.cc b/src/QuestScript.cc index f15a14d3..edfc0a81 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -644,8 +644,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF872, "ba_set_time_limit", {INT32}, {}, V2, V2}, {0xF872, "ba_set_time_limit", {}, {INT32}, V3, V4}, {0xF873, "dark_falz_is_dead", {REG}, {}, V2, V4}, - {0xF874, nullptr, {INT32, CSTRING}, {}, V2, V2}, // TODO (DX) - Similar to A0, but does something with the two strings in non-4P challenge mode - {0xF874, nullptr, {}, {INT32, CSTRING}, V3, V4}, // TODO (DX) + {0xF874, "set_cmode_rank_override", {INT32, CSTRING}, {}, V2, V2}, // argA is an XRGB8888 color, argB is two strings separated by \t or \n: the rank text to check for, and the rank text that should replace it if found + {0xF874, "set_cmode_rank_override", {}, {INT32, CSTRING}, V3, V4}, {0xF875, "enable_stealth_suit_effect", {REG}, {}, V2, V4}, {0xF876, "disable_stealth_suit_effect", {REG}, {}, V2, V4}, {0xF877, "enable_techs", {REG}, {}, V2, V4}, diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 1852b02d..53afae28 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2435,33 +2435,88 @@ static void on_61_98(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { switch (c->version()) { - case GameVersion::DC: + case GameVersion::DC: { + if (c->flags & Client::Flag::IS_DC_V1) { + const auto& pd = check_size_t(data); + auto player = c->game_data.player(); + player->inventory = pd.inventory; + player->disp = pd.disp.to_bb(); + } else { + const auto& pd = check_size_t(data, 0xFFFF); + auto player = c->game_data.player(); + player->inventory = pd.inventory; + player->disp = pd.disp.to_bb(); + // TODO: Parse pd.records and send C5 at an appropriate time + // TODO: Parse choice search config + } + break; + } case GameVersion::PC: { - const auto& pd = check_size_t(data, 0xFFFF); - c->game_data.import_player(pd); + const auto& pd = check_size_t(data, 0xFFFF); + auto player = c->game_data.player(); + auto account = c->game_data.account(); + player->inventory = pd.inventory; + player->disp = pd.disp.to_bb(); + // TODO: Parse pd.records and send C5 at an appropriate time + // TODO: Parse choice search config + account->blocked_senders = pd.blocked_senders; + if (pd.auto_reply_enabled) { + player->auto_reply = pd.auto_reply; + } else { + player->auto_reply.clear(0); + } break; } case GameVersion::GC: case GameVersion::XB: { - const PSOPlayerDataV3* pd; + const C_CharacterData_V3_61_98* cmd; if (flag == 4) { // Episode 3 if (!(c->flags & Client::Flag::IS_EPISODE_3)) { throw runtime_error("non-Episode 3 client sent Episode 3 player data"); } - const auto* pd3 = &check_size_t(data); + const auto* pd3 = &check_size_t(data); c->game_data.ep3_config.reset(new Episode3::PlayerConfig(pd3->ep3_config)); - pd = reinterpret_cast(pd3); + cmd = reinterpret_cast(pd3); } else { - pd = &check_size_t(data, - sizeof(PSOPlayerDataV3) + c->game_data.player()->auto_reply.bytes()); + cmd = &check_size_t(data, 0xFFFF); } - c->game_data.import_player(*pd, c->version() == GameVersion::GC); + + auto account = c->game_data.account(); + auto player = c->game_data.player(); + player->inventory = cmd->inventory; + if (c->version() == GameVersion::GC) { + for (size_t z = 0; z < 30; z++) { + player->inventory.items[z].data.bswap_data2_if_mag(); + } + } + player->disp = cmd->disp.to_bb(); + // TODO: Parse cmd->records and send C5 at an appropriate time + // TODO: Parse choice search config + player->info_board = cmd->info_board; + account->blocked_senders = cmd->blocked_senders; + if (cmd->auto_reply_enabled) { + player->auto_reply = cmd->auto_reply; + } else { + player->auto_reply.clear(0); + } + break; } case GameVersion::BB: { - const auto& pd = check_size_t(data, - sizeof(PSOPlayerDataBB) + c->game_data.player()->auto_reply.bytes()); - c->game_data.import_player(pd); + const auto& cmd = check_size_t(data, 0xFFFF); + auto account = c->game_data.account(); + auto player = c->game_data.player(); + // Note: we don't copy the inventory and disp here because we already have + // them (we sent the player data to the client in the first place) + // TODO: Parse pd.records and send C5 at an appropriate time + // TODO: Parse choice search config + player->info_board = cmd.info_board; + account->blocked_senders = cmd.blocked_senders; + if (cmd.auto_reply_enabled) { + player->auto_reply = cmd.auto_reply; + } else { + player->auto_reply.clear(0); + } break; } default: @@ -2901,10 +2956,14 @@ static void on_xxED_BB(shared_ptr, shared_ptr c, static void on_00E7_BB(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); // We only trust the player's quest data and challenge data. - c->game_data.player()->challenge_data = cmd.challenge_data; + // TODO: In the future, we shouldn't even need to trust these fields. We + // should instead verify our copy of the player against what the client sent, + // and alert on anything that's out of sync. + // TODO: In the future, we should save battle records here too. + c->game_data.player()->challenge_records = cmd.challenge_records; c->game_data.player()->quest_data1 = cmd.quest_data1; c->game_data.player()->quest_data2 = cmd.quest_data2; } diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index ef377c9e..8d13f272 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -125,14 +125,17 @@ struct PSOGCSaveFileChatShortcutEntry { struct PSOGCCharacterFile { /* 00000 */ be_uint32_t checksum; struct Character { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ be_uint32_t unknown_a1; - /* 0420 */ be_uint32_t creation_timestamp; + // This structure is internally split into two by the game. The offsets here + // are relative to the start of this structure (first column), and relative + // to the start of the second internal structure (second column). + /* 0000:---- */ PlayerInventory inventory; + /* 034C:---- */ PlayerDispDataDCPCV3 disp; + /* 041C:0000 */ be_uint32_t unknown_a1; + /* 0420:0004 */ be_uint32_t creation_timestamp; // The signature field holds the value 0xA205B064, which is 2718281828 in // decimal - approximately e * 10^9. It's unknown why Sega chose this value. - /* 0424 */ be_uint32_t signature; - /* 0428 */ be_uint32_t play_time_seconds; + /* 0424:0008 */ be_uint32_t signature; + /* 0428:000C */ be_uint32_t play_time_seconds; // This field is a collection of several flags and small values. The known // fields are: // ------zA BCDEFG-- HHHIIIJJ KLMNOPQR @@ -151,19 +154,21 @@ struct PSOGCCharacterFile { // P = Cursor position (0 = saved; 1 = non-saved) // Q = Button config (0 = normal; 1 = L/R reversed) // R = Map direction (0 = non-fixed; 1 = fixed) - /* 042C */ be_uint32_t option_flags; - /* 0430 */ be_uint32_t save_count; - /* 0434 */ parray unknown_a4; - /* 0664 */ PlayerBank bank; - /* 192C */ GuildCardV3 guild_card; - /* 19BC */ parray symbol_chats; - /* 1DDC */ parray chat_shortcuts; - /* 246C */ ptext auto_reply; - /* 2518 */ ptext info_board; - /* 25C4 */ parray unknown_a5; - /* 26E0 */ parray tech_menu_shortcut_entries; - /* 2708 */ parray unknown_a6; - /* 2798 */ + /* 042C:0010 */ be_uint32_t option_flags; + /* 0430:0014 */ be_uint32_t save_count; + /* 0434:0018 */ parray unknown_a4; + /* 0664:0248 */ PlayerBank bank; + /* 192C:1510 */ GuildCardV3 guild_card; + /* 19BC:15A0 */ parray symbol_chats; + /* 1DDC:19C0 */ parray chat_shortcuts; + /* 246C:2050 */ ptext auto_reply; + /* 2518:20FC */ ptext info_board; + /* 25C4:21A8 */ PlayerRecords_Battle battle_records; + /* 25DC:21C0 */ parray unknown_a2; + /* 25E0:21C4 */ PlayerRecordsV3_Challenge challenge_records; + /* 26E0:22C4 */ parray tech_menu_shortcut_entries; + /* 2708:22EC */ parray unknown_a6; + /* 2798:237C */ } __attribute__((packed)); /* 00004 */ parray characters; /* 1152C */ ptext serial_number; // As %08X (not decimal) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 3212c088..858cb26a 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -606,7 +606,43 @@ void send_approve_player_choice_bb(shared_ptr c) { } void send_complete_player_bb(shared_ptr c) { - send_command_t(c, 0x00E7, 0x00000000, c->game_data.export_player_bb()); + auto account = c->game_data.account(); + auto player = c->game_data.player(); + + SC_SyncCharacterSaveFile_BB_00E7 cmd; + cmd.inventory = player->inventory; + cmd.disp = player->disp; + cmd.unknown_a1 = 0; + cmd.creation_timestamp = 0; + cmd.signature = 0xA205B064; + cmd.play_time_seconds = 0; // TODO: Can we just use the same value as in disp? + cmd.option_flags = account->option_flags; + cmd.quest_data1 = player->quest_data1; + cmd.bank = player->bank; + cmd.guild_card.guild_card_number = c->game_data.guild_card_number; + cmd.guild_card.name = player->disp.name; + cmd.guild_card.team_name = account->team_name; + cmd.guild_card.description = player->guild_card_description; + cmd.guild_card.present = 1; + cmd.guild_card.language = cmd.inventory.language; + cmd.guild_card.section_id = player->disp.visual.section_id; + cmd.guild_card.char_class = player->disp.visual.char_class; + cmd.unknown_a3 = 0; + cmd.symbol_chats = account->symbol_chats; + cmd.shortcuts = account->shortcuts; + cmd.auto_reply = player->auto_reply; + cmd.info_board = player->info_board; + cmd.battle_records.place_counts.clear(0); + cmd.battle_records.disconnect_count = 0; + cmd.battle_records.unknown_a1.clear(0); + cmd.unknown_a4 = 0; + cmd.challenge_records = player->challenge_records; + cmd.tech_menu_config = player->tech_menu_config; + cmd.unknown_a6.clear(0); + cmd.quest_data2 = player->quest_data2; + cmd.key_config = account->key_config; + + send_command_t(c, 0x00E7, 0x00000000, cmd); } ////////////////////////////////////////////////////////////////////////////////