implement Ep3 card trade window

This commit is contained in:
Martin Michelsen
2022-10-02 19:00:13 -07:00
parent 44e28fd906
commit 6afc029152
6 changed files with 195 additions and 43 deletions
+26 -22
View File
@@ -2711,35 +2711,39 @@ union C_UpdateAccountData_BB_ED {
parray<uint8_t, 0x140> 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<le_uint32_t, 9> 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)
+9 -1
View File
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include <utility>
#include <phosg/Encoding.hh>
#include "LevelTable.hh"
@@ -91,6 +92,12 @@ struct PendingItemTrade {
std::vector<ItemData> items;
};
struct PendingCardTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent an EE D2 command
std::vector<std::pair<uint32_t, uint32_t>> 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<PendingItemTrade> pending_item_trade;
std::unique_ptr<PendingCardTrade> pending_card_trade;
// Null unless the client is Episode 3 and has sent its config already
std::shared_ptr<Ep3Config> ep3_config;
+109 -1
View File
@@ -2682,6 +2682,114 @@ static void on_trade_error(shared_ptr<ServerState> s, shared_ptr<Client> c,
static void on_card_trade(shared_ptr<ServerState> s, shared_ptr<Client> 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<SC_TradeCards_GC_Ep3_EE_FlagD0_FlagD3>(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<ServerState>, shared_ptr<Client> 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 */
+23 -19
View File
@@ -39,25 +39,27 @@ const CmdT* check_size_sc(
max_size = min_size;
}
const auto* cmd = &check_size_t<CmdT>(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<const PSOSubcommand*>(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<PSOSubcommand>(
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<PSOSubcommand>(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<Lobby> l, shared_ptr<Client> c,
@@ -596,10 +598,12 @@ static void on_subcommand_open_shop_bb_or_unknown_ep3(shared_ptr<ServerState>,
}
}
static void on_subcommand_open_bank_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t, uint8_t, const string&) {
static void on_subcommand_open_bank_bb_or_card_trade_counter_ep3(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> 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)
+26
View File
@@ -1315,6 +1315,32 @@ void send_execute_item_trade(std::shared_ptr<Client> c,
send_command_t(c, 0xD3, 0x00, cmd);
}
void send_execute_card_trade(std::shared_ptr<Client> c,
const std::vector<std::pair<uint32_t, uint32_t>>& 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);
}
////////////////////////////////////////////////////////////////////////////////
+2
View File
@@ -210,6 +210,8 @@ 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_execute_card_trade(std::shared_ptr<Client> c,
const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count);
void send_arrow_update(std::shared_ptr<Lobby> l);
void send_resume_game(std::shared_ptr<Lobby> l,