implement quest version separation

This commit is contained in:
Martin Michelsen
2023-10-15 23:15:30 -07:00
parent 7005b573f5
commit 5d71b66f84
671 changed files with 928 additions and 619 deletions
+250 -63
View File
@@ -1536,6 +1536,91 @@ void StateFlags::clear_FF() {
this->client_sc_card_types.clear(CardType::INVALID_FF);
}
void MapDefinition::assert_semantically_equivalent(const MapDefinition& other) const {
if (this->map_number != other.map_number) {
throw runtime_error("map number not equal");
}
if (this->width != other.width) {
throw runtime_error("width not equal");
}
if (this->height != other.height) {
throw runtime_error("width not equal");
}
if (this->environment_number != other.environment_number) {
throw runtime_error("environment number not equal");
}
if (this->map_tiles != other.map_tiles) {
throw runtime_error("tiles not equal");
}
if (this->start_tile_definitions != other.start_tile_definitions) {
throw runtime_error("start tile definitions not equal");
}
if (this->modification_tiles != other.modification_tiles) {
throw runtime_error("modification tiles not equal");
}
if (this->unknown_a5 != other.unknown_a5) {
throw runtime_error("unknown_a5 not equal");
}
if (this->default_rules != other.default_rules) {
throw runtime_error("default rules not equal");
}
for (size_t z = 0; z < this->npc_decks.size(); z++) {
if (this->npc_decks[z].card_ids != other.npc_decks[z].card_ids) {
throw runtime_error("npc deck card IDs not equal");
}
const auto& this_ai_params = this->npc_ai_params[z];
const auto& other_ai_params = other.npc_ai_params[z];
if (this_ai_params.unknown_a1 != other_ai_params.unknown_a1) {
throw runtime_error("npc AI params unknown_a1 not equal");
}
if (this_ai_params.is_arkz != other_ai_params.is_arkz) {
throw runtime_error("npc AI params is_arkz not equal");
}
if (this_ai_params.unknown_a2 != other_ai_params.unknown_a2) {
throw runtime_error("npc AI params unknown_a2 not equal");
}
if (this_ai_params.params != other_ai_params.params) {
throw runtime_error("npc AI params not equal");
}
}
if (this->unknown_a7 != other.unknown_a7) {
throw runtime_error("unknown_a7 not equal");
}
if (this->npc_ai_params_entry_index != other.npc_ai_params_entry_index) {
throw runtime_error("npc AI params entry indexes not equal");
}
if (this->reward_card_ids != other.reward_card_ids) {
throw runtime_error("reward card IDs not equal");
}
if (this->win_level_override != other.win_level_override) {
throw runtime_error("win level override not equal");
}
if (this->loss_level_override != other.loss_level_override) {
throw runtime_error("loss level override not equal");
}
if (this->field_offset_x != other.field_offset_x) {
throw runtime_error("field x offset not equal");
}
if (this->field_offset_y != other.field_offset_y) {
throw runtime_error("field y offset not equal");
}
if (this->map_category != other.map_category) {
throw runtime_error("map category not equal");
}
if (this->cyber_block_type != other.cyber_block_type) {
throw runtime_error("cyber block type not equal");
}
if (this->unknown_a11 != other.unknown_a11) {
throw runtime_error("unknown_a11 not equal");
}
if (this->unavailable_sc_cards != other.unavailable_sc_cards) {
throw runtime_error("unavailable SC cards not equal");
}
if (this->entry_states != other.entry_states) {
throw runtime_error("entry states not equal");
}
}
string MapDefinition::CameraSpec::str() const {
return string_printf(
"CameraSpec[a1=(%g %g %g %g %g %g %g %g %g) camera=(%g %g %g) focus=(%g %g %g) a2=(%g %g %g)]",
@@ -2294,31 +2379,157 @@ string CardIndex::normalize_card_name(const string& name) {
return ret;
}
MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_t language)
: map(map),
language(language) {}
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
: language(language),
compressed_data(std::move(compressed_data)) {
string decompressed = prs_decompress(this->compressed_data);
if (decompressed.size() != sizeof(MapDefinition)) {
throw runtime_error(string_printf(
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
sizeof(MapDefinition), decompressed.size()));
}
this->map.reset(new MapDefinition(*reinterpret_cast<const MapDefinition*>(decompressed.data())));
}
shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
if (!this->trial_map) {
this->trial_map.reset(new MapDefinitionTrial(*this->map));
}
return this->trial_map;
}
const std::string& MapIndex::VersionedMap::compressed(bool is_trial) const {
if (is_trial) {
if (this->compressed_trial_data.empty()) {
auto md = this->trial();
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
}
return this->compressed_trial_data;
} else {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
}
return this->compressed_data;
}
}
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
: map_number(initial_version->map->map_number),
initial_version(initial_version) {
this->versions.resize(this->initial_version->language + 1);
this->versions[this->initial_version->language] = initial_version;
}
void MapIndex::Map::add_version(std::shared_ptr<const VersionedMap> vm) {
if (this->versions.size() <= vm->language) {
this->versions.resize(vm->language + 1);
}
if (this->versions[vm->language]) {
throw runtime_error("map version already exists");
}
this->initial_version->map->assert_semantically_equivalent(*vm->map);
this->versions[vm->language] = vm;
}
bool MapIndex::Map::has_version(uint8_t language) const {
return (this->versions.size() > language) && !!this->versions[language];
}
shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language) const {
// If the requested language exists, return it
if ((language < this->versions.size()) && this->versions[language]) {
return this->versions[language];
}
// If English exists, return it
if ((1 < this->versions.size()) && this->versions[1]) {
return this->versions[1];
}
// Return the first version that exists
for (const auto& vm : this->versions) {
if (vm) {
return vm;
}
}
// This should never happen because Map cannot be constructed without an
// initial_version
throw logic_error("no map versions exist");
}
MapIndex::MapIndex(const string& directory) {
for (const auto& filename : list_directory(directory)) {
for (const auto& filename : list_directory_sorted(directory)) {
try {
shared_ptr<MapEntry> entry;
string base_filename;
string compressed_data;
shared_ptr<MapDefinition> decompressed_data;
if (ends_with(filename, ".mnmd") || ends_with(filename, ".bind")) {
entry.reset(new MapEntry(load_object_file<MapDefinition>(directory + "/" + filename)));
decompressed_data.reset(new MapDefinition(load_object_file<MapDefinition>(directory + "/" + filename)));
base_filename = filename.substr(0, filename.size() - 5);
} else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) {
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
compressed_data = load_file(directory + "/" + filename);
base_filename = filename.substr(0, filename.size() - 4);
} else if (ends_with(filename, ".bin.gci") || ends_with(filename, ".mnm.gci")) {
compressed_data = decode_gci_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 8);
} else if (ends_with(filename, ".gci")) {
entry.reset(new MapEntry(decode_gci_file(directory + "/" + filename)));
compressed_data = decode_gci_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 4);
} else if (ends_with(filename, ".bin.vms") || ends_with(filename, ".mnm.vms")) {
compressed_data = decode_vms_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 8);
} else if (ends_with(filename, ".vms")) {
compressed_data = decode_vms_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 4);
} else if (ends_with(filename, ".bin.dlq") || ends_with(filename, ".mnm.dlq")) {
compressed_data = decode_dlq_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 8);
} else if (ends_with(filename, ".dlq")) {
entry.reset(new MapEntry(decode_dlq_file(directory + "/" + filename)));
compressed_data = decode_dlq_data(load_file(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 4);
} else {
continue; // Silently skip file
}
if (entry.get()) {
if (!this->maps.emplace(entry->map.map_number, entry).second) {
throw runtime_error("duplicate map number");
}
this->maps_by_name.emplace(entry->map.name, entry);
string name = entry->map.name;
static_game_data_log.info("Indexed Episode 3 %s %s (%08" PRIX32 "; %s)",
entry->map.is_quest() ? "online quest" : "free battle map",
filename.c_str(), entry->map.map_number.load(), name.c_str());
if (base_filename.size() < 2) {
throw runtime_error("filename too short for language code");
}
if (base_filename[base_filename.size() - 2] != '-') {
throw runtime_error("language code not present");
}
uint8_t language = language_code_for_char(base_filename[base_filename.size() - 1]);
shared_ptr<VersionedMap> vm;
if (decompressed_data) {
vm.reset(new VersionedMap(decompressed_data, language));
} else if (!compressed_data.empty()) {
vm.reset(new VersionedMap(std::move(compressed_data), language));
} else {
throw runtime_error("unknown map file format");
}
string name = format_data_string(vm->map->name);
auto map_it = this->maps.find(vm->map->map_number);
if (map_it == this->maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, new Map(vm)).first;
static_game_data_log.info("(%s) Created Episode 3 map %08" PRIX32 " %c (%s; %s)",
filename.c_str(),
vm->map->map_number.load(),
char_for_language_code(vm->language),
vm->map->is_quest() ? "quest" : "free",
name.c_str());
} else {
map_it->second->add_version(vm);
static_game_data_log.info("(%s) Added Episode 3 map version %08" PRIX32 " %c (%s; %s)",
filename.c_str(),
vm->map->map_number.load(),
char_for_language_code(vm->language),
vm->map->is_quest() ? "quest" : "free",
name.c_str());
}
this->maps_by_name.emplace(vm->map->name, map_it->second);
} catch (const exception& e) {
static_game_data_log.warning("Failed to index Episode 3 map %s: %s",
@@ -2327,51 +2538,28 @@ MapIndex::MapIndex(const string& directory) {
}
}
MapIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) {}
MapIndex::MapEntry::MapEntry(const string& compressed)
: compressed_data(compressed) {
string decompressed = prs_decompress(this->compressed_data);
if (decompressed.size() != sizeof(MapDefinition)) {
throw runtime_error(string_printf(
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
sizeof(MapDefinition), decompressed.size()));
}
this->map = *reinterpret_cast<const MapDefinition*>(decompressed.data());
}
const string& MapIndex::MapEntry::compressed(bool is_trial) const {
if (is_trial) {
if (this->compressed_trial_data.empty()) {
MapDefinitionTrial mdt(this->map);
this->compressed_trial_data = prs_compress(&mdt, sizeof(mdt));
}
return this->compressed_trial_data;
} else {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
}
return this->compressed_data;
}
}
const string& MapIndex::get_compressed_list(size_t num_players) const {
const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language) const {
if (num_players == 0) {
throw runtime_error("cannot generate map list for no players");
}
if (num_players > 4) {
throw logic_error("player count is too high in map list generation");
}
string& compressed_map_list = this->compressed_map_lists.at(num_players - 1);
if (language >= this->compressed_map_lists.size()) {
this->compressed_map_lists.resize(language + 1);
}
string& compressed_map_list = this->compressed_map_lists[language].at(num_players - 1);
if (compressed_map_list.empty()) {
StringWriter entries_w;
StringWriter strings_w;
size_t num_maps = 0;
for (const auto& map_it : this->maps) {
auto vm = map_it.second->version(language);
size_t map_num_players = 0;
for (size_t z = 0; z < 4; z++) {
uint8_t player_type = map_it.second->map.entry_states[z].player_type;
uint8_t player_type = vm->map->entry_states[z].player_type;
if (player_type == 0x00 || player_type == 0x01 || player_type == 0xFF) {
map_num_players++;
}
@@ -2381,29 +2569,28 @@ const string& MapIndex::get_compressed_list(size_t num_players) const {
}
MapList::Entry e;
const auto& map = map_it.second->map;
e.map_x = map.map_x;
e.map_y = map.map_y;
e.environment_number = map.environment_number;
e.map_number = map.map_number.load();
e.width = map.width;
e.height = map.height;
e.map_tiles = map.map_tiles;
e.modification_tiles = map.modification_tiles;
e.map_x = vm->map->map_x;
e.map_y = vm->map->map_y;
e.environment_number = vm->map->environment_number;
e.map_number = vm->map->map_number.load();
e.width = vm->map->width;
e.height = vm->map->height;
e.map_tiles = vm->map->map_tiles;
e.modification_tiles = vm->map->modification_tiles;
e.name_offset = strings_w.size();
strings_w.write(map.name.data(), map.name.len());
strings_w.write(vm->map->name.data(), vm->map->name.len());
strings_w.put_u8(0);
e.location_name_offset = strings_w.size();
strings_w.write(map.location_name.data(), map.location_name.len());
strings_w.write(vm->map->location_name.data(), vm->map->location_name.len());
strings_w.put_u8(0);
e.quest_name_offset = strings_w.size();
strings_w.write(map.quest_name.data(), map.quest_name.len());
strings_w.write(vm->map->quest_name.data(), vm->map->quest_name.len());
strings_w.put_u8(0);
e.description_offset = strings_w.size();
strings_w.write(map.description.data(), map.description.len());
strings_w.write(vm->map->description.data(), vm->map->description.len());
strings_w.put_u8(0);
e.map_category = map_it.second->map.map_category;
e.map_category = vm->map->map_category;
entries_w.put(e);
num_maps++;
@@ -2434,11 +2621,11 @@ const string& MapIndex::get_compressed_list(size_t num_players) const {
return compressed_map_list;
}
shared_ptr<const MapIndex::MapEntry> MapIndex::definition_for_number(uint32_t id) const {
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
return this->maps.at(id);
}
shared_ptr<const MapIndex::MapEntry> MapIndex::definition_for_name(const string& name) const {
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
return this->maps_by_name.at(name);
}
+42 -12
View File
@@ -902,8 +902,8 @@ struct Rules {
Rules() = default;
explicit Rules(const JSON& json);
JSON json() const;
bool operator==(const Rules& other) const;
bool operator!=(const Rules& other) const;
bool operator==(const Rules& other) const = default;
bool operator!=(const Rules& other) const = default;
void clear();
void set_defaults();
@@ -1284,6 +1284,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
// 01 = DARK ONLY
// FF = any deck allowed
uint8_t deck_type;
bool operator==(const EntryState& other) const = default;
bool operator!=(const EntryState& other) const = default;
} __attribute__((packed));
/* 5A10 */ parray<EntryState, 4> entry_states;
/* 5A18 */
@@ -1292,6 +1295,12 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
return (this->map_category <= 2);
}
// This function throws runtime_error if the passed-in map is not semantically
// equivalent to *this. Semantic equivalence means all fields that affect
// gameplay and visuals are equivalent, but dialogue, names, and description
// text may differ.
void assert_semantically_equivalent(const MapDefinition& other) const;
std::string str(const CardIndex* card_index = nullptr) const;
} __attribute__((packed));
@@ -1390,30 +1399,51 @@ class MapIndex {
public:
MapIndex(const std::string& directory);
class MapEntry {
class VersionedMap {
public:
MapDefinition map;
std::shared_ptr<const MapDefinition> map;
uint8_t language;
explicit MapEntry(const MapDefinition& map);
explicit MapEntry(const std::string& compressed_data);
VersionedMap(std::shared_ptr<const MapDefinition> map, uint8_t language);
VersionedMap(std::string&& compressed_data, uint8_t language);
std::shared_ptr<const MapDefinitionTrial> trial() const;
const std::string& compressed(bool is_trial) const;
private:
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
mutable std::string compressed_data;
mutable std::string compressed_trial_data;
};
const std::string& get_compressed_list(size_t num_players) const;
std::shared_ptr<const MapEntry> definition_for_number(uint32_t id) const;
std::shared_ptr<const MapEntry> definition_for_name(const std::string& name) const;
class Map {
public:
uint32_t map_number;
std::shared_ptr<const VersionedMap> initial_version;
explicit Map(std::shared_ptr<const VersionedMap> initial_version);
void add_version(std::shared_ptr<const VersionedMap> vm);
bool has_version(uint8_t language) const;
std::shared_ptr<const VersionedMap> version(uint8_t language) const;
inline const std::vector<std::shared_ptr<const VersionedMap>>& all_versions() const {
return this->versions;
}
private:
std::vector<std::shared_ptr<const VersionedMap>> versions;
};
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
std::shared_ptr<const Map> for_number(uint32_t id) const;
std::shared_ptr<const Map> for_name(const std::string& name) const;
std::set<uint32_t> all_numbers() const;
private:
// The compressed map lists are generated on demand from the maps map below
mutable std::array<std::string, 4> compressed_map_lists;
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
std::map<uint32_t, std::shared_ptr<Map>> maps;
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
};
class COMDeckIndex {
+77 -42
View File
@@ -236,24 +236,26 @@ void Server::send_6xB4x46() const {
this->options.random_crypt->seed(),
this->options.random_crypt->absolute_offset());
if (this->last_chosen_map) {
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map.map_number.load());
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map_number);
}
cmd46.date_str2 = date_str2;
this->send(cmd46);
}
string Server::prepare_6xB6x41_map_definition(
shared_ptr<const MapIndex::MapEntry> map, bool is_trial) {
const auto& compressed = map->compressed(is_trial);
shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial) {
auto vm = map->version(language);
const auto& compressed = vm->compressed(is_trial);
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.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
w.write(compressed);
return std::move(w.str());
}
void Server::send_commands_for_joining_spectator(Channel& c, bool is_trial) const {
void Server::send_commands_for_joining_spectator(Channel& c, uint8_t language, bool is_trial) 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
@@ -265,7 +267,8 @@ void Server::send_commands_for_joining_spectator(Channel& c, bool is_trial) cons
}
if (this->last_chosen_map) {
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, is_trial);
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, language, is_trial);
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(language), this->last_chosen_map->map_number);
c.send(0x6C, 0x00, data);
}
@@ -1631,7 +1634,7 @@ const unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers({
{0x49, &Server::handle_CAx49_card_counts},
});
void Server::on_server_data_input(const string& data) {
void Server::on_server_data_input(shared_ptr<Client> sender_c, const string& data) {
auto header = check_size_t<G_CardBattleCommandHeader>(data, 0xFFFF);
if (header.size * 4 < data.size()) {
throw runtime_error("command is incomplete");
@@ -1650,10 +1653,10 @@ void Server::on_server_data_input(const string& data) {
string unmasked_data = data;
set_mask_for_ep3_game_command(unmasked_data.data(), unmasked_data.size(), 0);
(this->*handler)(unmasked_data);
(this->*handler)(sender_c, unmasked_data);
}
void Server::handle_CAx0B_mulligan_hand(const string& data) {
void Server::handle_CAx0B_mulligan_hand(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_RedrawInitialHand_GC_Ep3_6xB3x0B_CAx0B>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "REDRAW");
@@ -1684,7 +1687,7 @@ void Server::handle_CAx0B_mulligan_hand(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
}
void Server::handle_CAx0C_end_mulligan_phase(const string& data) {
void Server::handle_CAx0C_end_mulligan_phase(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndInitialRedrawPhase_GC_Ep3_6xB3x0C_CAx0C>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 2");
@@ -1739,7 +1742,7 @@ void Server::handle_CAx0C_end_mulligan_phase(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd_fin.error_code);
}
void Server::handle_CAx0D_end_non_action_phase(const string& data) {
void Server::handle_CAx0D_end_non_action_phase(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndNonAttackPhase_GC_Ep3_6xB3x0D_CAx0D>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "END PHASE");
@@ -1760,7 +1763,7 @@ void Server::handle_CAx0D_end_non_action_phase(const string& data) {
this->send(out_cmd_fin);
}
void Server::handle_CAx0E_discard_card_from_hand(const string& data) {
void Server::handle_CAx0E_discard_card_from_hand(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_DiscardCardFromHand_GC_Ep3_6xB3x0E_CAx0E>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "DISCARD");
@@ -1799,7 +1802,7 @@ void Server::handle_CAx0E_discard_card_from_hand(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
}
void Server::handle_CAx0F_set_card_from_hand(const string& data) {
void Server::handle_CAx0F_set_card_from_hand(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_SetCardFromHand_GC_Ep3_6xB3x0F_CAx0F>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "SET FC");
@@ -1841,7 +1844,7 @@ void Server::handle_CAx0F_set_card_from_hand(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
}
void Server::handle_CAx10_move_fc_to_location(const string& data) {
void Server::handle_CAx10_move_fc_to_location(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_MoveFieldCharacter_GC_Ep3_6xB3x10_CAx10>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "MOVE");
@@ -1879,7 +1882,7 @@ void Server::handle_CAx10_move_fc_to_location(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
}
void Server::handle_CAx11_enqueue_attack_or_defense(const string& data) {
void Server::handle_CAx11_enqueue_attack_or_defense(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EnqueueAttackOrDefense_GC_Ep3_6xB3x11_CAx11>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "ENQUEUE ACT");
@@ -1915,7 +1918,7 @@ void Server::handle_CAx11_enqueue_attack_or_defense(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, out_cmd.error_code);
}
void Server::handle_CAx12_end_attack_list(const string& data) {
void Server::handle_CAx12_end_attack_list(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndAttackList_GC_Ep3_6xB3x12_CAx12>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "END ATK LIST");
@@ -1938,7 +1941,7 @@ void Server::handle_CAx12_end_attack_list(const string& data) {
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_CAx13_update_map_during_setup(const string& data) {
void Server::handle_CAx13_update_map_during_setup(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_SetMapState_GC_Ep3_6xB3x13_CAx13>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "UPDATE MAP");
@@ -1980,7 +1983,7 @@ void Server::handle_CAx13_update_map_during_setup(const string& data) {
}
}
void Server::handle_CAx14_update_deck_during_setup(const string& data) {
void Server::handle_CAx14_update_deck_during_setup(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_SetPlayerDeck_GC_Ep3_6xB3x14_CAx14>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "UPDATE DECK");
@@ -2027,7 +2030,7 @@ void Server::handle_CAx14_update_deck_during_setup(const string& data) {
}
}
void Server::handle_CAx15_unused_hard_reset_server_state(const string& data) {
void Server::handle_CAx15_unused_hard_reset_server_state(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_HardResetServerState_GC_Ep3_6xB3x15_CAx15>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "HARD RESET");
@@ -2045,7 +2048,7 @@ void Server::handle_CAx15_unused_hard_reset_server_state(const string& data) {
throw runtime_error("hard reset command received");
}
void Server::handle_CAx1B_update_player_name(const string& data) {
void Server::handle_CAx1B_update_player_name(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_SetPlayerName_GC_Ep3_6xB3x1B_CAx1B>(data);
this->send_debug_command_received_message(
in_cmd.entry.client_id, in_cmd.header.subsubcommand, "UPDATE NAME");
@@ -2077,7 +2080,7 @@ void Server::handle_CAx1B_update_player_name(const string& data) {
this->send(out_cmd);
}
void Server::handle_CAx1D_start_battle(const string& data) {
void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_StartBattle_GC_Ep3_6xB3x1D_CAx1D>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "START BATTLE");
@@ -2116,7 +2119,7 @@ void Server::handle_CAx1D_start_battle(const string& data) {
}
}
void Server::handle_CAx21_end_battle(const string& data) {
void Server::handle_CAx21_end_battle(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndBattle_GC_Ep3_6xB3x21_CAx21>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "END BATTLE");
@@ -2131,7 +2134,7 @@ void Server::handle_CAx21_end_battle(const string& data) {
}
}
void Server::handle_CAx28_end_defense_list(const string& data) {
void Server::handle_CAx28_end_defense_list(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndDefenseList_GC_Ep3_6xB3x28_CAx28>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
@@ -2184,13 +2187,13 @@ void Server::handle_CAx28_end_defense_list(const string& data) {
this->send(out_cmd_fin);
}
void Server::handle_CAx2B_legacy_set_card(const string& data) {
void Server::handle_CAx2B_legacy_set_card(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_ExecLegacyCard_GC_Ep3_6xB3x2B_CAx2B>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "EXEC LEGACY");
// Sega's original implementation does nothing here, so we do nothing as well.
}
void Server::handle_CAx34_subtract_ally_atk_points(const string& data) {
void Server::handle_CAx34_subtract_ally_atk_points(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_PhotonBlastRequest_GC_Ep3_6xB3x34_CAx34>(data);
uint8_t card_ref_client_id = client_id_for_card_ref(in_cmd.card_ref);
@@ -2267,7 +2270,7 @@ void Server::handle_CAx34_subtract_ally_atk_points(const string& data) {
}
}
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const string& data) {
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37_CAx37>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 1");
@@ -2297,14 +2300,14 @@ void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const
}
}
void Server::handle_CAx3A_time_limit_expired(const string& data) {
void Server::handle_CAx3A_time_limit_expired(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_OverallTimeLimitExpired_GC_Ep3_6xB3x3A_CAx3A>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "TIME EXPIRED");
// We don't need to do anything here because the overall time limit is tracked
// server-side instead.
}
void Server::handle_CAx40_map_list_request(const string& data) {
void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const string& data) {
const auto& in_cmd = check_size_t<G_MapListRequest_GC_Ep3_6xB3x40_CAx40>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "MAP LIST");
@@ -2314,7 +2317,8 @@ void Server::handle_CAx40_map_list_request(const string& data) {
throw runtime_error("lobby is deleted");
}
const auto& list_data = this->options.map_index->get_compressed_list(l->count_clients());
const auto& list_data = this->options.map_index->get_compressed_list(
l->count_clients(), sender_c->language());
StringWriter w;
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
@@ -2332,30 +2336,61 @@ void Server::handle_CAx40_map_list_request(const string& data) {
}
}
void Server::handle_CAx41_map_request(const string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_GC_Ep3_6xB3x41_CAx41>(data);
this->send_debug_command_received_message(
cmd.header.subsubcommand, "MAP DATA");
void Server::send_6xB6x41_to_all_clients() const {
auto l = this->lobby.lock();
if (!l) {
throw runtime_error("lobby is deleted");
}
this->last_chosen_map = this->options.map_index->definition_for_number(cmd.map_number);
auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map, l->flags & Lobby::Flag::IS_EP3_TRIAL);
send_command(l, 0x6C, 0x00, out_cmd);
vector<string> map_commands_by_language;
auto send_to_client = [&](shared_ptr<Client> c) -> void {
if (!c) {
return;
}
uint8_t language = c->language();
if (map_commands_by_language.size() <= language) {
map_commands_by_language.resize(language + 1);
}
if (map_commands_by_language[language].empty()) {
map_commands_by_language[language] = this->prepare_6xB6x41_map_definition(
this->last_chosen_map, language, l->flags & Lobby::Flag::IS_EP3_TRIAL);
}
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(language), this->last_chosen_map->map_number);
send_command(c, 0x6C, 0x00, map_commands_by_language[language]);
};
for (const auto& c : l->clients) {
send_to_client(c);
}
for (auto watcher_l : l->watcher_lobbies) {
send_command_if_not_loading(watcher_l, 0x6C, 0x00, out_cmd);
for (const auto& c : watcher_l->clients) {
send_to_client(c);
}
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(out_cmd));
// TODO: It's not great that we just pick the first one; ideally we'd put
// all of them in the recording and send the appropriate one to the client
// in the playback lobby
for (string& data : map_commands_by_language) {
if (!data.empty()) {
l->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
break;
}
}
}
}
void Server::handle_CAx48_end_turn(const string& data) {
void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_GC_Ep3_6xB3x41_CAx41>(data);
this->send_debug_command_received_message(
cmd.header.subsubcommand, "MAP DATA");
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
this->send_6xB6x41_to_all_clients();
}
void Server::handle_CAx48_end_turn(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_EndTurn_GC_Ep3_6xB3x48_CAx48>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "END TURN");
@@ -2373,7 +2408,7 @@ void Server::handle_CAx48_end_turn(const string& data) {
this->send(out_cmd);
}
void Server::handle_CAx49_card_counts(const string& data) {
void Server::handle_CAx49_card_counts(shared_ptr<Client>, const string& data) {
const auto& in_cmd = check_size_t<G_CardCounts_GC_Ep3_6xB3x49_CAx49>(data);
this->send_debug_command_received_message(
in_cmd.header.sender_client_id, in_cmd.header.subsubcommand, "CARD COUNTS");
+29 -28
View File
@@ -111,7 +111,7 @@ public:
this->send(&cmd, cmd.header.size * 4);
}
void send(const void* data, size_t size) const;
void send_commands_for_joining_spectator(Channel& ch, bool is_trial) const;
void send_commands_for_joining_spectator(Channel& ch, uint8_t language, bool is_trial) const;
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
void force_destroy_field_character(uint8_t client_id, size_t set_index);
@@ -181,30 +181,30 @@ public:
void update_battle_state_flags_and_send_6xB4x03_if_needed(
bool always_send = false);
bool update_registration_phase();
void on_server_data_input(const std::string& data);
void handle_CAx0B_mulligan_hand(const std::string& data);
void handle_CAx0C_end_mulligan_phase(const std::string& data);
void handle_CAx0D_end_non_action_phase(const std::string& data);
void handle_CAx0E_discard_card_from_hand(const std::string& data);
void handle_CAx0F_set_card_from_hand(const std::string& data);
void handle_CAx10_move_fc_to_location(const std::string& data);
void handle_CAx11_enqueue_attack_or_defense(const std::string& data);
void handle_CAx12_end_attack_list(const std::string& data);
void handle_CAx13_update_map_during_setup(const std::string& data);
void handle_CAx14_update_deck_during_setup(const std::string& data);
void handle_CAx15_unused_hard_reset_server_state(const std::string& data);
void handle_CAx1B_update_player_name(const std::string& data);
void handle_CAx1D_start_battle(const std::string& data);
void handle_CAx21_end_battle(const std::string& data);
void handle_CAx28_end_defense_list(const std::string& data);
void handle_CAx2B_legacy_set_card(const std::string&);
void handle_CAx34_subtract_ally_atk_points(const std::string& data);
void handle_CAx37_client_ready_to_advance_from_starter_roll_phase(const std::string& data);
void handle_CAx3A_time_limit_expired(const std::string& data);
void handle_CAx40_map_list_request(const std::string& data);
void handle_CAx41_map_request(const std::string& data);
void handle_CAx48_end_turn(const std::string& data);
void handle_CAx49_card_counts(const std::string& data);
void on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0B_mulligan_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0C_end_mulligan_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0D_end_non_action_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0E_discard_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0F_set_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx10_move_fc_to_location(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx12_end_attack_list(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx13_update_map_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx14_update_deck_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx1B_update_player_name(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx1D_start_battle(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx21_end_battle(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx28_end_defense_list(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx2B_legacy_set_card(std::shared_ptr<Client> sender_c, const std::string&);
void handle_CAx34_subtract_ally_atk_points(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx37_client_ready_to_advance_from_starter_roll_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx3A_time_limit_expired(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx40_map_list_request(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx41_map_request(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx48_end_turn(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx49_card_counts(std::shared_ptr<Client> sender_c, const std::string& data);
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
uint32_t get_team_exp(uint8_t team_id) const;
uint32_t send_6xB4x06_if_card_ref_invalid(
@@ -226,21 +226,22 @@ public:
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 MapIndex::MapEntry> map, bool is_trial);
std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial);
void send_6xB6x41_to_all_clients() const;
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);
private:
typedef void (Server::*handler_t)(const std::string&);
typedef void (Server::*handler_t)(std::shared_ptr<Client>, const std::string&);
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
public:
// These fields are not part of the original implementation
std::weak_ptr<Lobby> lobby;
Options options;
std::shared_ptr<const MapIndex::MapEntry> last_chosen_map;
std::shared_ptr<const MapIndex::Map> last_chosen_map;
bool tournament_match_result_sent;
uint8_t override_environment_number;
mutable std::deque<StackLogger*> logger_stack;
+11 -6
View File
@@ -314,7 +314,7 @@ Tournament::Tournament(
shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index,
const string& name,
shared_ptr<const MapIndex::MapEntry> map,
shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
uint8_t flags)
@@ -355,7 +355,7 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name");
this->map = this->map_index->definition_for_number(this->source_json.get_int("map_number"));
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
this->rules = Rules(this->source_json.at("rules"));
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {
@@ -531,7 +531,7 @@ JSON Tournament::json() const {
}
return JSON::dict({
{"name", this->name},
{"map_number", this->map->map.map_number.load()},
{"map_number", this->map->map_number},
{"rules", this->rules.json()},
{"flags", this->flags},
{"is_registration_complete", (this->current_state != State::REGISTRATION)},
@@ -741,8 +741,13 @@ void Tournament::print_bracket(FILE* stream) const {
}
};
fprintf(stream, "Tournament \"%s\"\n", this->name.c_str());
string map_name = this->map->map.name;
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str());
auto en_vm = this->map->version(1);
if (en_vm) {
string map_name = en_vm->map->name;
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map_number, map_name.c_str());
} else {
fprintf(stream, " Map: %08" PRIX32 "\n", this->map->map_number);
}
string rules_str = this->rules.str();
fprintf(stream, " Rules: %s\n", rules_str.c_str());
fprintf(stream, " Structure: %s, %zu entries\n", (this->flags & Flag::IS_2V2) ? "2v2" : "1v1", this->num_teams);
@@ -850,7 +855,7 @@ void TournamentIndex::save() const {
shared_ptr<Tournament> TournamentIndex::create_tournament(
const string& name,
shared_ptr<const MapIndex::MapEntry> map,
shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
uint8_t flags) {
+4 -4
View File
@@ -108,7 +108,7 @@ public:
std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index,
const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map,
std::shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
uint8_t flags);
@@ -124,7 +124,7 @@ public:
inline const std::string& get_name() const {
return this->name;
}
inline std::shared_ptr<const MapIndex::MapEntry> get_map() const {
inline std::shared_ptr<const MapIndex::Map> get_map() const {
return this->map;
}
inline const Rules& get_rules() const {
@@ -171,7 +171,7 @@ private:
std::shared_ptr<const COMDeckIndex> com_deck_index;
JSON source_json;
std::string name;
std::shared_ptr<const MapIndex::MapEntry> map;
std::shared_ptr<const MapIndex::Map> map;
Rules rules;
size_t num_teams;
uint8_t flags;
@@ -225,7 +225,7 @@ public:
std::shared_ptr<Tournament> create_tournament(
const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map,
std::shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
uint8_t flags);