diff --git a/src/Client.cc b/src/Client.cc index a19f0d9f..d7423d25 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -53,6 +53,8 @@ Client::Client( bufferevent_get_base(bev), -1, EV_TIMEOUT | EV_PERSIST, &Client::dispatch_save_game_data, this), event_free), + card_battle_table_number(-1), + card_battle_table_seat_number(0), next_exp_value(0), override_section_id(-1), override_random_seed(-1), diff --git a/src/Client.hh b/src/Client.hh index dd1efa54..f9f8a5e8 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -105,6 +105,8 @@ struct Client { int64_t preferred_lobby_id; // <0 = no preference ClientGameData game_data; std::unique_ptr save_game_data_event; + int16_t card_battle_table_number; + uint8_t card_battle_table_seat_number; // Miscellaneous (used by chat commands) uint32_t next_exp_value; // next EXP value to give diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7cd8eba1..0a940b58 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2430,10 +2430,12 @@ struct S_PlayerPreview_NoPlayer_BB_00E4 { le_uint32_t error; // 2 = no player present }; -// E5 (C->S): Unknown (CARD lobby battle table) (Episode 3) +// E5 (C->S): Confirm CARD lobby battle table choice (Episode 3) +// header.flag specifies whether the client answered "Yes" (1) or "No" (0). -struct S_CardBattleTable_Unknown_GC_Ep3_E5 { - le_uint32_t unknown_a1; +struct S_CardBattleTableConfirmation_GC_Ep3_E5 { + le_uint16_t table_number; + le_uint16_t seat_number; }; // E5 (S->C): Player preview (BB) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3f243aeb..3715e18f 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -770,6 +770,41 @@ static void on_ep3_meseta_transaction(shared_ptr, send_command(c, command, 0x03, &out_cmd, sizeof(out_cmd)); } +static void on_ep3_battle_table_state(shared_ptr s, + shared_ptr c, uint16_t, uint32_t flag, const string& data) { // E4 + const auto& cmd = check_size_t(data); + auto l = s->find_lobby(c->lobby_id); + if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + throw runtime_error("battle table command sent in non-CARD lobby"); + } + + if (flag) { + c->card_battle_table_number = cmd.table_number; + c->card_battle_table_seat_number = cmd.seat_number; + } else { + c->card_battle_table_number = -1; + c->card_battle_table_seat_number = 0; + } + send_ep3_card_battle_table_state(l, c->card_battle_table_number); + // TODO: If a client disconnects while at the battle table, we need to send + // a table update to all the other clients at the table (if any). Use a + // disconnect hook for this. +} + +static void on_ep3_battle_table_confirm(shared_ptr s, + shared_ptr c, uint16_t, uint32_t flag, const string& data) { // E4 + check_size_t(data); + auto l = s->find_lobby(c->lobby_id); + if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + throw runtime_error("battle table command sent in non-CARD lobby"); + } + + if (flag) { + // TODO + send_lobby_message_box(c, u"CARD battles are\nnot yet supported."); + } +} + static void on_ep3_counter_state(shared_ptr, shared_ptr c, uint16_t, uint32_t flag, const string& data) { // DC check_size_v(data.size(), 0); @@ -3226,8 +3261,8 @@ static on_command_t handlers[0x100][6] = { /* E1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* E1 */ /* E2 */ {nullptr, nullptr, nullptr, on_ep3_tournament_control, nullptr, on_update_key_config_bb, }, /* E2 */ /* E3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_player_preview_request_bb, }, /* E3 */ - /* E4 */ {nullptr, nullptr, nullptr, on_ignored_command, nullptr, nullptr, }, /* E4 */ - /* E5 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_create_character_bb, }, /* E5 */ + /* E4 */ {nullptr, nullptr, nullptr, on_ep3_battle_table_state, nullptr, nullptr, }, /* E4 */ + /* E5 */ {nullptr, nullptr, nullptr, on_ep3_battle_table_confirm, nullptr, on_create_character_bb, }, /* E5 */ /* E6 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* E6 */ /* E7 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_return_player_data_bb, }, /* E7 */ /* E8 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_client_checksum_bb, }, /* E8 */ diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 8456f597..a2fd1eea 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1649,6 +1649,38 @@ void send_ep3_map_data(shared_ptr s, shared_ptr l, uint32_t send_command(l, 0x6C, 0x00, w.str()); } +void send_ep3_card_battle_table_state(shared_ptr l, uint16_t table_number) { + S_CardBattleTableState_GC_Ep3_E4 cmd; + for (size_t z = 0; z < 4; z++) { + cmd.entries[z].present = 0; + cmd.entries[z].unknown_a1 = 0; + cmd.entries[z].guild_card_number = 0; + } + + set> clients; + for (const auto& c : l->clients) { + if (!c) { + continue; + } + if (c->card_battle_table_number == table_number) { + if (c->card_battle_table_seat_number > 3) { + throw runtime_error("invalid battle table seat number"); + } + auto& e = cmd.entries[c->card_battle_table_seat_number]; + if (e.present) { + throw runtime_error("multiple clients in the same battle table seat"); + } + e.present = 1; + e.guild_card_number = c->license->serial_number; + clients.emplace(c); + } + } + + for (const auto& c : clients) { + send_command_t(c, 0xE4, table_number, cmd); + } +} + void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) { if (size < 8) { throw logic_error("Episode 3 game command is too short for masking"); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 0afcc79a..56eca602 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -264,6 +264,7 @@ void send_ep3_map_list( std::shared_ptr s, std::shared_ptr l); void send_ep3_map_data( std::shared_ptr s, std::shared_ptr l, uint32_t map_id); +void send_ep3_card_battle_table_state(std::shared_ptr l, uint16_t table_number); // Pass mask_key = 0 to unmask the command void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);