implement trade window

This commit is contained in:
Martin Michelsen
2022-07-25 00:40:05 -07:00
parent 52a853092c
commit df80933f40
6 changed files with 180 additions and 16 deletions
+40 -11
View File
@@ -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.
+11
View File
@@ -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<ItemData> 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<PendingItemTrade> pending_item_trade;
// Null unless the client is Episode 3 and has sent its config already
std::shared_ptr<Ep3Config> ep3_config;
+107 -4
View File
@@ -2019,6 +2019,109 @@ void process_client_ready(shared_ptr<ServerState> s, shared_ptr<Client> c,
}
}
////////////////////////////////////////////////////////////////////////////////
// Trade window commands
void process_trade_start(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D0
auto& cmd = check_size_t<SC_TradeItems_D0_D3>(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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<char>, 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<char16_t>, nullptr, nullptr,
process_guild_card_data_request_bb, nullptr, nullptr, nullptr,
+19
View File
@@ -1092,6 +1092,25 @@ void send_get_player_info(shared_ptr<Client> c) {
////////////////////////////////////////////////////////////////////////////////
// Trade window
void send_execute_item_trade(std::shared_ptr<Client> c,
const std::vector<ItemData>& 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
+3
View File
@@ -183,6 +183,9 @@ void send_player_leave_notification(std::shared_ptr<Lobby> l,
void send_self_leave_notification(std::shared_ptr<Client> c);
void send_get_player_info(std::shared_ptr<Client> c);
void send_execute_item_trade(std::shared_ptr<Client> c,
const std::vector<ItemData>& items);
void send_arrow_update(std::shared_ptr<Lobby> l);
void send_resume_game(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> ready_client);