initial spectator + recording implementation

This commit is contained in:
Martin Michelsen
2022-11-30 18:04:51 -08:00
parent b82be91edd
commit f8da4ac7be
22 changed files with 1059 additions and 122 deletions
+378
View File
@@ -0,0 +1,378 @@
#include "BattleRecord.hh"
#include <phosg/Time.hh>
#include "../CommandFormats.hh"
#include "../SendCommands.hh"
using namespace std;
namespace Episode3 {
BattleRecord::Event::Event(StringReader& r) {
this->type = r.get<Event::Type>();
this->timestamp = r.get_u64l();
switch (this->type) {
case Event::Type::PLAYER_JOIN:
this->players.emplace_back(r.get<PlayerEntry>());
break;
case Event::Type::PLAYER_LEAVE:
this->leaving_client_id = r.get_u8();
break;
case Event::Type::SET_INITIAL_PLAYERS: {
uint8_t count = r.get_u8();
while (this->players.size() < count) {
this->players.emplace_back(r.get<PlayerEntry>());
}
break;
}
case Event::Type::CHAT_MESSAGE:
this->guild_card_number = r.get_u32l();
[[fallthrough]];
case Event::Type::GAME_COMMAND:
case Event::Type::BATTLE_COMMAND:
case Event::Type::EP3_GAME_COMMAND:
this->data = r.read(r.get_u16l());
break;
default:
throw logic_error("unknown event type");
}
}
void BattleRecord::Event::serialize(StringWriter& w) const {
w.put(this->type);
w.put_u64l(this->timestamp);
switch (this->type) {
case Event::Type::PLAYER_JOIN:
if (this->players.size() != 1) {
throw logic_error("player join event does not contain 1 player entry");
}
w.put(this->players[0]);
break;
case Event::Type::PLAYER_LEAVE:
w.put_u8(this->leaving_client_id);
break;
case Event::Type::SET_INITIAL_PLAYERS:
w.put_u8(this->players.size());
for (const auto& player : this->players) {
w.put(player);
}
break;
case Event::Type::CHAT_MESSAGE:
w.put_u32l(this->guild_card_number);
[[fallthrough]];
case Event::Type::GAME_COMMAND:
case Event::Type::BATTLE_COMMAND:
case Event::Type::EP3_GAME_COMMAND:
w.put_u16l(this->data.size());
w.write(this->data);
break;
default:
throw logic_error("unknown event type");
}
}
BattleRecord::BattleRecord(uint32_t behavior_flags)
: is_writable(true),
behavior_flags(behavior_flags),
battle_start_timestamp(0),
battle_end_timestamp(0) { }
BattleRecord::BattleRecord(const string& data)
: is_writable(false),
behavior_flags(0),
battle_start_timestamp(0),
battle_end_timestamp(0) {
StringReader r(data);
uint64_t signature = r.get_u64l();
if (signature != this->SIGNATURE) {
throw runtime_error("incorrect battle record signature");
}
this->battle_start_timestamp = r.get_u64l();
this->battle_end_timestamp = r.get_u64l();
this->behavior_flags = r.get_u32l();
while (!r.eof()) {
this->events.emplace_back(r);
}
}
string BattleRecord::serialize() const {
StringWriter w;
w.put_u64l(this->SIGNATURE);
w.put_u64l(this->battle_start_timestamp);
w.put_u64l(this->battle_end_timestamp);
w.put_u32l(this->behavior_flags);
for (const auto& ev : this->events) {
ev.serialize(w);
}
return move(w.str());
}
bool BattleRecord::writable() const {
return this->is_writable;
}
bool BattleRecord::battle_in_progress() const {
return (this->battle_start_timestamp != 0);
}
const BattleRecord::Event* BattleRecord::get_first_event() const {
if (this->events.empty()) {
return nullptr;
}
return &this->events.front();
}
void BattleRecord::add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
if (this->battle_start_timestamp != 0) {
throw runtime_error("cannot add player during battle");
}
Event& ev = this->events.emplace_back();
ev.type = Event::Type::PLAYER_JOIN;
ev.timestamp = now();
auto& player = ev.players.emplace_back();
player.lobby_data = lobby_data;
player.inventory = inventory;
player.disp = disp;
}
void BattleRecord::delete_player(uint8_t client_id) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
Event& ev = this->events.emplace_back();
ev.type = Event::Type::PLAYER_LEAVE;
ev.timestamp = now();
ev.leaving_client_id = client_id;
}
void BattleRecord::add_command(Event::Type type, const void* data, size_t size) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
Event& ev = this->events.emplace_back();
ev.type = type;
ev.timestamp = now();
ev.data.assign(reinterpret_cast<const char*>(data), size);
}
void BattleRecord::add_command(Event::Type type, string&& data) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
Event& ev = this->events.emplace_back();
ev.type = type;
ev.timestamp = now();
ev.data = move(data);
}
void BattleRecord::add_chat_message(
uint32_t guild_card_number, string&& data) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
Event& ev = this->events.emplace_back();
ev.type = Event::Type::CHAT_MESSAGE;
ev.timestamp = now();
ev.guild_card_number = guild_card_number;
ev.data = move(data);
}
bool BattleRecord::is_map_definition_event(const Event& ev) {
if (ev.type == Event::Type::BATTLE_COMMAND) {
auto& header = check_size_t<G_CardBattleCommandHeader>(
ev.data, sizeof(G_CardBattleCommandHeader), 0xFFFF);
if (header.subcommand == 0xB6) {
auto& header = check_size_t<G_MapSubsubcommand_GC_Ep3_6xB6>(
ev.data, sizeof(G_MapSubsubcommand_GC_Ep3_6xB6), 0xFFFF);
if (header.subsubcommand == 0x41) {
return true;
}
}
}
return false;
}
void BattleRecord::set_battle_start_timestamp() {
if (this->battle_start_timestamp != 0) {
throw logic_error("battle start timestamp is already set");
}
this->battle_start_timestamp = now();
// First, find the correct map definition subcommand to keep, and execute
// player join/leave events to get the present players
size_t num_map_events = 0;
PlayerEntry players[4];
bool players_present[4];
for (auto& ev : this->events) {
if (ev.type == Event::Type::PLAYER_JOIN) {
if (ev.players.size() != 1) {
throw logic_error("player join event does not contain 1 player entry");
}
auto& player = ev.players[0];
if (player.lobby_data.client_id >= 4) {
throw runtime_error("invalid client ID");
}
players[player.lobby_data.client_id] = player;
players_present[player.lobby_data.client_id] = true;
} else if (ev.type == Event::Type::PLAYER_LEAVE) {
if (ev.leaving_client_id >= 4) {
throw logic_error("invalid client ID");
}
players_present[ev.leaving_client_id] = false;
} else if (ev.type == Event::Type::SET_INITIAL_PLAYERS) {
throw logic_error("BattleRecord::set_battle_start_timestamp called twice");
} else if (this->is_map_definition_event(ev)) {
num_map_events++;
}
}
deque<Event> new_events;
// Generate the initial players event
Event initial_ev;
initial_ev.type = Event::Type::SET_INITIAL_PLAYERS;
initial_ev.timestamp = this->battle_start_timestamp;
for (size_t z = 0; z < 4; z++) {
if (players_present[z]) {
initial_ev.players.emplace_back(players[z]);
}
}
new_events.emplace_back(move(initial_ev));
// Skip all events before the last map definition event, and only retain
// battle commands between then and now (since these battle commands will all
// be replayed at once)
auto it = this->events.begin();
for (; it != this->events.end(); it++) {
if (this->is_map_definition_event(*it)) {
num_map_events--;
if (num_map_events == 0) {
break;
}
}
}
for (; it != this->events.end(); it++) {
if (it->type == Event::Type::BATTLE_COMMAND) {
new_events.emplace_back(move(*it));
}
}
this->events = move(new_events);
}
void BattleRecord::set_battle_end_timestamp() {
this->battle_end_timestamp = now();
}
BattleRecordPlayer::BattleRecordPlayer(
shared_ptr<const BattleRecord> rec,
shared_ptr<struct event_base> base,
shared_ptr<Lobby> l)
: record(rec),
event_it(this->record->events.begin()),
play_start_timestamp(0),
base(base),
lobby(l),
next_command_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &BattleRecordPlayer::dispatch_schedule_events, this), event_free) { }
shared_ptr<const BattleRecord> BattleRecordPlayer::get_record() const {
return this->record;
}
void BattleRecordPlayer::start() {
this->play_start_timestamp = now();
this->schedule_events();
}
void BattleRecordPlayer::dispatch_schedule_events(
evutil_socket_t, short, void* ctx) {
reinterpret_cast<BattleRecordPlayer*>(ctx)->schedule_events();
}
void BattleRecordPlayer::schedule_events() {
// If the lobby is destroyed, we can't replay anything - just return without
// rescheduling
auto l = this->lobby.lock();
if (!l) {
return;
}
for (;;) {
uint64_t relative_ts = now() - this->play_start_timestamp + this->record->battle_start_timestamp;
if (this->event_it == this->record->events.end()) {
if (relative_ts >= this->record->battle_end_timestamp) {
// If the record is complete and the end timestamp has been reached, so
// send exit commands to all players in the lobby, and don't reschedule
// the event (it will be deleted along with the Player when the lobby is
// destroyed, when the last client leaves)
send_command(l, 0xED, 0x00);
} else {
// There are no more events to play, but the battle has not officially
// ended yet - reschedule the event for the end time
auto tv = usecs_to_timeval(this->record->battle_end_timestamp - relative_ts);
event_add(this->next_command_ev.get(), &tv);
}
break;
} else {
if (this->event_it->timestamp <= relative_ts) {
// Play the next event
auto& ev = *this->event_it;
switch (ev.type) {
case BattleRecord::Event::Type::PLAYER_JOIN:
// Technically we can support this, but it should never happen
throw runtime_error("player join event during battle replay");
case BattleRecord::Event::Type::PLAYER_LEAVE:
send_player_leave_notification(l, ev.leaving_client_id);
break;
case BattleRecord::Event::Type::SET_INITIAL_PLAYERS:
// This should have been handled before the lobby was even created
break;
case BattleRecord::Event::Type::BATTLE_COMMAND:
send_command(l, 0xC9, 0x00, ev.data);
break;
case BattleRecord::Event::Type::GAME_COMMAND:
send_command(l, 0x60, 0x00, ev.data);
break;
case BattleRecord::Event::Type::EP3_GAME_COMMAND:
send_command(l, 0xCB, 0x00, ev.data);
break;
case BattleRecord::Event::Type::CHAT_MESSAGE:
send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data));
break;
}
this->event_it++;
} else {
// The next event should not occur yet, so reschedule for the time when
// it should occur
auto tv = usecs_to_timeval(this->event_it->timestamp - relative_ts);
event_add(this->next_command_ev.get(), &tv);
break;
}
}
}
}
} // namespace Episode3
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include <stdint.h>
#include <event2/event.h>
#include <deque>
#include <memory>
#include <string>
#include <variant>
#include <phosg/Strings.hh>
#include "../Player.hh"
struct Lobby;
namespace Episode3 {
// The comment in Server.hh does not apply to this file (and BattleRecord.cc).
class BattleRecord {
public:
struct PlayerEntry {
PlayerLobbyDataDCGC lobby_data;
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
} __attribute__((packed));
struct Event {
enum class Type : uint8_t {
PLAYER_JOIN = 0,
PLAYER_LEAVE = 1,
SET_INITIAL_PLAYERS = 2,
BATTLE_COMMAND = 3,
GAME_COMMAND = 4,
EP3_GAME_COMMAND = 5,
CHAT_MESSAGE = 6,
};
// Fields used for all events
Type type;
uint64_t timestamp;
// Fields used for PLAYER_JOIN and SET_INITIAL_PLAYERS only
std::vector<PlayerEntry> players;
// Fields used for PLAYER_LEAVE only
uint8_t leaving_client_id;
// Fields used for CHAT_MESSAGE only
uint32_t guild_card_number;
// Fields used for the COMMAND types and CHAT_MESSAGE
std::string data;
Event() = default;
explicit Event(StringReader& r);
void serialize(StringWriter& w) const;
};
explicit BattleRecord(uint32_t behavior_flags);
explicit BattleRecord(const std::string& data);
std::string serialize() const;
bool writable() const;
bool battle_in_progress() const;
const Event* get_first_event() const;
void add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp);
void delete_player(uint8_t client_id);
void add_command(Event::Type type, const void* data, size_t size);
void add_command(Event::Type type, std::string&& data);
void add_chat_message(uint32_t guild_card_number, std::string&& data);
// This function collapses all the existing player join/leave events into a
// single SET_INITIAL_PLAYERS event, and deletes all events before the latest
// BATTLE_COMMAND command that specifies the battle map. This should provide a
// minimal set of commands to set up and start the battle during a replay.
void set_battle_start_timestamp();
void set_battle_end_timestamp();
private:
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC5A;
static bool is_map_definition_event(const Event& ev);
bool is_writable;
uint32_t behavior_flags;
uint64_t battle_start_timestamp;
uint64_t battle_end_timestamp;
std::deque<Event> events;
friend class BattleRecordPlayer;
};
class BattleRecordPlayer {
public:
BattleRecordPlayer(
std::shared_ptr<const BattleRecord> rec,
std::shared_ptr<struct event_base> base,
std::shared_ptr<Lobby> l);
~BattleRecordPlayer() = default;
std::shared_ptr<const BattleRecord> get_record() const;
void start();
private:
static void dispatch_schedule_events(evutil_socket_t, short, void* ctx);
void schedule_events();
std::shared_ptr<const BattleRecord> record;
std::deque<BattleRecord::Event>::const_iterator event_it;
uint64_t play_start_timestamp;
std::shared_ptr<struct event_base> base;
std::weak_ptr<Lobby> lobby;
std::shared_ptr<struct event> next_command_ev;
};
} // namespace Episode3
+2
View File
@@ -30,6 +30,8 @@ enum BehaviorFlag {
DISABLE_TIME_LIMITS = 0x00000008,
ENABLE_STATUS_MESSAGES = 0x00000010,
LOAD_CARD_TEXT = 0x00000020,
ENABLE_RECORDING = 0x00000040,
ENABLE_MASKING = 0x00000080,
};
+99 -17
View File
@@ -1,6 +1,7 @@
#include "Server.hh"
#include <phosg/Time.hh>
#include <phosg/Random.hh>
#include "../SendCommands.hh"
@@ -146,7 +147,64 @@ void Server::send(const void* data, size_t size) const {
if (!l) {
throw runtime_error("lobby is deleted");
}
string masked_data;
if (this->base()->data_index->behavior_flags & BehaviorFlag::ENABLE_MASKING) {
if (size >= 8) {
masked_data.assign(reinterpret_cast<const char*>(data), size);
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
data = masked_data.data();
size = masked_data.size();
}
}
send_command(l, 0xC9, 0x00, data, size);
for (auto watcher_l : l->watcher_lobbies) {
send_command(watcher_l, 0xC9, 0x00, data, size);
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
}
}
string Server::prepare_6xB6x41_map_definition(
shared_ptr<const DataIndex::MapEntry> map) {
const auto& compressed = map->compressed();
StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, map->map.map_number.load(), compressed.size(), 0});
w.write(compressed);
return move(w.str());
}
void Server::send_commands_for_joining_spectator(Channel& c) const {
bool should_send_state = true;
if (this->setup_phase == SetupPhase::REGISTRATION) {
// If registration is still in progress, we only need to send the map data
// (if a map is even chosen yet)
if ((this->registration_phase != RegistrationPhase::REGISTERED) &&
(this->registration_phase != RegistrationPhase::BATTLE_STARTED)) {
should_send_state = false;
}
}
if (this->last_chosen_map) {
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
c.send(0x6C, 0x00, data);
}
if (should_send_state) {
// Note: Some servers send the commented-out commands here. Is there a
// situation where we should send them too?
c.send(0xC9, 0x00, this->prepare_6xB4x07_decks_update());
c.send(0xC9, 0x00, this->prepare_6xB4x1C_names_update());
// 6xB4x3B - unknown
c.send(0xC9, 0x00, this->prepare_6xB4x50_trap_tile_locations());
// 6xB4x52 - unknown
}
}
__attribute__((format(printf, 2, 3)))
@@ -411,7 +469,7 @@ void Server::check_for_destroyed_cards_and_send_6xB4x05_6xB4x02() {
this->send_6xB4x02_for_all_players_if_needed();
}
bool Server::check_presence_entry(uint8_t client_id) {
bool Server::check_presence_entry(uint8_t client_id) const {
return (client_id < 4)
? this->base()->presence_entries[client_id].player_present : false;
}
@@ -870,7 +928,7 @@ void Server::move_phase_after() {
// randomness per pass.
if (this->num_trap_tiles_of_type[trap_type] == 2) {
this->chosen_trap_tile_index_of_type[trap_type] ^= 1;
this->send_6xB4x50();
this->send_6xB4x50_trap_tile_locations();
} else if (this->num_trap_tiles_of_type[trap_type] > 2) {
// Generate a new random index, but forbid it from matching the existing
// index
@@ -879,7 +937,7 @@ void Server::move_phase_after() {
new_index++;
}
this->chosen_trap_tile_index_of_type[trap_type] = new_index;
this->send_6xB4x50();
this->send_6xB4x50_trap_tile_locations();
}
}
@@ -898,12 +956,16 @@ void Server::action_phase_before() {
}
}
void Server::send_6xB4x1C_names_update() {
G_SetPlayerNames_GC_Ep3_6xB4x1C Server::prepare_6xB4x1C_names_update() const {
G_SetPlayerNames_GC_Ep3_6xB4x1C cmd;
for (size_t z = 0; z < 4; z++) {
cmd.entries[z] = this->base()->name_entries[z];
}
this->send(cmd);
return cmd;
}
void Server::send_6xB4x1C_names_update() {
this->send(this->prepare_6xB4x1C_names_update());
}
int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) {
@@ -963,7 +1025,7 @@ int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) {
return 1;
}
void Server::send_all_state_updates() {
G_UpdateDecks_GC_Ep3_6xB4x07 Server::prepare_6xB4x07_decks_update() const {
G_UpdateDecks_GC_Ep3_6xB4x07 cmd07;
for (size_t z = 0; z < 4; z++) {
if (!this->check_presence_entry(z)) {
@@ -975,7 +1037,11 @@ void Server::send_all_state_updates() {
cmd07.entries[z] = *this->base()->deck_entries[z];
}
}
this->send(cmd07);
return cmd07;
}
void Server::send_all_state_updates() {
this->send(this->prepare_6xB4x07_decks_update());
G_UpdateMap_GC_Ep3_6xB4x05 cmd05;
cmd05.state = *this->base()->map_and_rules1;
@@ -1299,7 +1365,7 @@ void Server::setup_and_start_battle() {
this->send_6xB4x1C_names_update();
this->registration_phase = RegistrationPhase::BATTLE_STARTED;
this->update_battle_state_flags_and_send_6xB4x03_if_needed(true);
this->send_6xB4x50();
this->send_6xB4x50_trap_tile_locations();
G_UpdateMap_GC_Ep3_6xB4x05 cmd05;
cmd05.state = *this->base()->map_and_rules1;
@@ -1818,6 +1884,13 @@ void Server::handle_6xB3x1D_start_battle(const string& data) {
}
this->battle_in_progress = false;
} else {
auto l = this->base()->lobby.lock();
if (!l) {
throw runtime_error("lobby is deleted");
}
if (l->battle_record) {
l->battle_record->set_battle_start_timestamp();
}
this->setup_and_start_battle();
this->battle_in_progress = true;
}
@@ -2009,6 +2082,11 @@ void Server::handle_6xB3x40_map_list_request(const string& data) {
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
w.write(list_data);
send_command(l, 0x6C, 0x00, w.str());
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, move(w.str()));
}
}
void Server::handle_6xB3x41_map_request(const string& data) {
@@ -2021,14 +2099,14 @@ void Server::handle_6xB3x41_map_request(const string& data) {
throw runtime_error("lobby is deleted");
}
auto entry = this->base()->data_index->definition_for_map_number(cmd.map_number);
const auto& compressed = entry->compressed();
this->last_chosen_map = this->base()->data_index->definition_for_map_number(cmd.map_number);
auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
send_command(l, 0x6C, 0x00, out_cmd);
StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0});
w.write(compressed);
send_command(l, 0x6C, 0x00, w.str());
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, move(out_cmd));
}
}
void Server::handle_6xB3x48_end_turn(const string& data) {
@@ -2509,7 +2587,7 @@ void Server::send_6xB4x02_for_all_players_if_needed(bool always_send) {
}
}
void Server::send_6xB4x50() const {
G_SetTrapTileLocations_GC_Ep3_6xB4x50 Server::prepare_6xB4x50_trap_tile_locations() const {
G_SetTrapTileLocations_GC_Ep3_6xB4x50 cmd;
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
uint8_t trap_index = this->chosen_trap_tile_index_of_type[trap_type];
@@ -2519,7 +2597,11 @@ void Server::send_6xB4x50() const {
cmd.locations[trap_type].clear(0xFF);
}
}
this->send(cmd);
return cmd;
}
void Server::send_6xB4x50_trap_tile_locations() const {
this->send(this->prepare_6xB4x50_trap_tile_locations());
}
+12 -2
View File
@@ -6,6 +6,7 @@
#include "../Text.hh"
#include "../CommandFormats.hh"
#include "../Channel.hh"
#include "AssistServer.hh"
#include "CardSpecial.hh"
#include "MapState.hh"
@@ -109,6 +110,8 @@ public:
}
void send(const void* data, size_t size) const;
void send_commands_for_joining_spectator(Channel& ch) const;
__attribute__((format(printf, 2, 3)))
void send_debug_message_printf(const char* fmt, ...) const;
__attribute__((format(printf, 2, 3)))
@@ -131,7 +134,7 @@ public:
bool card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const;
bool check_for_battle_end();
void check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
bool check_presence_entry(uint8_t client_id);
bool check_presence_entry(uint8_t client_id) const;
void clear_player_flags_after_dice_phase();
void compute_all_map_occupied_bits();
void compute_team_dice_boost(uint8_t team_id);
@@ -213,7 +216,13 @@ public:
void send_6xB4x39() const;
void send_6xB4x05(); // Recomputes the map occupied bits, so can't be const
void send_6xB4x02_for_all_players_if_needed(bool always_send = false);
void send_6xB4x50() const;
void send_6xB4x50_trap_tile_locations() const;
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
static std::string prepare_6xB6x41_map_definition(
std::shared_ptr<const DataIndex::MapEntry> map);
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
const std::vector<std::shared_ptr<const Card>>& cards);
@@ -222,6 +231,7 @@ private:
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
std::weak_ptr<ServerBase> w_base;
std::shared_ptr<const DataIndex::MapEntry> last_chosen_map;
public:
uint32_t battle_finished;