implement trade window
This commit is contained in:
@@ -14,7 +14,6 @@ This project is primarily for my own nostalgia; I offer no guarantees on how or
|
|||||||
|
|
||||||
Current known issues / missing features:
|
Current known issues / missing features:
|
||||||
- Test all the communication features in cross-version scenarios (info board, simple mail, card search, etc.)
|
- 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.
|
- 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.
|
- 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.
|
- Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly.
|
||||||
|
|||||||
+40
-11
@@ -1700,24 +1700,53 @@ struct S_Unknown_GC_Ep3_CC {
|
|||||||
// CE: Invalid command
|
// CE: Invalid command
|
||||||
// CF: Invalid command
|
// CF: Invalid command
|
||||||
|
|
||||||
// D0 (C->S): Execute trade via trade window (GC/BB)
|
// D0 (C->S): Start trade sequence (GC/BB)
|
||||||
// General sequence: client sends D0, server sends D1 to that client, server
|
// The trade window sequence is a bit complicated. The normal flow is:
|
||||||
// sends D3 to other client, server sends D4 to both (?) clients.
|
// - Clients sync trade state with 60xA6 commands (technically 62xA6)
|
||||||
// Format unknown. On PSO GC it appears to be always 0x288 bytes in size; on BB
|
// - When both have confirmed, one client (the initiator) sends a D0
|
||||||
// it is 0x28C bytes in size, implying that the format is the same between the
|
// - Server sends a D1 to the non-initiator
|
||||||
// two versions (since BB headers are 4 bytes longer).
|
// - 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
|
// 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
|
// No arguments
|
||||||
|
// See D0 description for usage information.
|
||||||
|
|
||||||
// D3 (S->C): Execute trade with accepter (GC/BB)
|
// D3 (S->C): Execute trade (GC/BB)
|
||||||
// Format unknown; appears to be same as D0.
|
// 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
|
// 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)
|
// D5: Large message box (GC/BB)
|
||||||
// Same as 1A command, except the maximum length of the message is 0x1000 bytes.
|
// 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;
|
struct PlayerDispDataBB;
|
||||||
|
|
||||||
// PC/GC player appearance and stats data
|
// 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
|
// 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
|
// Null unless the client is Episode 3 and has sent its config already
|
||||||
std::shared_ptr<Ep3Config> ep3_config;
|
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
|
// Team commands
|
||||||
|
|
||||||
@@ -2342,8 +2445,8 @@ static process_command_t gc_handlers[0x100] = {
|
|||||||
nullptr, nullptr, nullptr, nullptr,
|
nullptr, nullptr, nullptr, nullptr,
|
||||||
|
|
||||||
// D0
|
// D0
|
||||||
nullptr, nullptr, nullptr, nullptr, // D0 is process trade
|
process_trade_start, nullptr, process_trade_execute, nullptr,
|
||||||
nullptr, nullptr, process_message_box_closed, process_gba_file_request,
|
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_info_board_request, process_write_info_board_t<char>, nullptr, process_verify_license_gc,
|
||||||
process_ep3_menu_challenge, nullptr, nullptr, nullptr,
|
process_ep3_menu_challenge, nullptr, nullptr, nullptr,
|
||||||
|
|
||||||
@@ -2431,8 +2534,8 @@ static process_command_t bb_handlers[0x100] = {
|
|||||||
nullptr, nullptr, nullptr, nullptr,
|
nullptr, nullptr, nullptr, nullptr,
|
||||||
|
|
||||||
// D0
|
// D0
|
||||||
nullptr, nullptr, nullptr, nullptr,
|
process_trade_start, nullptr, process_trade_execute, nullptr,
|
||||||
nullptr, nullptr, nullptr, nullptr,
|
process_trade_error, nullptr, nullptr, nullptr,
|
||||||
process_info_board_request, process_write_info_board_t<char16_t>, nullptr, nullptr,
|
process_info_board_request, process_write_info_board_t<char16_t>, nullptr, nullptr,
|
||||||
process_guild_card_data_request_bb, nullptr, 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
|
// 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_self_leave_notification(std::shared_ptr<Client> c);
|
||||||
void send_get_player_info(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_arrow_update(std::shared_ptr<Lobby> l);
|
||||||
void send_resume_game(std::shared_ptr<Lobby> l,
|
void send_resume_game(std::shared_ptr<Lobby> l,
|
||||||
std::shared_ptr<Client> ready_client);
|
std::shared_ptr<Client> ready_client);
|
||||||
|
|||||||
Reference in New Issue
Block a user