From 6afc02915222c0dc2da7f9688ed2c7a51d04642b Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 2 Oct 2022 19:00:13 -0700 Subject: [PATCH] implement Ep3 card trade window --- src/CommandFormats.hh | 48 +++++++++-------- src/Player.hh | 10 +++- src/ReceiveCommands.cc | 110 +++++++++++++++++++++++++++++++++++++- src/ReceiveSubcommands.cc | 42 ++++++++------- src/SendCommands.cc | 26 +++++++++ src/SendCommands.hh | 2 + 6 files changed, 195 insertions(+), 43 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index fc380b5e..fd0078c5 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2711,35 +2711,39 @@ union C_UpdateAccountData_BB_ED { parray challenge_battle_config; // 08ED }; -// EE (S->C): Unknown (Episode 3) -// This command has different forms depending on the header.flag value. Since -// the relevant flag values match the Episodes 1 & 2 trade window commands, this -// is likely used for trading cards. +// EE (S->C): Trade cards sequence (Episode 3) +// This command has different forms depending on the header.flag value; the flag +// values match the command numbers from the Episodes 1&2 trade window sequence. +// The general sequence of events with the EE command also matches that of the +// Episodes 1&2 trade - see the description of the D0 command for details. -struct C_Unknown_GC_Ep3_EE_FlagD0 { - parray unknown_a1; -}; - -struct S_Unknown_GC_Ep3_EE_FlagD1 { - le_uint32_t unknown_a1; -}; - -// EE D2 04 00 (C->S) - no arguments - -struct S_Unknown_GC_Ep3_EE_FlagD3 { - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; +// EE D0 (C->S): Begin trade +struct SC_TradeCards_GC_Ep3_EE_FlagD0_FlagD3 { + le_uint16_t target_client_id; + le_uint16_t entry_count; struct Entry { - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; + le_uint32_t card_type; + le_uint32_t count; }; Entry entries[4]; }; -// EE D4 04 00 (C->S) - no arguments +// EE D1 (S->C): Advance trade state +struct S_AdvanceCardTradeState_GC_Ep3_EE_FlagD1 { + le_uint32_t unused; +}; -struct S_Unknown_GC_Ep3_EE_FlagD4 { - le_uint32_t unknown_a1; +// EE D2 (C->S): Trade can proceed +// No arguments + +// EE D3 (S->C): Execute trade +// Same format as EE D0 + +// EE D4 (C->S): Trade failed +// EE D4 (S->C): Trade complete + +struct S_CardTradeComplete_GC_Ep3_EE_FlagD4 { + le_uint32_t success; // 0 = failed, 1 = success, anything else = invalid }; // EE (S->C): Scrolling message (BB) diff --git a/src/Player.hh b/src/Player.hh index 6937fd3a..e51f3a99 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -5,6 +5,7 @@ #include #include +#include #include #include "LevelTable.hh" @@ -91,6 +92,12 @@ struct PendingItemTrade { std::vector items; }; +struct PendingCardTrade { + uint8_t other_client_id; + bool confirmed; // true if client has sent an EE D2 command + std::vector> card_to_count; +}; + struct PlayerDispDataBB; @@ -494,8 +501,9 @@ public: // The following fields are not saved, and are only used in certain situations - // Null unless the client is within the trade sequence (D0-D4 commands) + // Null unless the client is within the trade sequence (D0-D4 or EE commands) std::unique_ptr pending_item_trade; + std::unique_ptr pending_card_trade; // Null unless the client is Episode 3 and has sent its config already std::shared_ptr ep3_config; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3e844180..52269dc4 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2682,6 +2682,114 @@ static void on_trade_error(shared_ptr s, shared_ptr c, +static void on_card_trade(shared_ptr s, shared_ptr c, + uint16_t, uint32_t flag, const string& data) { // EE + if (!(c->flags & Client::Flag::IS_EPISODE_3)) { + throw runtime_error("non-Ep3 client sent card trade command"); + } + auto l = s->find_lobby(c->lobby_id); + if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + throw runtime_error("client sent card trade command outside of Ep3 lobby"); + } + if (!l->is_game()) { + throw runtime_error("client sent card trade command in non-game lobby"); + } + + if (flag == 0xD0) { + auto& cmd = check_size_t(data); + + if (c->game_data.pending_card_trade) { + throw runtime_error("player started a card trade when one is already pending"); + } + if (cmd.entry_count > 4) { + throw runtime_error("invalid entry count in card trade command"); + } + + auto target_c = l->clients.at(cmd.target_client_id); + if (!target_c) { + throw runtime_error("card trade command sent to missing player"); + } + if (!(target_c->flags & Client::Flag::IS_EPISODE_3)) { + throw runtime_error("card trade target is not Episode 3"); + } + + c->game_data.pending_card_trade.reset(new PendingCardTrade()); + c->game_data.pending_card_trade->other_client_id = cmd.target_client_id; + for (size_t x = 0; x < cmd.entry_count; x++) { + c->game_data.pending_card_trade->card_to_count.emplace_back( + make_pair(cmd.entries[x].card_type, cmd.entries[x].count)); + } + + // If the other player has a pending trade as well, assume this is the + // second half of the trade sequence, and send an EE D1 to both clients. If + // the other player does not have a pending trade, assume this is the first + // half of the trade sequence, and send an EE D1 only to the target player + // (to request its EE D0 command). + // See the description of the D0 command in CommandFormats.hh for more + // information on how this sequence is supposed to work. (The EE D0 command + // is analogous to Episodes 1&2's D0 command.) + S_AdvanceCardTradeState_GC_Ep3_EE_FlagD1 resp = {0}; + send_command_t(target_c, 0xEE, 0xD1, resp); + if (target_c->game_data.pending_card_trade) { + send_command_t(c, 0xEE, 0xD1, resp); + } + + } else if (flag == 0xD2) { + check_size_v(data.size(), 0); + + if (!c->game_data.pending_card_trade) { + throw runtime_error("player executed a card trade with none pending"); + } + + auto target_c = l->clients.at(c->game_data.pending_card_trade->other_client_id); + if (!target_c) { + throw runtime_error("card trade target player is missing"); + } + if (!target_c->game_data.pending_card_trade) { + throw runtime_error("player executed a card trade with no other side pending"); + } + + c->game_data.pending_card_trade->confirmed = true; + if (target_c->game_data.pending_card_trade->confirmed) { + send_execute_card_trade(c, target_c->game_data.pending_card_trade->card_to_count); + send_execute_card_trade(target_c, c->game_data.pending_card_trade->card_to_count); + S_CardTradeComplete_GC_Ep3_EE_FlagD4 resp = {1}; + send_command_t(c, 0xEE, 0xD4, resp); + send_command_t(target_c, 0xEE, 0xD4, resp); + c->game_data.pending_card_trade.reset(); + target_c->game_data.pending_card_trade.reset(); + } + + } else if (flag == 0xD4) { + check_size_v(data.size(), 0); + + // See the D4 handler for why this check exists (and why it doesn't throw) + if (!c->game_data.pending_card_trade) { + return; + } + uint8_t other_client_id = c->game_data.pending_card_trade->other_client_id; + c->game_data.pending_card_trade.reset(); + S_CardTradeComplete_GC_Ep3_EE_FlagD4 resp = {0}; + send_command_t(c, 0xEE, 0xD4, resp); + + // Cancel the other side of the trade too, if it's open + auto target_c = l->clients.at(other_client_id); + if (!target_c) { + return; + } + if (!target_c->game_data.pending_card_trade) { + return; + } + target_c->game_data.pending_card_trade.reset(); + send_command_t(target_c, 0xEE, 0xD4, resp); + + } else { + throw runtime_error("invalid card trade operation"); + } +} + + + static void on_team_command_bb(shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string&) { // EA @@ -3111,7 +3219,7 @@ static on_command_t handlers[0x100][6] = { /* EB */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_stream_file_request_bb, }, /* EB */ /* EC */ {nullptr, nullptr, nullptr, on_create_game_dc_v3, nullptr, on_leave_char_select_bb, }, /* EC */ /* ED */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_change_account_data_bb, }, /* ED */ - /* EE */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* EE */ + /* EE */ {nullptr, nullptr, nullptr, on_card_trade, nullptr, nullptr, }, /* EE */ /* EF */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* EF */ /* F0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F0 */ /* F1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F1 */ diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index ebd4517b..50a4cb0e 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -39,25 +39,27 @@ const CmdT* check_size_sc( max_size = min_size; } const auto* cmd = &check_size_t(data, min_size, max_size); - if (check_size_field && (cmd->size != data.size() / 4)) { - throw runtime_error("invalid subcommand size field"); + if (check_size_field) { + const PSOSubcommand* sub = reinterpret_cast(data.data()); + if (sub[0].byte[1] == 0) { + if (data.size() < 8) { + throw runtime_error("subcommand has extended size but is shorter than 8 bytes"); + } + if (sub[1].dword != data.size()) { + throw runtime_error("invalid subcommand extended size field"); + } + } else { + if (data.size() < 4) { + throw runtime_error("subcommand is shorter than 4 bytes"); + } + if ((sub[0].byte[1] * 4) != data.size()) { + throw runtime_error("invalid subcommand size field"); + } + } } return cmd; } -template <> -const PSOSubcommand* check_size_sc( - const string& data, size_t min_size, size_t max_size, bool check_size_field) { - if (max_size < min_size) { - max_size = min_size; - } - const auto* ret = &check_size_t(data, min_size, max_size); - if (check_size_field && (ret[0].byte[1] != data.size() / 4)) { - throw runtime_error("invalid subcommand size field"); - } - return ret; -} - static void forward_subcommand(shared_ptr l, shared_ptr c, @@ -596,10 +598,12 @@ static void on_subcommand_open_shop_bb_or_unknown_ep3(shared_ptr, } } -static void on_subcommand_open_bank_bb(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string&) { +static void on_subcommand_open_bank_bb_or_card_trade_counter_ep3(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if ((l->version == GameVersion::BB) && l->is_game()) { send_bank(c); + } else if (l->version == GameVersion::GC && l->flags & Lobby::Flag::EPISODE_3_ONLY) { + forward_subcommand(l, c, command, flag, data); } } @@ -1355,8 +1359,8 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* B8 */ on_subcommand_identify_item_bb, /* B9 */ nullptr, /* BA */ on_subcommand_accept_identify_item_bb, - /* BB */ on_subcommand_open_bank_bb, - /* BC */ nullptr, // BB bank contents (server->client only) + /* BB */ on_subcommand_open_bank_bb_or_card_trade_counter_ep3, + /* BC */ on_subcommand_forward_check_size_ep3_game, // BB bank contents (server->client only), Ep3 card trade sequence /* BD */ on_subcommand_bank_action_bb, /* BE */ nullptr, // BB create inventory item (server->client only) /* BF */ on_subcommand_forward_check_size_ep3_lobby, // Ep3 change music, also BB give EXP (BB usage is server->client only) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 95d32236..b81b54b9 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1315,6 +1315,32 @@ void send_execute_item_trade(std::shared_ptr c, send_command_t(c, 0xD3, 0x00, cmd); } +void send_execute_card_trade(std::shared_ptr c, + const std::vector>& card_to_count) { + if (!(c->flags & Client::Flag::IS_EPISODE_3)) { + throw logic_error("cannot send trade cards command to non-Ep3 client"); + } + + SC_TradeCards_GC_Ep3_EE_FlagD0_FlagD3 cmd; + constexpr size_t max_entries = sizeof(cmd.entries) / sizeof(cmd.entries[0]); + if (card_to_count.size() > max_entries) { + throw logic_error("too many items in execute card trade command"); + } + + cmd.target_client_id = c->lobby_client_id; + cmd.entry_count = card_to_count.size(); + size_t x; + for (x = 0; x < card_to_count.size(); x++) { + cmd.entries[x].card_type = card_to_count[x].first; + cmd.entries[x].count = card_to_count[x].second; + } + for (; x < max_entries; x++) { + cmd.entries[x].card_type = 0; + cmd.entries[x].count = 0; + } + send_command_t(c, 0xEE, 0xD3, cmd); +} + //////////////////////////////////////////////////////////////////////////////// diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 0f67eb4e..1e60c736 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -210,6 +210,8 @@ void send_get_player_info(std::shared_ptr c); void send_execute_item_trade(std::shared_ptr c, const std::vector& items); +void send_execute_card_trade(std::shared_ptr c, + const std::vector>& card_to_count); void send_arrow_update(std::shared_ptr l); void send_resume_game(std::shared_ptr l,