diff --git a/README.md b/README.md index d3869007..e1c6943d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ 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 (info board, simple mail, card search, etc.) -- The trade window isn't implemented. - Episode 3 battles aren't implemented. - PSO PC and PSOBB are not well-tested and likely will disconnect when clients try to use unimplemented features. Only GC is known to be stable and mostly complete. - 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/CommandFormats.hh b/src/CommandFormats.hh index 158ccf85..a7661fdd 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1700,24 +1700,53 @@ struct S_Unknown_GC_Ep3_CC { // CE: Invalid command // CF: Invalid command -// D0 (C->S): Execute trade via trade window (GC/BB) -// General sequence: client sends D0, server sends D1 to that client, server -// sends D3 to other client, server sends D4 to both (?) clients. -// Format unknown. On PSO GC it appears to be always 0x288 bytes in size; on BB -// it is 0x28C bytes in size, implying that the format is the same between the -// two versions (since BB headers are 4 bytes longer). +// D0 (C->S): Start trade sequence (GC/BB) +// The trade window sequence is a bit complicated. The normal flow is: +// - Clients sync trade state with 60xA6 commands (technically 62xA6) +// - When both have confirmed, one client (the initiator) sends a D0 +// - Server sends a D1 to the non-initiator +// - Non-initiator sends a D0 +// - Server sends a D1 to both clients +// - Both clients delete the sent items from their inventories (and send the +// appropriate subcommand) +// - Both clients send a D2 (similarly to how AC works, the server should not +// proceed until both D2s are received) +// - Server sends a D3 to both clients with each other's data from their D0s, +// followed immediately by a D4 01 to both clients, which completes the trade +// - Both clients send the appropriate subcommand to create inventory items +// TODO: On BB, is the server responsible for sending the appropriate item +// subcommands? +// At any point if an error occurs, either client may send a D4 00, which +// cancels the entire sequence. The server should then send D4 00 to both +// clients. -// D1 (S->C): Confirm trade to initiator (GC/BB) +struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server + le_uint16_t target_client_id; + le_uint16_t item_count; + // Note: PSO GC sends uninitialized data in the unused entries of this + // command. newserv parses and regenerates the item data when sending D3, + // which effectively erases the uninitialized data. + ItemData items[0x20]; +}; + +// D1 (S->C): Advance trade state (GC/BB) // No arguments +// See D0 description for usage information. -// D2 (C->S): Unknown (used in trade sequence) +// D2 (C->S): Trade can proceed (GC/BB) // No arguments +// See D0 description for usage information. -// D3 (S->C): Execute trade with accepter (GC/BB) -// Format unknown; appears to be same as D0. +// D3 (S->C): Execute trade (GC/BB) +// Same format as D0. See D0 description for usage information. -// D4 (S->C): Close trade (GC/BB) +// D4 (C->S): Trade failed (GC/BB) // No arguments +// See D0 description for usage information. + +// D4 (S->C): Trade complete (GC/BB) +// header.flag must be 0 (trade failed) or 1 (trade complete). +// See D0 description for usage information. // D5: Large message box (GC/BB) // Same as 1A command, except the maximum length of the message is 0x1000 bytes. diff --git a/src/Player.hh b/src/Player.hh index e3888fb3..a9af7a06 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -83,6 +83,14 @@ struct PlayerBank { // 0xFA8 bytes +struct PendingItemTrade { + uint8_t other_client_id; + bool confirmed; // true if client has sent a D2 command + std::vector items; +}; + + + struct PlayerDispDataBB; // PC/GC player appearance and stats data @@ -437,6 +445,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) + std::unique_ptr pending_item_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 58f01f38..a1564708 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2019,6 +2019,109 @@ void process_client_ready(shared_ptr s, shared_ptr c, } } +//////////////////////////////////////////////////////////////////////////////// +// Trade window commands + +void process_trade_start(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // D0 + auto& cmd = check_size_t(data); + + if (c->game_data.pending_item_trade) { + throw runtime_error("player started a trade when one is already pending"); + } + if (cmd.item_count > 0x20) { + throw runtime_error("invalid item count in trade items command"); + } + + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + throw runtime_error("trade command received in non-game lobby"); + } + auto target_c = l->clients.at(cmd.target_client_id); + if (!target_c) { + throw runtime_error("trade command sent to missing player"); + } + + c->game_data.pending_item_trade.reset(new PendingItemTrade()); + c->game_data.pending_item_trade->other_client_id = cmd.target_client_id; + for (size_t x = 0; x < cmd.item_count; x++) { + c->game_data.pending_item_trade->items.emplace_back(cmd.items[x]); + } + + // If the other player has a pending trade as well, assume this is the second + // half of the trade sequence, and send a D1 to both clients (which should + // cause them to delete the appropriate inventory items and send D2s). If the + // other player does not have a pending trade, assume this is the first half + // of the trade sequence, and send a D1 only to the target player (to request + // its D0 command). + send_command(target_c, 0xD1, 0x00); + if (target_c->game_data.pending_item_trade) { + send_command(c, 0xD1, 0x00); + } +} + +void process_trade_execute(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // D2 + check_size_v(data.size(), 0); + + if (!c->game_data.pending_item_trade) { + throw runtime_error("player executed a trade with none pending"); + } + + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + throw runtime_error("trade command received in non-game lobby"); + } + auto target_c = l->clients.at(c->game_data.pending_item_trade->other_client_id); + if (!target_c) { + throw runtime_error("target player is missing"); + } + if (!target_c->game_data.pending_item_trade) { + throw runtime_error("player executed a trade with no other side pending"); + } + + c->game_data.pending_item_trade->confirmed = true; + if (target_c->game_data.pending_item_trade->confirmed) { + send_execute_item_trade(c, target_c->game_data.pending_item_trade->items); + send_execute_item_trade(target_c, c->game_data.pending_item_trade->items); + send_command(c, 0xD4, 0x01); + send_command(target_c, 0xD4, 0x01); + c->game_data.pending_item_trade.reset(); + target_c->game_data.pending_item_trade.reset(); + } +} + +void process_trade_error(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // D4 + check_size_v(data.size(), 0); + + // Annoyingly, if the other client disconnects at a certain point during the + // trade sequence, the client can get into a state where it sends this command + // many times in a row. To deal with this, we just do nothing if the client + // has no trade pending. + if (!c->game_data.pending_item_trade) { + return; + } + uint8_t other_client_id = c->game_data.pending_item_trade->other_client_id; + c->game_data.pending_item_trade.reset(); + send_command(c, 0xD4, 0x00); + + // Cancel the other side of the trade too, if it's open + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + throw runtime_error("trade command received in non-game lobby"); + } + auto target_c = l->clients.at(other_client_id); + if (!target_c) { + return; + } + if (!target_c->game_data.pending_item_trade) { + return; + } + target_c->game_data.pending_item_trade.reset(); + send_command(target_c, 0xD4, 0x00); +} + //////////////////////////////////////////////////////////////////////////////// // Team commands @@ -2342,8 +2445,8 @@ static process_command_t gc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, // D0 - nullptr, nullptr, nullptr, nullptr, // D0 is process trade - nullptr, nullptr, process_message_box_closed, process_gba_file_request, + process_trade_start, nullptr, process_trade_execute, nullptr, + process_trade_error, nullptr, process_message_box_closed, process_gba_file_request, process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_gc, process_ep3_menu_challenge, nullptr, nullptr, nullptr, @@ -2431,8 +2534,8 @@ static process_command_t bb_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, // D0 - nullptr, nullptr, nullptr, nullptr, - nullptr, nullptr, nullptr, nullptr, + process_trade_start, nullptr, process_trade_execute, nullptr, + process_trade_error, nullptr, nullptr, nullptr, process_info_board_request, process_write_info_board_t, nullptr, nullptr, process_guild_card_data_request_bb, nullptr, nullptr, nullptr, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 9e1afe60..462db23d 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1092,6 +1092,25 @@ void send_get_player_info(shared_ptr c) { +//////////////////////////////////////////////////////////////////////////////// +// Trade window + +void send_execute_item_trade(std::shared_ptr c, + const std::vector& items) { + SC_TradeItems_D0_D3 cmd; + if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) { + throw logic_error("too many items in execute trade command"); + } + cmd.target_client_id = c->lobby_client_id; + cmd.item_count = items.size(); + for (size_t x = 0; x < items.size(); x++) { + cmd.items[x] = items[x]; + } + send_command_t(c, 0xD3, 0x00, cmd); +} + + + //////////////////////////////////////////////////////////////////////////////// // arrows diff --git a/src/SendCommands.hh b/src/SendCommands.hh index e8acb555..8d2c311b 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -183,6 +183,9 @@ void send_player_leave_notification(std::shared_ptr l, void send_self_leave_notification(std::shared_ptr c); void send_get_player_info(std::shared_ptr c); +void send_execute_item_trade(std::shared_ptr c, + const std::vector& items); + void send_arrow_update(std::shared_ptr l); void send_resume_game(std::shared_ptr l, std::shared_ptr ready_client);