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
|
parray<uint8_t, 0x140> challenge_battle_config; // 08ED
|
||||||
};
|
};
|
||||||
|
|
||||||
// EE (S->C): Unknown (Episode 3)
|
// EE (S->C): Trade cards sequence (Episode 3)
|
||||||
// This command has different forms depending on the header.flag value. Since
|
// This command has different forms depending on the header.flag value; the flag
|
||||||
// the relevant flag values match the Episodes 1 & 2 trade window commands, this
|
// values match the command numbers from the Episodes 1&2 trade window sequence.
|
||||||
// is likely used for trading cards.
|
// 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 {
|
// EE D0 (C->S): Begin trade
|
||||||
parray<le_uint32_t, 9> unknown_a1;
|
struct SC_TradeCards_GC_Ep3_EE_FlagD0_FlagD3 {
|
||||||
};
|
le_uint16_t target_client_id;
|
||||||
|
le_uint16_t entry_count;
|
||||||
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;
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
le_uint32_t unknown_a1;
|
le_uint32_t card_type;
|
||||||
le_uint32_t unknown_a2;
|
le_uint32_t count;
|
||||||
};
|
};
|
||||||
Entry entries[4];
|
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 {
|
// EE D2 (C->S): Trade can proceed
|
||||||
le_uint32_t unknown_a1;
|
// 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)
|
// EE (S->C): Scrolling message (BB)
|
||||||
|
|||||||
+9
-1
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
#include <phosg/Encoding.hh>
|
#include <phosg/Encoding.hh>
|
||||||
|
|
||||||
#include "LevelTable.hh"
|
#include "LevelTable.hh"
|
||||||
@@ -91,6 +92,12 @@ struct PendingItemTrade {
|
|||||||
std::vector<ItemData> items;
|
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;
|
struct PlayerDispDataBB;
|
||||||
@@ -494,8 +501,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)
|
// Null unless the client is within the trade sequence (D0-D4 or EE commands)
|
||||||
std::unique_ptr<PendingItemTrade> pending_item_trade;
|
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
|
// 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;
|
||||||
|
|||||||
+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,
|
static void on_team_command_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||||
uint16_t command, uint32_t, const string&) { // EA
|
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 */
|
/* 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 */
|
/* 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 */
|
/* 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 */
|
/* EF */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* EF */
|
||||||
/* F0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F0 */
|
/* F0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F0 */
|
||||||
/* F1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F1 */
|
/* F1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F1 */
|
||||||
|
|||||||
+23
-19
@@ -39,25 +39,27 @@ const CmdT* check_size_sc(
|
|||||||
max_size = min_size;
|
max_size = min_size;
|
||||||
}
|
}
|
||||||
const auto* cmd = &check_size_t<CmdT>(data, min_size, max_size);
|
const auto* cmd = &check_size_t<CmdT>(data, min_size, max_size);
|
||||||
if (check_size_field && (cmd->size != data.size() / 4)) {
|
if (check_size_field) {
|
||||||
throw runtime_error("invalid subcommand 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;
|
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,
|
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>,
|
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, uint8_t, const string&) {
|
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()) {
|
if ((l->version == GameVersion::BB) && l->is_game()) {
|
||||||
send_bank(c);
|
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,
|
/* B8 */ on_subcommand_identify_item_bb,
|
||||||
/* B9 */ nullptr,
|
/* B9 */ nullptr,
|
||||||
/* BA */ on_subcommand_accept_identify_item_bb,
|
/* BA */ on_subcommand_accept_identify_item_bb,
|
||||||
/* BB */ on_subcommand_open_bank_bb,
|
/* BB */ on_subcommand_open_bank_bb_or_card_trade_counter_ep3,
|
||||||
/* BC */ nullptr, // BB bank contents (server->client only)
|
/* BC */ on_subcommand_forward_check_size_ep3_game, // BB bank contents (server->client only), Ep3 card trade sequence
|
||||||
/* BD */ on_subcommand_bank_action_bb,
|
/* BD */ on_subcommand_bank_action_bb,
|
||||||
/* BE */ nullptr, // BB create inventory item (server->client only)
|
/* 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)
|
/* 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);
|
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,
|
void send_execute_item_trade(std::shared_ptr<Client> c,
|
||||||
const std::vector<ItemData>& items);
|
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_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,
|
||||||
|
|||||||
Reference in New Issue
Block a user