implement Ep3 card trade window
This commit is contained in:
+26
-22
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user