From b901e8846d51ad2017ebbbad3838958d2a21f7fe Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 30 Jul 2022 00:25:35 -0700 Subject: [PATCH] implement some bb guild card commands --- README.md | 1 + src/ChatCommands.cc | 6 ++ src/CommandFormats.hh | 31 ++++------ src/Player.cc | 20 ++++++- src/Player.hh | 76 +++++++++++++----------- src/ReceiveCommands.cc | 122 +++++++++++++++++++++++++++++++++++--- src/ReceiveSubcommands.cc | 10 ++-- src/SendCommands.cc | 23 +++---- 8 files changed, 209 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index a93170ce..3fe6c5f4 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This project is primarily for my own nostalgia; I offer no guarantees on how or Current known issues / missing features: - Test all the communication features in cross-version scenarios involving BB (info board, simple mail, card search, etc.) +- Guild card file modifications don't save on BB. - Episode 3 battles aren't implemented. - PSOBB is not well-tested and likely will disconnect when clients try to use unimplemented features. GC is known to be stable and mostly complete; PC is not well-tested but is likely stable and complete as well. - Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 1d2eb270..b17606f7 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -194,6 +194,11 @@ static void server_command_dbgid(shared_ptr, shared_ptr, c->prefer_high_lobby_client_id = !c->prefer_high_lobby_client_id; } +static void server_command_get_self_card(shared_ptr, shared_ptr, + shared_ptr c, const std::u16string&) { + send_guild_card(c, c); +} + //////////////////////////////////////////////////////////////////////////////// // Lobby commands @@ -844,6 +849,7 @@ static const unordered_map chat_commands({ {u"$dbgid" , {server_command_dbgid , nullptr , u"Usage:\ndbgid"}}, {u"$edit" , {server_command_edit , nullptr , u"Usage:\nedit "}}, {u"$event" , {server_command_lobby_event , proxy_command_lobby_event , u"Usage:\nevent "}}, + {u"$gc" , {server_command_get_self_card , nullptr , u"Usage:\ngc"}}, {u"$infhp" , {server_command_infinite_hp , proxy_command_infinite_hp , u"Usage:\ninfhp"}}, {u"$inftp" , {server_command_infinite_tp , proxy_command_infinite_tp , u"Usage:\ninftp"}}, {u"$item" , {server_command_item , nullptr , u"Usage:\nitem "}}, diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 9dcb241f..adbd4931 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1405,7 +1405,7 @@ struct S_QuestMenuEntry { le_uint32_t menu_id; le_uint32_t item_id; ptext name; - ptext short_desc; + ptext short_description; }; struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { }; struct S_QuestMenuEntry_GC_A2_A4 : S_QuestMenuEntry { }; @@ -2261,14 +2261,14 @@ struct S_Unknown_GC_Ep3_E8 { // This struct is for documentation purposes only; newserv ignores the contents // of this command. -struct C_ClientChecksum_01E8 { +struct C_GuildCardChecksum_01E8 { le_uint32_t checksum; le_uint32_t unused; }; -// 02E8 (S->C): Accept guild card file checksum +// 02E8 (S->C): Accept/decline guild card file checksum -struct S_AcceptClientChecksum_BB_02E8 { +struct S_GuildCardChecksumResponse_BB_02E8 { le_uint32_t verify; le_uint32_t unused; }; @@ -2278,11 +2278,7 @@ struct S_AcceptClientChecksum_BB_02E8 { // Server should send the guild card file data using DC commands. // 04E8 (C->S): Add guild card - -struct C_AddOrUpdateGuildCard_BB_04E8_06E8_07E8 { - // TODO: Document this format - parray unknown_a1; -}; +// Format is GuildCardBB (see Player.hh) // 05E8 (C->S): Delete guild card @@ -2291,10 +2287,10 @@ struct C_DeleteGuildCard_BB_05E8_08E8 { }; // 06E8 (C->S): Set guild card text -// Same format as 04E8. +// Format is GuildCardBB (see Player.hh) // 07E8 (C->S): Add blocked user -// Same format as 04E8. +// Format is GuildCardBB (see Player.hh) // 08E8 (C->S): Delete blocked user // Same format as 05E8. @@ -2302,16 +2298,15 @@ struct C_DeleteGuildCard_BB_05E8_08E8 { // 09E8 (C->S): Write comment struct C_WriteGuildCardComment_BB_09E8 { - ptext comment; + le_uint32_t guild_card_number; + ptext comment; }; // 0AE8 (C->S): Set guild card position in list struct C_MoveGuildCard_BB_0AE8 { - // TODO: One of these is the GC number, the other is the position. Figure out - // which is which. - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; + le_uint32_t guild_card_number; + le_uint32_t position; }; // E9 (S->C): Other player left spectator team (probably) (Episode 3) @@ -2597,7 +2592,7 @@ struct G_SendGuildCard_PC_V3 { le_uint32_t player_tag; le_uint32_t guild_card_number; ptext name; - ptext desc; + ptext description; parray unused2; uint8_t reserved1; uint8_t reserved2; @@ -2614,7 +2609,7 @@ struct G_SendGuildCard_BB_6x06 { le_uint32_t guild_card_number; ptext name; ptext team_name; - ptext desc; + ptext description; uint8_t reserved1; uint8_t reserved2; uint8_t section_id; diff --git a/src/Player.cc b/src/Player.cc index 55e6df22..c318df78 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -276,12 +276,28 @@ GuildCardV3::GuildCardV3() noexcept char_class(0) { } GuildCardBB::GuildCardBB() noexcept - : serial_number(0), + : guild_card_number(0), reserved1(1), reserved2(1), section_id(0), char_class(0) { } +void GuildCardBB::clear() { + this->guild_card_number = 0; + this->name.clear(); + this->team_name.clear(); + this->description.clear(); + this->reserved1 = 1; + this->reserved2 = 1; + this->section_id = 0; + this->char_class = 0; +} + +void GuildCardEntryBB::clear() { + this->data.clear(); + this->unknown_a1.clear(); +} + void PlayerBank::load(const string& filename) { @@ -506,7 +522,7 @@ PlayerBB ClientGameData::export_player_bb() { ret.serial_number = this->serial_number; ret.name = player->disp.name; ret.team_name = account->team_name; - ret.guild_card_desc = player->guild_card_desc; + ret.guild_card_description = player->guild_card_description; ret.reserved1 = 0; ret.reserved2 = 0; ret.section_id = player->disp.section_id; diff --git a/src/Player.hh b/src/Player.hh index eb878784..c7deedc8 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -213,7 +213,7 @@ struct GuildCardV3 { le_uint32_t player_tag; le_uint32_t serial_number; ptext name; - ptext desc; + ptext description; uint8_t reserved1; // should be 1 uint8_t reserved2; // should be 1 uint8_t section_id; @@ -224,29 +224,37 @@ struct GuildCardV3 { // BB guild card format struct GuildCardBB { - le_uint32_t serial_number; + le_uint32_t guild_card_number; ptext name; - ptext teamname; - ptext desc; + ptext team_name; + ptext description; uint8_t reserved1; // should be 1 uint8_t reserved2; // should be 1 uint8_t section_id; uint8_t char_class; GuildCardBB() noexcept; + void clear(); } __attribute__((packed)); // an entry in the BB guild card file struct GuildCardEntryBB { GuildCardBB data; - parray unknown; + // TODO: Almost all of this space (0x58 char16_ts) is probably the comment, + // but is the unknown 4 bytes at the beginning or end of this space? + parray unknown_a1; + + void clear(); } __attribute__((packed)); // the format of the BB guild card file struct GuildCardFileBB { - parray unknown_a1; - GuildCardEntryBB entry[0x0068]; // that's 104 of them in decimal - parray unknown_a2; + parray unknown_a1; + uint8_t num_guild_cards; // TODO: This is a guess. Verify this. + uint8_t unknown_a2; + parray unknown_a3; + GuildCardEntryBB entries[0x0068]; // that's 104 of them in decimal + parray unknown_a4; } __attribute__((packed)); struct KeyAndTeamConfigBB { @@ -404,31 +412,31 @@ struct PSOPlayerDataBB { // For command 61 } __attribute__((packed)); struct PlayerBB { // Used in 00E7 command - PlayerInventory inventory; // player - PlayerDispDataBB disp; // player - parray unknown; // not saved - le_uint32_t option_flags; // account - parray quest_data1; // player - PlayerBank bank; // player - le_uint32_t serial_number; // player - ptext name; // player - ptext team_name; // player - ptext guild_card_desc; // player - uint8_t reserved1; // player - uint8_t reserved2; // player - uint8_t section_id; // player - uint8_t char_class; // player - le_uint32_t unknown3; // not saved - parray symbol_chats; // account - parray shortcuts; // account - ptext auto_reply; // player - ptext info_board; // player - parray unknown5; // not saved - parray challenge_data; // player - parray tech_menu_config; // player - parray unknown6; // not saved - parray quest_data2; // player - KeyAndTeamConfigBB key_config; // account + PlayerInventory inventory; // player + PlayerDispDataBB disp; // player + parray unknown; // not saved + le_uint32_t option_flags; // account + parray quest_data1; // player + PlayerBank bank; // player + le_uint32_t serial_number; // player + ptext name; // player + ptext team_name; // player + ptext guild_card_description; // player + uint8_t reserved1; // player + uint8_t reserved2; // player + uint8_t section_id; // player + uint8_t char_class; // player + le_uint32_t unknown3; // not saved + parray symbol_chats; // account + parray shortcuts; // account + ptext auto_reply; // player + ptext info_board; // player + parray unknown5; // not saved + parray challenge_data; // player + parray tech_menu_config; // player + parray unknown6; // not saved + parray quest_data2; // player + KeyAndTeamConfigBB key_config; // account } __attribute__((packed)); @@ -440,7 +448,7 @@ struct SavedPlayerDataBB { // .nsc file format PlayerBank bank; parray challenge_data; PlayerDispDataBB disp; - ptext guild_card_desc; + ptext guild_card_description; ptext info_board; PlayerInventory inventory; parray quest_data1; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 43d32919..16c88398 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1571,14 +1571,120 @@ void process_player_preview_request_bb(shared_ptr, shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string& data) { - if (command == 0x01E8) { - check_size_v(data.size(), 8); - send_accept_client_checksum_bb(c); - } else if (command == 0x03E8) { - check_size_v(data.size(), 0); - send_guild_card_header_bb(c); - } else { - throw invalid_argument("unimplemented command"); + switch (command) { + case 0x01E8: { // Check guild card file checksum + check_size_v(data.size(), sizeof(C_GuildCardChecksum_01E8)); + // TODO: Presumably this response tells the client whether the file needs + // to be downloaded at all. In the future, we could actually use the + // checksum to skip downloading if the file isn't modified, to save time. + S_GuildCardChecksumResponse_BB_02E8 cmd = {1, 0}; + send_command_t(c, 0x02E8, 0x00000000, cmd); + break; + } + case 0x03E8: // Download guild card file + check_size_v(data.size(), 0); + send_guild_card_header_bb(c); + break; + case 0x04E8: { // Add guild card + auto& new_gc = check_size_t(data); + auto& gcf = c->game_data.account()->guild_cards; + if (gcf.num_guild_cards < sizeof(gcf.entries) / sizeof(gcf.entries[0])) { + gcf.entries[gcf.num_guild_cards].data = new_gc; + gcf.entries[gcf.num_guild_cards].unknown_a1.clear(); + c->log.info("Added guild card %" PRIu32 " at position %hhu", + new_gc.guild_card_number.load(), gcf.num_guild_cards); + gcf.num_guild_cards++; + } + break; + } + case 0x05E8: { // Delete guild card + auto& cmd = check_size_t(data); + auto& gcf = c->game_data.account()->guild_cards; + size_t z; + for (z = 0; z < gcf.num_guild_cards; z++) { + if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { + break; + } + } + if (z < gcf.num_guild_cards) { + c->log.info("Deleted guild card %" PRIu32 " at position %zu", + cmd.guild_card_number.load(), z); + gcf.num_guild_cards--; + for (z = 0; z < gcf.num_guild_cards; z++) { + gcf.entries[z] = gcf.entries[z + 1]; + } + } + break; + } + case 0x06E8: { // Update guild card + auto& new_gc = check_size_t(data); + auto& gcf = c->game_data.account()->guild_cards; + for (size_t z = 0; z < gcf.num_guild_cards; z++) { + if (gcf.entries[z].data.guild_card_number == new_gc.guild_card_number) { + gcf.entries[z].data = new_gc; + c->log.info("Updated guild card %" PRIu32 " at position %zu", + new_gc.guild_card_number.load(), z); + } + } + break; + } + case 0x07E8: { // Add blocked user + // TODO: Where do these go in GuildCardFileBB? + // auto& cmd = check_size_t(data); + throw runtime_error("blocked users are not yet implemented on BB"); + break; + } + case 0x08E8: { // Delete blocked user + // TODO: Where do these go in GuildCardFileBB? + // auto& cmd = check_size_t(data); + throw runtime_error("blocked users are not yet implemented on BB"); + break; + } + case 0x09E8: { // Write comment + // TODO: We need to know the comment storage format in the guild card file + // in order to implement this (see comment in Player.hh). The + // implementation should go very much like this: + // auto& cmd = check_size_t(data); + // auto& gcf = c->game_data.account()->guild_cards; + // for (size_t z = 0; z < gcf.num_guild_cards; z++) { + // if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { + // gcf.entries[z].comment = cmd.comment; + // c->log.info("Updated comment on guild card %" PRIu32 " at position %zu", + // cmd.guild_card_number.load(), z); + // } + // } + throw runtime_error("guild card comments are not yet implemented on BB"); + break; + } + case 0x0AE8: { // Move guild card in list + auto& cmd = check_size_t(data); + auto& gcf = c->game_data.account()->guild_cards; + if (cmd.position >= gcf.num_guild_cards) { + throw invalid_argument("invalid new position"); + } + size_t index; + for (index = 0; index < gcf.num_guild_cards; index++) { + if (gcf.entries[index].data.guild_card_number == cmd.guild_card_number) { + break; + } + } + if (index >= gcf.num_guild_cards) { + throw invalid_argument("player does not have requested guild card"); + } + auto moved_gc = gcf.entries[index]; + for (; index < cmd.position; index++) { + gcf.entries[index] = gcf.entries[index + 1]; + } + for (; index > cmd.position; index--) { + gcf.entries[index] = gcf.entries[index - 1]; + } + gcf.entries[index] = moved_gc; + c->log.info("Moved guild card %" PRIu32 " to position %zu", + cmd.guild_card_number.load(), index); + break; + } + default: + throw invalid_argument("invalid command"); } } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 180bc2cb..4672e055 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -116,13 +116,15 @@ static void process_subcommand_send_guild_card(shared_ptr, if (c->version == GameVersion::PC) { const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_desc = cmd->desc; + c->game_data.player()->guild_card_description = cmd->description; } else if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_desc = cmd->desc; + c->game_data.player()->guild_card_description = cmd->description; } else if (c->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_desc = cmd->desc; + // Nothing to do... the command is blank; the server generates the guild + // card to be sent + } else { + throw runtime_error("unsupported game version"); } send_guild_card(l->clients[flag], c); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index abebfe54..7603bd6c 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -321,11 +321,6 @@ void send_player_preview_bb(shared_ptr c, uint8_t player_index, } } -void send_accept_client_checksum_bb(shared_ptr c) { - S_AcceptClientChecksum_BB_02E8 cmd = {1, 0}; - send_command_t(c, 0x02E8, 0x00000000, cmd); -} - void send_guild_card_header_bb(shared_ptr c) { uint32_t checksum = crc32( &c->game_data.account()->guild_cards, sizeof(GuildCardFileBB)); @@ -684,7 +679,7 @@ void send_card_search_result( template -void send_guild_card_pc_v3(shared_ptr c, shared_ptr source) { +void send_guild_card_pc_v3_t(shared_ptr c, shared_ptr source) { CmdT cmd; cmd.subcommand = 0x06; cmd.size = sizeof(CmdT) / 4; @@ -693,7 +688,7 @@ void send_guild_card_pc_v3(shared_ptr c, shared_ptr source) { cmd.guild_card_number = source->license->serial_number; cmd.name = source->game_data.player()->disp.name; remove_language_marker_inplace(cmd.name); - cmd.desc = source->game_data.player()->guild_card_desc; + cmd.description = source->game_data.player()->guild_card_description; cmd.reserved1 = 1; cmd.reserved2 = 1; cmd.section_id = source->game_data.player()->disp.section_id; @@ -709,7 +704,7 @@ void send_guild_card_bb(shared_ptr c, shared_ptr source) { cmd.guild_card_number = source->license->serial_number; cmd.name = remove_language_marker(source->game_data.player()->disp.name); cmd.team_name = remove_language_marker(source->game_data.account()->team_name); - cmd.desc = source->game_data.player()->guild_card_desc; + cmd.description = source->game_data.player()->guild_card_description; cmd.reserved1 = 1; cmd.reserved2 = 1; cmd.section_id = source->game_data.player()->disp.section_id; @@ -719,10 +714,10 @@ void send_guild_card_bb(shared_ptr c, shared_ptr source) { void send_guild_card(shared_ptr c, shared_ptr source) { if (c->version == GameVersion::PC) { - send_guild_card_pc_v3(c, source); + send_guild_card_pc_v3_t(c, source); } else if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { - send_guild_card_pc_v3(c, source); + send_guild_card_pc_v3_t(c, source); } else if (c->version == GameVersion::BB) { send_guild_card_bb(c, source); } else { @@ -850,8 +845,8 @@ void send_quest_menu_t( e.menu_id = menu_id; e.item_id = quest->menu_item_id; e.name = quest->name; - e.short_desc = quest->short_description; - add_color_inplace(e.short_desc); + e.short_description = quest->short_description; + add_color_inplace(e.short_description); } send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } @@ -868,8 +863,8 @@ void send_quest_menu_t( e.menu_id = menu_id; e.item_id = item.item_id; e.name = item.name; - e.short_desc = item.description; - add_color_inplace(e.short_desc); + e.short_description = item.description; + add_color_inplace(e.short_description); } send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); }