implement trade window
This commit is contained in:
+40
-11
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user