reimplement Episode 3 map categories
This commit is contained in:
+88
-24
@@ -2670,8 +2670,9 @@ std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() cons
|
||||
return this->download_data_trial;
|
||||
}
|
||||
|
||||
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
|
||||
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version, uint8_t visibility_flags)
|
||||
: map_number(initial_version->map->map_number),
|
||||
visibility_flags(visibility_flags),
|
||||
initial_version(initial_version) {
|
||||
size_t lang_index = static_cast<size_t>(this->initial_version->language);
|
||||
this->versions.resize(lang_index + 1);
|
||||
@@ -2718,40 +2719,47 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(Language languag
|
||||
throw logic_error("no map versions exist");
|
||||
}
|
||||
|
||||
MapIndex::MapIndex(const string& directory) {
|
||||
MapIndex::Category::Category(uint32_t category_id, const phosg::JSON& json)
|
||||
: category_id(category_id),
|
||||
visibility_flags(json.get_int("VisibilityFlags")),
|
||||
name(json.get_string("Name", "")),
|
||||
description(json.get_string("Description", "")) {}
|
||||
|
||||
MapIndex::MapIndex(const string& directory, bool raise_on_any_failure) {
|
||||
map<uint32_t, shared_ptr<Map>> mutable_maps;
|
||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
||||
string filename = item.path().filename().string();
|
||||
|
||||
auto try_add_map_file = [&](std::shared_ptr<Category> category, const std::string& file_path) -> void {
|
||||
try {
|
||||
string filename = phosg::basename(file_path);
|
||||
string base_filename;
|
||||
string compressed_data;
|
||||
shared_ptr<MapDefinition> decompressed_data;
|
||||
if (filename.ends_with(".mnmd") || filename.ends_with(".bind")) {
|
||||
decompressed_data = make_shared<MapDefinition>(phosg::load_object_file<MapDefinition>(directory + "/" + filename));
|
||||
decompressed_data = make_shared<MapDefinition>(phosg::load_object_file<MapDefinition>(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 5);
|
||||
} else if (filename.ends_with(".mnm") || filename.ends_with(".bin")) {
|
||||
compressed_data = phosg::load_file(directory + "/" + filename);
|
||||
compressed_data = phosg::load_file(file_path);
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (filename.ends_with(".bin.gci") || filename.ends_with(".mnm.gci")) {
|
||||
compressed_data = decode_gci_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_gci_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (filename.ends_with(".gci")) {
|
||||
compressed_data = decode_gci_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_gci_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (filename.ends_with(".bin.vms") || filename.ends_with(".mnm.vms")) {
|
||||
compressed_data = decode_vms_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_vms_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (filename.ends_with(".vms")) {
|
||||
compressed_data = decode_vms_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_vms_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else if (filename.ends_with(".bin.dlq") || filename.ends_with(".mnm.dlq")) {
|
||||
compressed_data = decode_dlq_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_dlq_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 8);
|
||||
} else if (filename.ends_with(".dlq")) {
|
||||
compressed_data = decode_dlq_data(phosg::load_file(directory + "/" + filename));
|
||||
compressed_data = decode_dlq_data(phosg::load_file(file_path));
|
||||
base_filename = filename.substr(0, filename.size() - 4);
|
||||
} else {
|
||||
continue; // Silently skip file
|
||||
return; // Silently skip file
|
||||
}
|
||||
|
||||
if (base_filename.size() < 2) {
|
||||
@@ -2771,18 +2779,31 @@ MapIndex::MapIndex(const string& directory) {
|
||||
throw runtime_error("unknown map file format");
|
||||
}
|
||||
|
||||
uint8_t visibility_flags = category ? category->visibility_flags : 0x00;
|
||||
|
||||
string name = vm->map->name.decode(vm->language);
|
||||
auto map_it = mutable_maps.find(vm->map->map_number);
|
||||
if (map_it == mutable_maps.end()) {
|
||||
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
|
||||
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm, visibility_flags)).first;
|
||||
this->maps.emplace(vm->map->map_number, map_it->second);
|
||||
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
|
||||
|
||||
string in_category_str;
|
||||
if (category) {
|
||||
in_category_str = std::format(" in category {}", category->name);
|
||||
category->add_map(map_it->second);
|
||||
}
|
||||
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {}{} ({}; {})",
|
||||
filename,
|
||||
vm->map->map_number,
|
||||
char_for_language(vm->language),
|
||||
in_category_str,
|
||||
vm->map->is_quest() ? "quest" : "free",
|
||||
name);
|
||||
} else {
|
||||
if (map_it->second->visibility_flags != visibility_flags) {
|
||||
throw std::runtime_error(std::format("visibility flags {:02X} for added map {} do not match existing flags {}",
|
||||
map_it->second->visibility_flags, file_path, visibility_flags));
|
||||
}
|
||||
map_it->second->add_version(vm);
|
||||
static_game_data_log.debug_f("({}) Added Episode 3 map version {:08X} {} ({}; {})",
|
||||
filename,
|
||||
@@ -2794,13 +2815,45 @@ MapIndex::MapIndex(const string& directory) {
|
||||
this->maps_by_name.emplace(vm->map->name.decode(vm->language), map_it->second);
|
||||
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning_f("Failed to index Episode 3 map {}: {}",
|
||||
filename, e.what());
|
||||
if (raise_on_any_failure) {
|
||||
throw;
|
||||
}
|
||||
static_game_data_log.warning_f("Failed to index Episode 3 map {}: {}", file_path, e.what());
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& cat_item : std::filesystem::directory_iterator(directory)) {
|
||||
string cat_dir_path = cat_item.path().string();
|
||||
|
||||
if (cat_item.is_directory()) {
|
||||
shared_ptr<Category> category;
|
||||
try {
|
||||
string json_filename = std::format("{}/{}", cat_item.path().string(), "category.json");
|
||||
auto category_json = phosg::JSON::parse(phosg::load_file(json_filename));
|
||||
uint32_t category_id = this->categories.size() + 1;
|
||||
auto category = make_shared<Category>(category_id, category_json);
|
||||
this->categories.emplace(category_id, category);
|
||||
static_game_data_log.debug_f("({}) Created Episode 3 map category {:08X} ({})",
|
||||
cat_item.path().filename().string(), category_id, category->name);
|
||||
|
||||
for (const auto& map_item : std::filesystem::directory_iterator(cat_item)) {
|
||||
try_add_map_file(category, map_item.path().string());
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
if (raise_on_any_failure) {
|
||||
throw;
|
||||
}
|
||||
static_game_data_log.warning_f("Failed to index Episode 3 map category {}: {}", cat_item.path().string(), e.what());
|
||||
}
|
||||
|
||||
} else {
|
||||
try_add_map_file(nullptr, cat_dir_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const string& MapIndex::get_compressed_list(size_t num_players, Language language) const {
|
||||
const string& MapIndex::get_compressed_list(size_t num_players, Language language, bool is_trial) const {
|
||||
if (num_players == 0) {
|
||||
throw runtime_error("cannot generate map list for no players");
|
||||
}
|
||||
@@ -2808,17 +2861,27 @@ const string& MapIndex::get_compressed_list(size_t num_players, Language languag
|
||||
throw logic_error("player count is too high in map list generation");
|
||||
}
|
||||
|
||||
auto& compressed_lists = is_trial ? this->compressed_map_lists_trial : this->compressed_map_lists_final;
|
||||
|
||||
size_t lang_index = static_cast<size_t>(language);
|
||||
if (lang_index >= this->compressed_map_lists.size()) {
|
||||
this->compressed_map_lists.resize(lang_index + 1);
|
||||
if (lang_index >= compressed_lists.size()) {
|
||||
compressed_lists.resize(lang_index + 1);
|
||||
}
|
||||
string& compressed_map_list = this->compressed_map_lists[lang_index].at(num_players - 1);
|
||||
string& compressed_map_list = compressed_lists[lang_index].at(num_players - 1);
|
||||
if (compressed_map_list.empty()) {
|
||||
phosg::StringWriter entries_w;
|
||||
phosg::StringWriter strings_w;
|
||||
|
||||
auto vis_flag = is_trial
|
||||
? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL
|
||||
: Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL;
|
||||
|
||||
size_t num_maps = 0;
|
||||
for (const auto& map_it : this->maps) {
|
||||
if (!map_it.second->check_visibility_flag(vis_flag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto vm = map_it.second->version(language);
|
||||
size_t map_num_players = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2875,11 +2938,12 @@ const string& MapIndex::get_compressed_list(size_t num_players, Language languag
|
||||
compressed_w.write(prs.close());
|
||||
compressed_map_list = std::move(compressed_w.str());
|
||||
if (compressed_map_list.size() > 0x7BEC) {
|
||||
throw runtime_error(std::format("compressed map list for {} players is too large (0x{:X} bytes)", num_players, compressed_map_list.size()));
|
||||
throw runtime_error(std::format("compressed {} map list for {} players is too large (0x{:X} bytes)",
|
||||
is_trial ? "trial" : "final", num_players, compressed_map_list.size()));
|
||||
}
|
||||
size_t decompressed_size = sizeof(header) + entries_w.size() + strings_w.size();
|
||||
static_game_data_log.info_f("Generated Episode 3 compressed map list for {} player(s) ({} maps; 0x{:X} -> 0x{:X} bytes)",
|
||||
num_players, num_maps, decompressed_size, compressed_map_list.size());
|
||||
static_game_data_log.info_f("Generated Episode 3 compressed {} map list for {} player(s) ({} maps; 0x{:X} -> 0x{:X} bytes)",
|
||||
is_trial ? "trial" : "final", num_players, num_maps, decompressed_size, compressed_map_list.size());
|
||||
}
|
||||
return compressed_map_list;
|
||||
}
|
||||
|
||||
@@ -1587,7 +1587,12 @@ private:
|
||||
|
||||
class MapIndex {
|
||||
public:
|
||||
explicit MapIndex(const std::string& directory);
|
||||
enum class VisibilityFlag : uint8_t {
|
||||
ONLINE_TRIAL = 0x01,
|
||||
ONLINE_FINAL = 0x02,
|
||||
DOWNLOAD_TRIAL = 0x04,
|
||||
DOWNLOAD_FINAL = 0x08,
|
||||
};
|
||||
|
||||
class VersionedMap {
|
||||
public:
|
||||
@@ -1611,9 +1616,14 @@ public:
|
||||
class Map {
|
||||
public:
|
||||
uint32_t map_number;
|
||||
uint8_t visibility_flags;
|
||||
std::shared_ptr<const VersionedMap> initial_version;
|
||||
|
||||
explicit Map(std::shared_ptr<const VersionedMap> initial_version);
|
||||
Map(std::shared_ptr<const VersionedMap> initial_version, uint8_t visibility_flags);
|
||||
|
||||
inline bool check_visibility_flag(VisibilityFlag flag) const {
|
||||
return (this->visibility_flags & static_cast<uint8_t>(flag));
|
||||
}
|
||||
|
||||
void add_version(std::shared_ptr<const VersionedMap> vm);
|
||||
bool has_version(Language language) const;
|
||||
@@ -1626,24 +1636,76 @@ public:
|
||||
std::vector<std::shared_ptr<const VersionedMap>> versions;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_list(size_t num_players, Language language) const;
|
||||
inline std::shared_ptr<const Map> get(uint32_t id) const {
|
||||
class Category {
|
||||
public:
|
||||
uint32_t category_id;
|
||||
uint8_t visibility_flags;
|
||||
std::string name;
|
||||
std::string description;
|
||||
|
||||
Category(uint32_t category_id, const phosg::JSON& json);
|
||||
|
||||
inline bool check_visibility_flag(VisibilityFlag flag) const {
|
||||
return (this->visibility_flags & static_cast<uint8_t>(flag));
|
||||
}
|
||||
|
||||
inline void add_map(std::shared_ptr<const Map> map) {
|
||||
this->maps.emplace(map->map_number, map);
|
||||
}
|
||||
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all_maps() const {
|
||||
return this->maps;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
||||
};
|
||||
|
||||
explicit MapIndex(const std::string& directory, bool raise_on_any_failure = false);
|
||||
|
||||
const std::string& get_compressed_list(size_t num_players, Language language, bool is_trial) const;
|
||||
inline std::shared_ptr<const Map> map_for_id(uint32_t id) const {
|
||||
return this->maps.at(id);
|
||||
}
|
||||
inline std::shared_ptr<const Map> get(const std::string& name) const {
|
||||
inline std::shared_ptr<const Map> map_for_name(const std::string& name) const {
|
||||
return this->maps_by_name.at(name);
|
||||
}
|
||||
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all() const {
|
||||
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all_maps() const {
|
||||
return this->maps;
|
||||
}
|
||||
|
||||
inline std::shared_ptr<const Category> category_for_id(uint32_t id) const {
|
||||
return this->categories.at(id);
|
||||
}
|
||||
inline const std::map<uint32_t, std::shared_ptr<const Category>>& all_categories() const {
|
||||
return this->categories;
|
||||
}
|
||||
|
||||
private:
|
||||
// The compressed map lists are generated on demand from the maps map below
|
||||
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
|
||||
// The compressed map lists are generated on demand from the maps map below.
|
||||
// THey are indexed as [language][num_players]
|
||||
mutable std::vector<std::array<std::string, 4>> compressed_map_lists_trial;
|
||||
mutable std::vector<std::array<std::string, 4>> compressed_map_lists_final;
|
||||
|
||||
std::map<uint32_t, std::shared_ptr<const Category>> categories;
|
||||
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
||||
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
|
||||
};
|
||||
|
||||
class MapCategoryIndex {
|
||||
public:
|
||||
explicit MapCategoryIndex(const std::string& directory);
|
||||
|
||||
inline std::shared_ptr<const MapIndex> get(uint32_t id) const {
|
||||
return this->indexes.at(id);
|
||||
}
|
||||
inline const std::map<uint32_t, std::shared_ptr<const MapIndex>>& all() const {
|
||||
return this->indexes;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<uint32_t, std::shared_ptr<const MapIndex>> indexes;
|
||||
};
|
||||
|
||||
class COMDeckIndex {
|
||||
public:
|
||||
COMDeckIndex(const std::string& filename);
|
||||
|
||||
@@ -2527,7 +2527,7 @@ void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const st
|
||||
|
||||
size_t num_players = l ? l->count_clients() : 1;
|
||||
Language language = sender_c ? sender_c->language() : Language::ENGLISH;
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language);
|
||||
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language, this->options.is_nte());
|
||||
|
||||
phosg::StringWriter w;
|
||||
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_Ep3_6xB6x40) + 3) & (~3);
|
||||
@@ -2596,7 +2596,7 @@ void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
|
||||
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
|
||||
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
|
||||
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
|
||||
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
|
||||
this->last_chosen_map = this->options.map_index->map_for_id(cmd.map_number);
|
||||
this->send_6xB6x41_to_all_clients();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,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->get(this->source_json.get_int("map_number"));
|
||||
this->map = this->map_index->map_for_id(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)) {
|
||||
|
||||
Reference in New Issue
Block a user