From d052163a9eab6ba6c5cd2c53b1ae60c980fc125a Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 12 Jan 2024 23:54:23 -0800 Subject: [PATCH] add brute-force search command for game seeds that result in rare enemies --- src/FileContentsCache.cc | 22 ++++ src/FileContentsCache.hh | 19 ++++ src/Lobby.cc | 219 +++++++++++++++++++++------------------ src/Lobby.hh | 20 ++++ src/Main.cc | 151 +++++++++++++++++++++++++++ src/Map.cc | 60 +++++++++-- src/Map.hh | 4 +- src/Quest.cc | 9 ++ src/Quest.hh | 2 + src/ServerState.cc | 50 ++++++++- src/ServerState.hh | 6 +- src/Text.hh | 11 +- 12 files changed, 455 insertions(+), 118 deletions(-) diff --git a/src/FileContentsCache.cc b/src/FileContentsCache.cc index e2638ed0..15c176a1 100644 --- a/src/FileContentsCache.cc +++ b/src/FileContentsCache.cc @@ -74,3 +74,25 @@ FileContentsCache::GetResult FileContentsCache::get(const char* name, std::function generate) { return this->get(string(name), generate); } + +shared_ptr ThreadSafeFileCache::get( + const string& name, std::function(const std::string&)> generate) { + try { + shared_lock g(this->lock); + auto ret = this->name_to_file.at(name); + if (!ret) { + throw cannot_open_file(name); + } + return ret; + } catch (const out_of_range&) { + unique_lock g(this->lock); + auto it = this->name_to_file.find(name); + if (it == this->name_to_file.end()) { + it = this->name_to_file.emplace(name, generate(name)).first; + } + if (!it->second) { + throw cannot_open_file(name); + } + return it->second; + } +} diff --git a/src/FileContentsCache.hh b/src/FileContentsCache.hh index eb2b5bac..1d50be60 100644 --- a/src/FileContentsCache.hh +++ b/src/FileContentsCache.hh @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -102,3 +103,21 @@ private: std::unordered_map> name_to_file; uint64_t ttl_usecs; }; + +class ThreadSafeFileCache { +public: + explicit ThreadSafeFileCache() = default; + ThreadSafeFileCache(const ThreadSafeFileCache&) = delete; + ThreadSafeFileCache(ThreadSafeFileCache&&) = delete; + ThreadSafeFileCache& operator=(const ThreadSafeFileCache&) = delete; + ThreadSafeFileCache& operator=(ThreadSafeFileCache&&) = delete; + ~ThreadSafeFileCache() = default; + + // Warning: generate() is called while the lock is held for writing, so it + // will block other threads. + std::shared_ptr get(const std::string& name, std::function(const std::string&)> generate); + +private: + std::shared_mutex lock; + std::unordered_map> name_to_file; +}; diff --git a/src/Lobby.cc b/src/Lobby.cc index cab4d8e8..ec4695de 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -261,14 +261,109 @@ void Lobby::create_item_creator() { this->quest ? this->quest->battle_rules : nullptr); } -void Lobby::load_maps() { - this->map = make_shared(this->base_version, this->lobby_id, this->random_crypt); +shared_ptr Lobby::load_maps( + Version version, + Episode episode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + shared_ptr rare_rates, + shared_ptr random_crypt, + shared_ptr vq) { + auto map = make_shared(version, lobby_id, random_crypt); + auto dat_contents = prs_decompress(*vq->dat_contents); + map->add_enemies_and_objects_from_quest_data( + episode, + difficulty, + event, + dat_contents.data(), + dat_contents.size(), + rare_rates); + return map; +} +shared_ptr Lobby::load_maps( + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + function(Version, const string&)> get_file_data, + shared_ptr rare_rates, + shared_ptr random_crypt, + const parray& variations) { + auto map = make_shared(version, lobby_id, random_crypt); + + // Don't load free-roam maps in Challenge mode, since players can't go to + // Ragol without a quest loaded + if (mode == GameMode::CHALLENGE) { + return map; + } + + for (size_t floor = 0; floor < 0x10; floor++) { + auto enemy_filenames = map_filenames_for_variation( + version, + episode, + mode, + floor, + variations[floor * 2], + variations[floor * 2 + 1], + true); + if (!enemy_filenames.empty()) { + bool any_map_loaded = false; + for (const string& filename : enemy_filenames) { + auto map_data = get_file_data(version, filename); + if (map_data) { + map->add_enemies_from_map_data( + episode, + difficulty, + event, + floor, + map_data->data(), + map_data->size(), + rare_rates); + any_map_loaded = true; + break; + } + } + if (!any_map_loaded) { + throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor)); + } + } + + auto object_filenames = map_filenames_for_variation( + version, + episode, + mode, + floor, + variations[floor * 2], + variations[floor * 2 + 1], + false); + if (!object_filenames.empty()) { + bool any_map_loaded = false; + for (const string& filename : object_filenames) { + auto map_data = get_file_data(version, filename); + if (map_data) { + map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); + any_map_loaded = true; + break; + } + } + if (!any_map_loaded) { + throw runtime_error(string_printf("no object maps loaded for floor %zu", floor)); + } + } + } + + return map; +} + +void Lobby::load_maps() { auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates) ? this->rare_enemy_rates : Map::DEFAULT_RARE_ENEMIES; - auto s = this->require_server_state(); if (this->quest) { auto leader_c = this->clients.at(this->leader_id); if (!leader_c) { @@ -276,110 +371,32 @@ void Lobby::load_maps() { } auto vq = this->quest->version(this->base_version, leader_c->language()); - auto dat_contents = prs_decompress(*vq->dat_contents); - this->map->clear(); - this->map->add_enemies_and_objects_from_quest_data( + this->map = this->load_maps( + this->base_version, this->episode, this->difficulty, this->event, - dat_contents.data(), - dat_contents.size(), - this->random_seed, - rare_rates); + this->lobby_id, + rare_rates, + this->random_crypt, + vq); } else if (this->mode != GameMode::CHALLENGE) { - // No quest loaded - load free-roam maps instead. Don't load free-roam maps - // in Challenge mode, since players can't go to Ragol without a quest loaded + auto s = this->require_server_state(); + this->map = this->load_maps( + this->base_version, + this->episode, + this->mode, + this->difficulty, + this->event, + this->lobby_id, + bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), + rare_rates, + this->random_crypt, + this->variations); - for (size_t floor = 0; floor < 0x10; floor++) { - this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32, - floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load()); - - auto get_file_data = [&](const string& filename) -> shared_ptr { - if (this->base_version == Version::BB_V4) { - try { - return s->load_bb_file(filename); - } catch (const exception& e) { - this->log.info("[Map/%zu:e] Failed to load %s from BB patch tree: %s", floor, filename.c_str(), e.what()); - } - } else if (this->base_version == Version::PC_V2) { - try { - string path = "system/patch-pc/Media/PSO/" + filename; - auto ret = make_shared(load_file(path)); - this->log.info("[Map/%zu:e] Loaded %s from PC patch tree", floor, filename.c_str()); - return ret; - } catch (const exception& e) { - this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what()); - } - } - try { - string path = string_printf("system/maps/%s/%s", file_path_token_for_version(this->base_version), filename.c_str()); - auto ret = make_shared(load_file(path)); - this->log.info("[Map/%zu:e] Loaded %s from default maps", floor, filename.c_str()); - return ret; - } catch (const exception& e) { - this->log.info("[Map/%zu:e] Failed to load %s default maps: %s", floor, filename.c_str(), e.what()); - } - return nullptr; - }; - - auto enemy_filenames = map_filenames_for_variation( - this->base_version, - this->episode, - this->mode, - floor, - this->variations[floor * 2], - this->variations[floor * 2 + 1], - true); - if (enemy_filenames.empty()) { - this->log.info("[Map/%zu:e] No file to load", floor); - } else { - bool any_map_loaded = false; - for (const string& filename : enemy_filenames) { - auto map_data = get_file_data(filename); - if (map_data) { - this->map->add_enemies_from_map_data( - this->episode, - this->difficulty, - this->event, - floor, - map_data->data(), - map_data->size(), - rare_rates); - any_map_loaded = true; - break; - } - } - if (!any_map_loaded) { - throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor)); - } - } - - auto object_filenames = map_filenames_for_variation( - this->base_version, - this->episode, - this->mode, - floor, - this->variations[floor * 2], - this->variations[floor * 2 + 1], - false); - if (object_filenames.empty()) { - this->log.info("[Map/%zu:o] No file to load", floor); - } else { - bool any_map_loaded = false; - for (const string& filename : object_filenames) { - auto map_data = get_file_data(filename); - if (map_data) { - this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); - any_map_loaded = true; - break; - } - } - if (!any_map_loaded) { - throw runtime_error(string_printf("no object maps loaded for floor %zu", floor)); - } - } - } + } else { + this->map = make_shared(this->base_version, this->lobby_id, this->random_crypt); } this->log.info("Generated objects list (%zu entries):", this->map->objects.size()); diff --git a/src/Lobby.hh b/src/Lobby.hh index 2307cc0d..82d93b83 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -195,6 +195,26 @@ struct Lobby : public std::enable_shared_from_this { std::shared_ptr require_challenge_params() const; void set_drop_mode(DropMode new_mode); void create_item_creator(); + static std::shared_ptr load_maps( + Version version, + Episode episode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + std::shared_ptr rare_rates, + std::shared_ptr random_crypt, + std::shared_ptr vq); + static std::shared_ptr load_maps( + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + std::function(Version, const std::string&)> get_file_data, + std::shared_ptr rare_rates, + std::shared_ptr random_crypt, + const parray& variations); void load_maps(); void create_ep3_server(); diff --git a/src/Main.cc b/src/Main.cc index 93da7a75..3bcf8f00 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -125,6 +125,44 @@ Version get_cli_version(Arguments& args, Version default_value = Version::UNKNOW } } +Episode get_cli_episode(Arguments& args) { + if (args.get("ep1")) { + return Episode::EP1; + } else if (args.get("ep2")) { + return Episode::EP2; + } else if (args.get("ep3")) { + return Episode::EP3; + } else if (args.get("ep4")) { + return Episode::EP4; + } else { + throw runtime_error("an episode option is required"); + } +} + +GameMode get_cli_game_mode(Arguments& args) { + if (args.get("battle")) { + return GameMode::BATTLE; + } else if (args.get("challenge")) { + return GameMode::CHALLENGE; + } else if (args.get("solo")) { + return GameMode::SOLO; + } else { + return GameMode::NORMAL; + } +} + +uint8_t get_cli_difficulty(Arguments& args) { + if (args.get("hard")) { + return 1; + } else if (args.get("very-hard")) { + return 2; + } else if (args.get("ultimate")) { + return 3; + } else { + return 0; + } +} + string read_input_data(Arguments& args) { const string& input_filename = args.get(1, false); @@ -1783,6 +1821,103 @@ Action a_show_battle_params( s.battle_params->get_table(true, Episode::EP4).print(stdout); }); +Action a_find_rare_enemy_seeds( + "find-rare-enemy-seeds", "\ + find-rare-enemy-seeds OPTIONS...\n\ + Search all possible rare seeds to find those that produce one or more rare\n\ + enemies in any set of variations. A version option (e.g. --gc) is required;\n\ + an episode option (--ep1, --ep2, or --ep4) is also required. A difficulty\n\ + option (--normal, --hard, --very-hard, or --ultimate) may be given; this\n\ + affects which rare rates from config.json are used if --bb was given.\n\ + Similarly, --battle, --challenge, or --solo may also be given; this affects\n\ + which variations are used on all versions and which rare rates to use for\n\ + BB. --threads=COUNT controls the number of threads to use for the search\n\ + by default, one thread per CPU core is used. --min-count specifies how many\n\ + rare enemies must be found to output the seed. Finally, --quest=NAME may be\n\ + given to use that quest\'s map instead of the free-roam maps.\n", + +[](Arguments& args) { + auto version = get_cli_version(args); + auto episode = get_cli_episode(args); + auto difficulty = get_cli_difficulty(args); + auto mode = get_cli_game_mode(args); + size_t num_threads = args.get("threads", 0); + size_t min_count = args.get("min-count", 1); + string quest_name = args.get("quest", false); + + ServerState s("system/config.json"); + shared_ptr vq; + if (!quest_name.empty()) { + s.load_objects_and_upstream_dependents("quest_index"); + auto q = s.quest_index(version)->get(quest_name); + if (!q) { + throw runtime_error("quest does not exist"); + } + vq = q->version(version, 1); + if (!vq) { + throw runtime_error("quest version does not exist"); + } + } else if (version == Version::BB_V4) { + s.load_objects_and_upstream_dependents("config"); + } else if (version == Version::PC_V2) { + s.load_objects_and_upstream_dependents("patch_indexes"); + } else { + s.load_objects_and_upstream_dependents("map_file_caches"); + } + + shared_ptr rare_rates; + if (version != Version::BB_V4) { + rare_rates = Map::DEFAULT_RARE_ENEMIES; + } else if (mode == GameMode::CHALLENGE) { + rare_rates = s.rare_enemy_rates_challenge; + } else { + rare_rates = s.rare_enemy_rates_by_difficulty[difficulty]; + } + + mutex output_lock; + auto thread_fn = [&](uint64_t seed, size_t) -> bool { + auto random_crypt = make_shared(seed); + parray variations; + + shared_ptr map; + if (vq) { + map = Lobby::load_maps(version, episode, difficulty, 0, 0, rare_rates, random_crypt, vq); + + } else { + generate_variations(variations, random_crypt, version, episode, (mode == GameMode::SOLO)); + map = Lobby::load_maps( + version, + episode, + mode, + difficulty, + 0, + 0, + bind(&ServerState::load_map_file, &s, placeholders::_1, placeholders::_2), + rare_rates, + random_crypt, + variations); + } + + vector rare_indexes; + for (size_t z = 0; z < map->enemies.size(); z++) { + if (enemy_type_is_rare(map->enemies[z].type)) { + rare_indexes.emplace_back(z); + } + } + + if (rare_indexes.size() >= min_count) { + fprintf(stdout, "%08" PRIX64 ":", seed); + for (size_t index : rare_indexes) { + fprintf(stdout, " E-%zX:%s", index, name_for_enum(map->enemies[index].type)); + } + fprintf(stdout, "\n"); + } + + return false; + }; + + parallel_range(thread_fn, 0, 0x100000000, num_threads); + }); + Action a_parse_object_graph( "parse-object-graph", nullptr, +[](Arguments& args) { uint32_t root_object_address = args.get("root", Arguments::IntFormat::HEX); @@ -2117,6 +2252,22 @@ Most options that take data as input also accept the following option:\n\ --parse-data\n\ For modes that take input (from a file or from stdin), parse the input as\n\ a hex string before encrypting/decoding/etc.\n\ +\n\ +Many versions also accept or require a version option. The version options are:\n\ + --pc-patch: PC patch server\n\ + --bb-patch: BB patch server\n\ + --dc-nte: DC Network Trial Edition\n\ + --dc-proto or --dc-11-2000: DC 11/2000 prototype\n\ + --dc-v1: DC v1\n\ + --dc-v2 or --dc: DC v2\n\ + --pc-nte: PC Network Trial Edition\n\ + --pc: PC v2\n\ + --gc-nte: GC Episodes 1&2 Trial Edition\n\ + --gc: GC Episodes 1&2\n\ + --xb: Xbox Episodes 1&2\n\ + --ep3-trial: GC Episode 3 Trial Edition\n\ + --ep3: GC Episode 3\n\ + --bb: Blue Burst\n\ \n", stderr); } diff --git a/src/Map.cc b/src/Map.cc index 5f4ee6e0..bc2c38f0 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -914,10 +914,15 @@ void Map::add_enemy( break; } case 0x0064: // TObjEneSlime - add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam1 & 1), rare_rates->pouilly_slime) - ? EnemyType::POUILLY_SLIME - : EnemyType::POFUILLY_SLIME); - default_num_children = 4; + if ((e.num_children != 0) && (e.num_children != 4)) { + this->log.warning("POFUILLY_SLIME has an unusual num_children (0x%hX)", e.num_children.load()); + } + default_num_children = -1; // Skip adding children (because we do it here) + for (size_t z = 0; z < 5; z++) { + add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam2 & 1), rare_rates->pouilly_slime) + ? EnemyType::POUILLY_SLIME + : EnemyType::POFUILLY_SLIME); + } break; case 0x0065: // TObjEnePanarms if ((e.num_children != 0) && (e.num_children != 2)) { @@ -1489,7 +1494,6 @@ void Map::add_enemies_and_objects_from_quest_data( uint8_t event, const void* data, size_t size, - uint32_t rare_seed, std::shared_ptr rare_rates) { auto all_floor_sections = this->collect_quest_map_data_sections(data, size); @@ -1503,7 +1507,6 @@ void Map::add_enemies_and_objects_from_quest_data( if (header.data_size % sizeof(ObjectEntry)) { throw runtime_error("quest layout object section size is not a multiple of object entry size"); } - this->log.info("(Floor %02zX) Adding objects", floor); this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size); } @@ -1512,7 +1515,6 @@ void Map::add_enemies_and_objects_from_quest_data( if (header.data_size % sizeof(EnemyEntry)) { throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); } - this->log.info("(Floor %02zX) Adding enemies", floor); this->add_enemies_from_map_data( episode, difficulty, @@ -1525,12 +1527,11 @@ void Map::add_enemies_and_objects_from_quest_data( } else if ((floor_sections.wave_events != 0xFFFFFFFF) && (floor_sections.random_enemy_locations != 0xFFFFFFFF) && (floor_sections.random_enemy_definitions != 0xFFFFFFFF)) { - this->log.info("(Floor %02zX) Adding random enemies", floor); const auto& wave_events_header = r.pget(floor_sections.wave_events); const auto& random_enemy_locations_header = r.pget(floor_sections.random_enemy_locations); const auto& random_enemy_definitions_header = r.pget(floor_sections.random_enemy_definitions); if (!random_state) { - random_state = make_shared(rare_seed); + random_state = make_shared(this->random_crypt->seed()); } this->add_random_enemies_from_map_data( episode, @@ -1907,12 +1908,49 @@ void generate_variations( variations[z * 2 + 0] = 0; variations[z * 2 + 1] = 0; } else { - variations[z * 2 + 0] = (a.variation1_values.size() < 2) ? 0 : (random_crypt->next() % a.variation1_values.size()); - variations[z * 2 + 1] = (a.variation2_values.size() < 2) ? 0 : (random_crypt->next() % a.variation2_values.size()); + variations[z * 2 + 0] = (a.variation1_values.size() <= 1) ? 0 : (random_crypt->next() % a.variation1_values.size()); + variations[z * 2 + 1] = (a.variation2_values.size() <= 1) ? 0 : (random_crypt->next() % a.variation2_values.size()); } } } +vector> generate_all_possible_variations(Version version, Episode episode, bool is_solo) { + parray maxes; + for (size_t z = 0; z < 0x10; z++) { + const auto& a = file_info_for_variation(version, episode, z, is_solo); + if (!a.name_token) { + maxes[z * 2 + 0] = 0; + maxes[z * 2 + 1] = 0; + } else { + maxes[z * 2 + 0] = (a.variation1_values.size() <= 1) ? 0 : (a.variation1_values.size() - 1); + maxes[z * 2 + 1] = (a.variation2_values.size() <= 1) ? 0 : (a.variation2_values.size() - 1); + } + } + + vector> ret; + parray current; + for (;;) { + ret.emplace_back(current); + + // Increment current by 1 as if it were an 0x20-place integer, with each + // "place" having a base of maxes[x] + 1 + ssize_t x; + for (x = 0x1F; x >= 0; x--) { + if (current[x] < maxes[x]) { + current[x]++; + break; + } else { + current[x] = 0; + } + } + if (x < 0) { + break; + } + } + + return ret; +} + vector map_filenames_for_variation( Version version, Episode episode, diff --git a/src/Map.hh b/src/Map.hh index 28b44782..18db5475 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -306,7 +306,6 @@ struct Map { uint8_t event, const void* data, size_t size, - uint32_t rare_seed, std::shared_ptr rare_rates = Map::DEFAULT_RARE_ENEMIES); const Enemy& find_enemy(uint8_t floor, EnemyType type) const; @@ -357,5 +356,8 @@ void generate_variations( Version version, Episode episode, bool is_solo); +std::vector> generate_all_possible_variations( + Version version, Episode episode, bool is_solo); + std::vector map_filenames_for_variation( Version version, Episode episode, GameMode mode, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies); diff --git a/src/Quest.cc b/src/Quest.cc index 53d2bc77..fa97f3aa 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -725,6 +725,7 @@ QuestIndex::QuestIndex( } else { auto q = make_shared(vq); this->quests_by_number.emplace(vq->quest_number, q); + this->quests_by_name.emplace(vq->name, q); this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q); static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)", filenames_str.c_str(), @@ -751,6 +752,14 @@ shared_ptr QuestIndex::get(uint32_t quest_number) const { } } +shared_ptr QuestIndex::get(const std::string& name) const { + try { + return this->quests_by_name.at(name); + } catch (const out_of_range&) { + return nullptr; + } +} + vector> QuestIndex::categories( QuestMenuType menu_type, Episode episode, diff --git a/src/Quest.hh b/src/Quest.hh index da5931c1..864a35d0 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -135,11 +135,13 @@ struct QuestIndex { std::shared_ptr category_index; std::map> quests_by_number; + std::map> quests_by_name; std::map>> quests_by_category_id_and_number; QuestIndex(const std::string& directory, std::shared_ptr category_index, bool is_ep3); std::shared_ptr get(uint32_t quest_number) const; + std::shared_ptr get(const std::string& name) const; std::vector> categories( QuestMenuType menu_type, diff --git a/src/ServerState.cc b/src/ServerState.cc index 00db6665..16339f34 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -30,7 +30,9 @@ ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr< } } -ServerState::ServerState() : creation_time(now()) { +ServerState::ServerState(const string& config_filename) + : creation_time(now()), + config_filename(config_filename) { this->create_load_step_graph(); } @@ -497,6 +499,39 @@ shared_ptr ServerState::load_bb_file( } } +shared_ptr ServerState::load_map_file(Version version, const string& filename) const { + auto& cache = this->map_file_caches.at(static_cast(version)); + return cache->get(filename, bind(&ServerState::load_map_file_uncached, this, version, placeholders::_1)); +} + +shared_ptr ServerState::load_map_file_uncached(Version version, const string& filename) const { + if (version == Version::BB_V4) { + try { + return this->load_bb_file(filename); + } catch (const exception& e) { + static_game_data_log.info("Failed to load %s from BB patch tree: %s", filename.c_str(), e.what()); + } + } else if (version == Version::PC_V2) { + try { + string path = "system/patch-pc/Media/PSO/" + filename; + auto ret = make_shared(load_file(path)); + static_game_data_log.info("Loaded %s from PC patch tree", filename.c_str()); + return ret; + } catch (const exception& e) { + static_game_data_log.info("Failed to load %s from PC patch tree: %s", filename.c_str(), e.what()); + } + } + try { + string path = string_printf("system/maps/%s/%s", file_path_token_for_version(version), filename.c_str()); + auto ret = make_shared(load_file(path)); + static_game_data_log.info("Loaded %s from default maps", filename.c_str()); + return ret; + } catch (const exception& e) { + static_game_data_log.info("Failed to load %s from default maps: %s", filename.c_str(), e.what()); + } + return nullptr; +} + pair ServerState::parse_port_spec(const JSON& json) const { if (json.is_list()) { string addr = json.at(0).as_string(); @@ -1087,6 +1122,13 @@ void ServerState::load_patch_indexes() { } } +void ServerState::clear_map_file_caches() { + config_log.info("Clearing map file caches"); + for (auto& cache : this->map_file_caches) { + cache = make_shared(); + } +} + void ServerState::load_battle_params() { config_log.info("Loading battle parameters"); this->battle_params = make_shared( @@ -1439,9 +1481,13 @@ void ServerState::create_load_step_graph() { // Out: license_index this->load_step_graph.add_step("licenses", {"all"}, bind(&ServerState::load_licenses, this)); + // In: none + // Out: map_file_caches + this->load_step_graph.add_step("map_file_caches", {"all"}, bind(&ServerState::clear_map_file_caches, this)); + // In: none // Out: pc_patch_file_index, bb_patch_file_index, bb_data_gsl - this->load_step_graph.add_step("patch_indexes", {"all"}, bind(&ServerState::load_patch_indexes, this)); + this->load_step_graph.add_step("patch_indexes", {"all", "map_file_caches"}, bind(&ServerState::load_patch_indexes, this)); // In: none // Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index diff --git a/src/ServerState.hh b/src/ServerState.hh index acf972a5..de27ebf1 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -121,6 +121,7 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr function_code_index; std::shared_ptr pc_patch_file_index; std::shared_ptr bb_patch_file_index; + std::array, NUM_VERSIONS> map_file_caches; std::shared_ptr dol_file_index; std::shared_ptr ep3_card_index; std::shared_ptr ep3_card_index_trial; @@ -230,7 +231,7 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr proxy_server; std::shared_ptr game_server; - ServerState(); + explicit ServerState(const std::string& config_filename = ""); ServerState(std::shared_ptr base, const std::string& config_filename, bool is_replay); ServerState(const ServerState&) = delete; ServerState(ServerState&&) = delete; @@ -290,6 +291,8 @@ struct ServerState : public std::enable_shared_from_this { const std::string& patch_index_filename, const std::string& gsl_filename = "", const std::string& bb_directory_filename = "") const; + std::shared_ptr load_map_file(Version version, const std::string& filename) const; + std::shared_ptr load_map_file_uncached(Version version, const std::string& filename) const; std::pair parse_port_spec(const JSON& json) const; std::vector parse_port_configuration(const JSON& json) const; @@ -302,6 +305,7 @@ struct ServerState : public std::enable_shared_from_this { void load_licenses(); void load_teams(); void load_patch_indexes(); + void clear_map_file_caches(); void load_battle_params(); void load_level_table(); void load_text_index(); diff --git a/src/Text.hh b/src/Text.hh index 12f16156..34d69420 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -82,7 +82,9 @@ struct parray { parray(const parray& other) { this->operator=(other); } - parray(parray&& s) = delete; + parray(parray&& other) { + this->operator=(std::move(other)); + } template parray(const parray& s) { @@ -168,7 +170,12 @@ struct parray { } return *this; } - parray& operator=(parray&& s) = delete; + parray& operator=(parray&& s) { + for (size_t x = 0; x < Count; x++) { + this->items[x] = s.items[x]; + } + return *this; + } template parray& operator=(const parray& s) {