diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d94e6f6..d2f94e8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,10 +123,10 @@ set(SOURCES src/ServerState.cc src/Shell.cc src/StaticGameData.cc + src/StepGraph.cc src/TeamIndex.cc src/Text.cc - src/TextArchive.cc - src/UnicodeTextSet.cc + src/TextIndex.cc src/Version.cc src/WordSelectTable.cc ) diff --git a/src/Main.cc b/src/Main.cc index 7a519477..fbfe1e47 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1493,24 +1493,20 @@ Action a_show_ep3_cards( +[](Arguments& args) { bool one_line = args.get("one-line"); - Episode3::CardIndex card_index( - "system/ep3/card-definitions.mnr", - "system/ep3/card-definitions.mnrd", - "system/ep3/card-text.mnr", - "system/ep3/card-text.mnrd", - "system/ep3/card-dice-text.mnr", - "system/ep3/card-dice-text.mnrd"); - unique_ptr text_english; + ServerState s; + s.load_objects("ep3_data"); + + unique_ptr text_english; try { JSON json = JSON::parse(load_file("system/ep3/text-english.json")); - text_english = make_unique(json); + text_english = make_unique(json); } catch (const exception& e) { } - auto card_ids = card_index.all_ids(); + auto card_ids = s.ep3_card_index->all_ids(); log_info("%zu card definitions", card_ids.size()); for (uint32_t card_id : card_ids) { - auto entry = card_index.definition_for_id(card_id); + auto entry = s.ep3_card_index->definition_for_id(card_id); string s = entry->def.str(one_line, text_english.get()); if (one_line) { fprintf(stdout, "%s\n", s.c_str()); @@ -1544,18 +1540,14 @@ Action a_generate_ep3_cards_html( +[](Arguments& args) { size_t num_threads = args.get("threads", 0); - Episode3::CardIndex card_index( - "system/ep3/card-definitions.mnr", - "system/ep3/card-definitions.mnrd", - "system/ep3/card-text.mnr", - "system/ep3/card-text.mnrd", - "system/ep3/card-dice-text.mnr", - "system/ep3/card-dice-text.mnrd"); - unique_ptr text_english; + ServerState s; + s.load_objects("ep3_data"); + s.load_objects("text_index"); + + shared_ptr text_english; try { - JSON json = JSON::parse(load_file("system/ep3/text-english.json")); - text_english = make_unique(json); - } catch (const exception& e) { + text_english = s.text_index->get(Version::GC_EP3, 1); + } catch (const out_of_range&) { } struct CardInfo { @@ -1572,11 +1564,11 @@ Action a_generate_ep3_cards_html( } }; vector infos; - for (uint32_t card_id : card_index.all_ids()) { + for (uint32_t card_id : s.ep3_card_index->all_ids()) { if (infos.size() <= card_id) { infos.resize(card_id + 1); } - infos[card_id].ce = card_index.definition_for_id(card_id); + infos[card_id].ce = s.ep3_card_index->definition_for_id(card_id); } for (const auto& filename : list_directory_sorted("system/ep3/cardtex")) { if ((filename[0] == 'C' || filename[0] == 'M' || filename[0] == 'L') && (filename[1] == '_')) { @@ -1684,20 +1676,21 @@ Action a_show_ep3_maps( human-readable format.\n", +[](Arguments&) { config_log.info("Collecting Episode 3 data"); - Episode3::MapIndex map_index("system/ep3/maps"); - Episode3::CardIndex card_index("system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd"); - auto map_ids = map_index.all_numbers(); + ServerState s; + s.load_objects("ep3_data"); + + auto map_ids = s.ep3_map_index->all_numbers(); log_info("%zu maps", map_ids.size()); for (uint32_t map_id : map_ids) { - auto map = map_index.for_number(map_id); + auto map = s.ep3_map_index->for_number(map_id); const auto& vms = map->all_versions(); for (size_t language = 0; language < vms.size(); language++) { if (!vms[language]) { continue; } - string s = vms[language]->map->str(&card_index, language); - fprintf(stdout, "(%c) %s\n", char_for_language_code(language), s.c_str()); + string map_s = vms[language]->map->str(s.ep3_card_index.get(), language); + fprintf(stdout, "(%c) %s\n", char_for_language_code(language), map_s.c_str()); } } }); @@ -1708,26 +1701,21 @@ Action a_show_battle_params( Print the Blue Burst battle parameters from the system/blueburst directory\n\ in a human-readable format.\n", +[](Arguments&) { - BattleParamsIndex index( - make_shared(load_file("system/blueburst/BattleParamEntry_on.dat")), - make_shared(load_file("system/blueburst/BattleParamEntry_lab_on.dat")), - make_shared(load_file("system/blueburst/BattleParamEntry_ep4_on.dat")), - make_shared(load_file("system/blueburst/BattleParamEntry.dat")), - make_shared(load_file("system/blueburst/BattleParamEntry_lab.dat")), - make_shared(load_file("system/blueburst/BattleParamEntry_ep4.dat"))); + ServerState s; + s.load_objects("battle_params"); fprintf(stdout, "Episode 1 multi\n"); - index.get_table(false, Episode::EP1).print(stdout); + s.battle_params->get_table(false, Episode::EP1).print(stdout); fprintf(stdout, "Episode 1 solo\n"); - index.get_table(true, Episode::EP1).print(stdout); + s.battle_params->get_table(true, Episode::EP1).print(stdout); fprintf(stdout, "Episode 2 multi\n"); - index.get_table(false, Episode::EP2).print(stdout); + s.battle_params->get_table(false, Episode::EP2).print(stdout); fprintf(stdout, "Episode 2 solo\n"); - index.get_table(true, Episode::EP2).print(stdout); + s.battle_params->get_table(true, Episode::EP2).print(stdout); fprintf(stdout, "Episode 4 multi\n"); - index.get_table(false, Episode::EP4).print(stdout); + s.battle_params->get_table(false, Episode::EP4).print(stdout); fprintf(stdout, "Episode 4 solo\n"); - index.get_table(true, Episode::EP4).print(stdout); + s.battle_params->get_table(true, Episode::EP4).print(stdout); }); Action a_parse_object_graph( @@ -1848,12 +1836,13 @@ Action a_diff_dol_files( Action a_replay_ep3_battle_commands( "replay-ep3-battle-commands", nullptr, +[](Arguments& args) { - auto card_index = make_shared("system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd"); - auto map_index = make_shared("system/ep3/maps"); + ServerState s; + s.load_objects("ep3_data"); + auto random_crypt = make_shared(args.get("seed", 0, Arguments::IntFormat::HEX)); Episode3::Server::Options options = { - .card_index = card_index, - .map_index = map_index, + .card_index = s.ep3_card_index, + .map_index = s.ep3_map_index, .behavior_flags = 0x0092, .random_crypt = random_crypt, .tournament = nullptr, @@ -1897,7 +1886,7 @@ Action a_run_server_replay_log( shared_ptr base(event_base_new(), event_base_free); auto state = make_shared(base, config_filename, is_replay); - state->init(); + state->load_objects("all"); shared_ptr dns_server; if (state->dns_server_port && !is_replay) { diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 56536750..75f4d147 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -120,17 +120,24 @@ General commands:\n\ \n\ Server commands:\n\ reload ITEM [ITEM...]\n\ - Reload various parts of the server configuration. ITEMs can be:\n\ - licenses - reload the license index file\n\ - patches - reindex the PC and BB patch directories\n\ - battle-params - reload the enemy stats files\n\ - level-table - reload the level-up tables\n\ - item-tables - reload the item generation tables\n\ - ep3 - reload Episode 3 card definitions and maps (not download quests)\n\ - quests - reindex all quests (including Episode 3 download quests)\n\ - functions - recompile all client-side functions\n\ - dol-files - reindex all DOL files\n\ + Reload various parts of the server configuration. When you reload any item,\n\ + any other item that depends on it will be reloaded as well. The items are:\n\ + all - reindex/reload everything\n\ + battle-params - reload the BB enemy stats files\n\ + bb-private-keys - reload BB private keys\n\ config - reload most fields from config.json\n\ + dol-files - reindex all DOL files\n\ + drop-tables - reload drop tables\n\ + ep3-data - reload Episode 3 cards and maps (not download quests)\n\ + functions - recompile all client-side patches and functions\n\ + item-definitions - reload item definitions files\n\ + level-table - reload the level-up tables\n\ + licenses - reindex user licenses\n\ + patch-indexes - reindex the PC and BB patch directories\n\ + quest-index - reindex all quests (including Episode3 download quests)\n\ + teams - reindex all BB teams\n\ + text-index - reload in-game text\n\ + word-select-table - regenerate the Word Select translation table\n\ Reloading will not affect items that are in use; for example, if an Episode\n\ 3 battle is in progress, it will continue to use the previous map and card\n\ definitions. Similarly, BB clients are not forced to disconnect or reload\n\ @@ -285,41 +292,14 @@ Proxy session commands:\n\ if (types.empty()) { throw invalid_argument("no data type given"); } - for (const string& type : types) { - if (type == "licenses") { - this->state->load_licenses(); - } else if (type == "teams") { - this->state->load_teams(); - } else if (type == "patches") { - this->state->load_patch_indexes(); - } else if (type == "battle-params") { - this->state->load_battle_params(); - } else if (type == "level-table") { - this->state->load_level_table(); - } else if (type == "item-tables") { - this->state->load_item_name_index(); - this->state->load_item_tables(); - } else if (type == "word-select") { - this->state->load_word_select_table(); - } else if (type == "ep3") { - this->state->load_ep3_data(); - } else if (type == "quests") { - this->state->load_quest_index(); - } else if (type == "functions") { - auto config_json = this->state->load_config(); - this->state->compile_functions(); - } else if (type == "dol-files") { - auto config_json = this->state->load_config(); - this->state->load_dol_files(); - } else if (type == "config") { - auto config_json = this->state->load_config(); - this->state->parse_config(config_json, true); - this->state->resolve_ep3_card_names(); - this->state->load_teams(); - } else { - throw invalid_argument("incorrect data type"); + for (auto& type : types) { + for (char& ch : type) { + if (ch == '-') { + ch = '_'; + } } } + this->state->load_objects(types); } else if (command_name == "add-license") { auto l = this->state->license_index->create_license(); diff --git a/src/ServerState.cc b/src/ServerState.cc index c5b9d200..84588654 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -25,137 +25,31 @@ ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr< this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0); for (size_t day = 0; day < 7; day++) { for (const auto& item_it : json.get_list(day_names[day])) { - this->results[day].emplace_back(name_index->parse_item_description(Version::BB_V4, item_it->as_string())); + this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string())); } } } +ServerState::ServerState() : creation_time(now()) { + this->create_load_step_graph(); +} + ServerState::ServerState(shared_ptr base, const string& config_filename, bool is_replay) : creation_time(now()), base(base), config_filename(config_filename), is_replay(is_replay), - dns_server_port(0), - ip_stack_debug(false), - allow_unregistered_users(false), - allow_pc_nte(false), - use_temp_licenses_for_prototypes(true), - allow_dc_pc_games(false), - allow_gc_xb_games(true), - allowed_drop_modes_v1_v2_normal(0x1F), - allowed_drop_modes_v1_v2_battle(0x07), - allowed_drop_modes_v1_v2_challenge(0x07), - allowed_drop_modes_v3_normal(0x1F), - allowed_drop_modes_v3_battle(0x07), - allowed_drop_modes_v3_challenge(0x07), - allowed_drop_modes_v4_normal(0x1D), // CLIENT not allowed - allowed_drop_modes_v4_battle(0x05), - allowed_drop_modes_v4_challenge(0x05), - default_drop_mode_v1_v2_normal(Lobby::DropMode::CLIENT), - default_drop_mode_v1_v2_battle(Lobby::DropMode::CLIENT), - default_drop_mode_v1_v2_challenge(Lobby::DropMode::CLIENT), - default_drop_mode_v3_normal(Lobby::DropMode::CLIENT), - default_drop_mode_v3_battle(Lobby::DropMode::CLIENT), - default_drop_mode_v3_challenge(Lobby::DropMode::CLIENT), - default_drop_mode_v4_normal(Lobby::DropMode::SERVER_SHARED), - default_drop_mode_v4_battle(Lobby::DropMode::SERVER_SHARED), - default_drop_mode_v4_challenge(Lobby::DropMode::SERVER_SHARED), - persistent_game_idle_timeout_usecs(0), - ep3_send_function_call_enabled(false), - catch_handler_exceptions(true), - ep3_infinite_meseta(false), - ep3_defeat_player_meseta_rewards({400, 500, 600, 700, 800}), - ep3_defeat_com_meseta_rewards({100, 200, 300, 400, 500}), - ep3_final_round_meseta_bonus(300), - ep3_jukebox_is_free(false), - ep3_behavior_flags(0), - hide_download_commands(true), - run_shell_behavior(RunShellBehavior::DEFAULT), - cheat_mode_behavior(BehaviorSwitch::OFF_BY_DEFAULT), - bb_global_exp_multiplier(1), - ep3_card_auction_points(0), - ep3_card_auction_min_size(0), - ep3_card_auction_max_size(0), - player_files_manager(make_shared(base)), - destroy_lobbies_event(event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this), event_free), - next_lobby_id(1), - pre_lobby_event(0), - ep3_menu_song(-1), - local_address(0), - external_address(0), - proxy_allow_save_files(true), - proxy_enable_login_options(false) {} + player_files_manager(this->base ? make_shared(base) : nullptr), + destroy_lobbies_event(this->base ? event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this) : nullptr, event_free) { + this->create_load_step_graph(); +} -void ServerState::init() { - vector> non_v1_only_lobbies; - vector> ep3_only_lobbies; +void ServerState::load_objects(const std::string& what) { + this->load_step_graph.run(what); +} - for (size_t x = 0; x < 20; x++) { - auto lobby_name = string_printf("LOBBY%zu", x + 1); - bool allow_v1 = (x <= 9); - bool allow_non_ep3 = (x <= 14); - - shared_ptr l = this->create_lobby(false); - l->set_flag(Lobby::Flag::PUBLIC); - l->set_flag(Lobby::Flag::DEFAULT); - l->set_flag(Lobby::Flag::PERSISTENT); - if (allow_non_ep3) { - if (allow_v1) { - l->allow_version(Version::DC_NTE); - l->allow_version(Version::DC_V1_11_2000_PROTOTYPE); - l->allow_version(Version::DC_V1); - } - l->allow_version(Version::DC_V2); - l->allow_version(Version::PC_NTE); - l->allow_version(Version::PC_V2); - l->allow_version(Version::GC_NTE); - l->allow_version(Version::GC_V3); - l->allow_version(Version::XB_V3); - l->allow_version(Version::BB_V4); - } - l->allow_version(Version::GC_EP3_NTE); - l->allow_version(Version::GC_EP3); - - l->block = x + 1; - l->name = lobby_name; - l->max_clients = 12; - if (!allow_non_ep3) { - l->episode = Episode::EP3; - } - - if (allow_non_ep3) { - this->public_lobby_search_order.emplace_back(l); - } else { - ep3_only_lobbies.emplace_back(l); - } - } - - // Annoyingly, the CARD lobbies should be searched first, but are sent at the - // end of the lobby list command, so we have to change the search order - // manually here. - this->public_lobby_search_order.insert( - this->public_lobby_search_order.begin(), - ep3_only_lobbies.begin(), - ep3_only_lobbies.end()); - - // Load all the necessary data - auto config = this->load_config(); - this->collect_network_addresses(); - this->load_item_name_index(); - this->parse_config(config, false); - this->load_bb_private_keys(); - this->load_licenses(); - this->load_teams(); - this->load_patch_indexes(); - this->load_battle_params(); - this->load_level_table(); - this->load_item_tables(); - this->load_word_select_table(); - this->load_ep3_data(); - this->resolve_ep3_card_names(); - this->load_quest_index(); - this->compile_functions(); - this->load_dol_files(); +void ServerState::load_objects(const std::vector& what) { + this->load_step_graph.run(what); } void ServerState::add_client_to_available_lobby(shared_ptr c) { @@ -578,20 +472,6 @@ shared_ptr ServerState::load_bb_file( } } -void ServerState::collect_network_addresses() { - config_log.info("Reading network addresses"); - this->all_addresses = get_local_addresses(); - for (const auto& it : this->all_addresses) { - string addr_str = string_for_address(it.second); - config_log.info("Found interface: %s = %s", it.first.c_str(), addr_str.c_str()); - } -} - -JSON ServerState::load_config() const { - config_log.info("Loading configuration"); - return JSON::parse(load_file(this->config_filename)); -} - static vector parse_port_configuration(const JSON& json) { vector ret; for (const auto& item_json_it : json.as_dict()) { @@ -605,8 +485,22 @@ static vector parse_port_configuration(const JSON& json) { return ret; } -void ServerState::parse_config(const JSON& json, bool is_reload) { - config_log.info("Parsing configuration"); +void ServerState::collect_network_addresses() { + config_log.info("Reading network addresses"); + this->all_addresses = get_local_addresses(); + for (const auto& it : this->all_addresses) { + string addr_str = string_for_address(it.second); + config_log.info("Found interface: %s = %s", it.first.c_str(), addr_str.c_str()); + } +} + +void ServerState::load_config() { + if (this->config_filename.empty()) { + throw logic_error("configuration filename is missing"); + } + + config_log.info("Loading configuration"); + auto json = JSON::parse(load_file(this->config_filename)); auto parse_behavior_switch = [&](const string& json_key, BehaviorSwitch default_value) -> ServerState::BehaviorSwitch { try { @@ -629,7 +523,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { this->name = json.at("ServerName").as_string(); - if (!is_reload) { + if (!this->config_loaded) { try { this->username = json.at("User").as_string(); if (this->username == "$SUDO_USER") { @@ -772,12 +666,17 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { try { for (const auto& it : json.get_dict("CardAuctionPool")) { + uint16_t card_id; + try { + card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id; + } catch (const out_of_range&) { + throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", it.first.c_str())); + } this->ep3_card_auction_pool.emplace_back( CardAuctionPoolEntry{ .probability = static_cast(it.second->at(0).as_int()), - .card_id = 0, - .min_price = static_cast(it.second->at(1).as_int()), - .card_name = it.first}); + .card_id = card_id, + .min_price = static_cast(it.second->at(1).as_int())}); } } catch (const out_of_range&) { } @@ -788,11 +687,18 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { if (ep3_trap_cards_json.size() != 5) { throw runtime_error("Episode3TrapCards must be a list of 5 lists"); } - this->ep3_trap_card_names.clear(); - for (const auto& trap_type_it : ep3_trap_cards_json) { - auto& names = this->ep3_trap_card_names.emplace_back(); - for (const auto& card_it : trap_type_it->as_list()) { - names.emplace_back(card_it->as_string()); + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + auto& trap_card_ids = this->ep3_trap_card_ids[trap_type]; + for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) { + try { + const auto& card = this->ep3_card_index->definition_for_name_normalized(card_it->as_string()); + if (card->def.type != Episode3::CardType::ASSIST) { + throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str())); + } + trap_card_ids.emplace_back(card->def.card_id); + } catch (const out_of_range&) { + throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str())); + } } } } @@ -800,6 +706,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { } if (!this->is_replay) { + this->ep3_lobby_banners.clear(); for (const auto& it : json.get("Episode3LobbyBanners", JSON::list()).as_list()) { Image img("system/ep3/banners/" + it->at(2).as_string()); string gvm = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565); @@ -889,13 +796,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { set_log_levels_from_json(json.get("LogLevels", JSON::dict())); - if (!is_reload) { - try { - this->run_shell_behavior = json.at("RunInteractiveShell").as_bool() - ? ServerState::RunShellBehavior::ALWAYS - : ServerState::RunShellBehavior::NEVER; - } catch (const out_of_range&) { - } + try { + this->run_shell_behavior = json.at("RunInteractiveShell").as_bool() + ? ServerState::RunShellBehavior::ALWAYS + : ServerState::RunShellBehavior::NEVER; + } catch (const out_of_range&) { } this->allow_dc_pc_games = json.get_bool("AllowDCPCGames", this->allow_dc_pc_games); @@ -913,13 +818,11 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { this->ep3_menu_song = json.get_int("Episode3MenuSong", this->ep3_menu_song); - if (!is_reload) { - try { - this->quest_category_index = make_shared(json.at("QuestCategories")); - } catch (const exception& e) { - throw runtime_error(string_printf( - "QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what())); - } + try { + this->quest_category_index = make_shared(json.at("QuestCategories")); + } catch (const exception& e) { + throw runtime_error(string_printf( + "QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what())); } config_log.info("Creating menus"); @@ -1071,6 +974,8 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { } } catch (const out_of_range&) { } + + this->config_loaded = true; } void ServerState::load_bb_private_keys() { @@ -1093,7 +998,6 @@ void ServerState::load_licenses() { void ServerState::load_teams() { config_log.info("Indexing teams"); this->team_index = make_shared("system/teams", this->team_reward_defs_json); - this->team_reward_defs_json = nullptr; } void ServerState::load_patch_indexes() { @@ -1393,37 +1297,6 @@ void ServerState::load_ep3_data() { config_log.info("Loaded Episode 3 tournament state"); } -void ServerState::resolve_ep3_card_names() { - config_log.info("Resolving Episode 3 card names"); - for (auto& e : this->ep3_card_auction_pool) { - try { - const auto& card = this->ep3_card_index->definition_for_name_normalized(e.card_name); - e.card_id = card->def.card_id; - } catch (const out_of_range&) { - throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", e.card_name.c_str())); - } - } - - for (size_t z = 0; z < this->ep3_trap_card_ids.size(); z++) { - auto& ids = this->ep3_trap_card_ids[z]; - ids.clear(); - if (z < this->ep3_trap_card_names.size()) { - auto& names = this->ep3_trap_card_names[z]; - for (const auto& name : names) { - try { - const auto& card = this->ep3_card_index->definition_for_name_normalized(name); - if (card->def.type != Episode3::CardType::ASSIST) { - throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str())); - } - ids.emplace_back(card->def.card_id); - } catch (const out_of_range&) { - throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str())); - } - } - } - } -} - void ServerState::load_quest_index() { config_log.info("Collecting quests"); this->default_quest_index = make_shared("system/quests", this->quest_category_index, false); @@ -1441,14 +1314,137 @@ void ServerState::load_dol_files() { this->dol_file_index = make_shared("system/dol"); } -shared_ptr> ServerState::information_contents_for_client(shared_ptr c) const { - return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3; +void ServerState::create_default_lobbies() { + if (this->default_lobbies_created) { + return; + } + this->default_lobbies_created = true; + + vector> non_v1_only_lobbies; + vector> ep3_only_lobbies; + + for (size_t x = 0; x < 20; x++) { + auto lobby_name = string_printf("LOBBY%zu", x + 1); + bool allow_v1 = (x <= 9); + bool allow_non_ep3 = (x <= 14); + + shared_ptr l = this->create_lobby(false); + l->event = this->pre_lobby_event; + l->set_flag(Lobby::Flag::PUBLIC); + l->set_flag(Lobby::Flag::DEFAULT); + l->set_flag(Lobby::Flag::PERSISTENT); + if (allow_non_ep3) { + if (allow_v1) { + l->allow_version(Version::DC_NTE); + l->allow_version(Version::DC_V1_11_2000_PROTOTYPE); + l->allow_version(Version::DC_V1); + } + l->allow_version(Version::DC_V2); + l->allow_version(Version::PC_NTE); + l->allow_version(Version::PC_V2); + l->allow_version(Version::GC_NTE); + l->allow_version(Version::GC_V3); + l->allow_version(Version::XB_V3); + l->allow_version(Version::BB_V4); + } + l->allow_version(Version::GC_EP3_NTE); + l->allow_version(Version::GC_EP3); + + l->block = x + 1; + l->name = lobby_name; + l->max_clients = 12; + if (!allow_non_ep3) { + l->episode = Episode::EP3; + } + + if (allow_non_ep3) { + this->public_lobby_search_order.emplace_back(l); + } else { + ep3_only_lobbies.emplace_back(l); + } + } + + // Annoyingly, the CARD lobbies should be searched first, but are sent at the + // end of the lobby list command, so we have to change the search order + // manually here. + this->public_lobby_search_order.insert( + this->public_lobby_search_order.begin(), + ep3_only_lobbies.begin(), + ep3_only_lobbies.end()); } -shared_ptr ServerState::quest_index_for_version(Version version) const { - return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index; -} +void ServerState::create_load_step_graph() { + this->load_step_graph.add_step("all", {}, nullptr); -void ServerState::dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->lobbies_to_destroy.clear(); + // In: none + // Out: all_addresses + this->load_step_graph.add_step("network_addresses", {"all"}, bind(&ServerState::collect_network_addresses, this)); + + // In: none + // Out: bb_private_keys + this->load_step_graph.add_step("bb_private_keys", {"all"}, bind(&ServerState::load_bb_private_keys, this)); + + // In: none + // Out: license_index + this->load_step_graph.add_step("licenses", {"all"}, bind(&ServerState::load_licenses, 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)); + + // In: none + // Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index + this->load_step_graph.add_step("ep3_data", {"all"}, bind(&ServerState::load_ep3_data, this)); + + // In: none + // Out: function_code_index + this->load_step_graph.add_step("functions", {"all"}, bind(&ServerState::compile_functions, this)); + + // In: none + // Out: dol_file_index + this->load_step_graph.add_step("dol_files", {"all"}, bind(&ServerState::load_dol_files, this)); + + // In: none + // Out: lobbies + this->load_step_graph.add_step("lobbies", {"all"}, bind(&ServerState::create_default_lobbies, this)); + + // In: bb_patch_file_index + // Out: battle_params + this->load_step_graph.add_step("battle_params", {"all", "patch_indexes"}, bind(&ServerState::load_battle_params, this)); + + // In: bb_patch_file_index + // Out: level_table + this->load_step_graph.add_step("level_table", {"all", "patch_indexes"}, bind(&ServerState::load_level_table, this)); + + // In: bb_patch_file_index + // Out: text_index + this->load_step_graph.add_step("text_index", {"all", "patch_indexes"}, bind(&ServerState::load_text_index, this)); + + // In: text_index (optional) + // Out: word_select_table + this->load_step_graph.add_step("word_select_table", {"all"}, bind(&ServerState::load_word_select_table, this)); + + // In: none + // Out: item_parameter_tables, mag_evolution_table + this->load_step_graph.add_step("item_definitions", {"all"}, bind(&ServerState::load_item_definitions, this)); + + // In: text_index, item_parameter_tables + // Out: item_name_indexes + this->load_step_graph.add_step("item_name_indexes", {"all", "text_index", "item_definitions"}, bind(&ServerState::load_item_name_indexes, this)); + + // In: none + // Out: rare_item_sets, common_item_sets, armor_random_set, tool_random_set, weapon_random_sets, tekker_adjustment_set + this->load_step_graph.add_step("drop_tables", {"all", "item_definitions", "item_name_indexes"}, bind(&ServerState::load_drop_tables, this)); + + // In: all_addresses, ep3_card_index, item_name_indexes + // Out: config, ep3_lobby_banners, quest_category_index, information menus, proxy destinations menus, team_reward_defs_json + this->load_step_graph.add_step("config", {"all", "network_addresses", "ep3_data", "item_name_indexes"}, bind(&ServerState::load_config, this)); + + // In: team_reward_defs_json + // Out: team_index + this->load_step_graph.add_step("teams", {"all", "config"}, bind(&ServerState::load_teams, this)); + + // In: quest_category_index + // Out: default_quest_index, ep3_download_quest_index + this->load_step_graph.add_step("quest_index", {"all", "config"}, bind(&ServerState::load_quest_index, this)); } diff --git a/src/ServerState.hh b/src/ServerState.hh index 32a8b7b4..83f8e19e 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -25,6 +25,7 @@ #include "Menu.hh" #include "PlayerFilesManager.hh" #include "Quest.hh" +#include "StepGraph.hh" #include "TeamIndex.hh" #include "WordSelectTable.hh" @@ -63,52 +64,56 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr base; std::string config_filename; - bool is_replay; + bool is_replay = false; + bool config_loaded = false; + bool default_lobbies_created = false; + + StepGraph load_step_graph; std::string name; std::unordered_map> name_to_port_config; std::unordered_map> number_to_port_config; std::string username; - uint16_t dns_server_port; + uint16_t dns_server_port = 0; std::vector ip_stack_addresses; std::vector ppp_stack_addresses; - bool ip_stack_debug; - bool allow_unregistered_users; - bool allow_pc_nte; - bool use_temp_licenses_for_prototypes; - bool allow_dc_pc_games; - bool allow_gc_xb_games; - uint8_t allowed_drop_modes_v1_v2_normal; - uint8_t allowed_drop_modes_v1_v2_battle; - uint8_t allowed_drop_modes_v1_v2_challenge; - uint8_t allowed_drop_modes_v3_normal; - uint8_t allowed_drop_modes_v3_battle; - uint8_t allowed_drop_modes_v3_challenge; - uint8_t allowed_drop_modes_v4_normal; - uint8_t allowed_drop_modes_v4_battle; - uint8_t allowed_drop_modes_v4_challenge; - Lobby::DropMode default_drop_mode_v1_v2_normal; - Lobby::DropMode default_drop_mode_v1_v2_battle; - Lobby::DropMode default_drop_mode_v1_v2_challenge; - Lobby::DropMode default_drop_mode_v3_normal; - Lobby::DropMode default_drop_mode_v3_battle; - Lobby::DropMode default_drop_mode_v3_challenge; - Lobby::DropMode default_drop_mode_v4_normal; - Lobby::DropMode default_drop_mode_v4_battle; - Lobby::DropMode default_drop_mode_v4_challenge; + bool ip_stack_debug = false; + bool allow_unregistered_users = false; + bool allow_pc_nte = false; + bool use_temp_licenses_for_prototypes = true; + bool allow_dc_pc_games = true; + bool allow_gc_xb_games = true; + uint8_t allowed_drop_modes_v1_v2_normal = 0x1F; + uint8_t allowed_drop_modes_v1_v2_battle = 0x07; + uint8_t allowed_drop_modes_v1_v2_challenge = 0x07; + uint8_t allowed_drop_modes_v3_normal = 0x1F; + uint8_t allowed_drop_modes_v3_battle = 0x07; + uint8_t allowed_drop_modes_v3_challenge = 0x07; + uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed + uint8_t allowed_drop_modes_v4_battle = 0x05; + uint8_t allowed_drop_modes_v4_challenge = 0x05; + Lobby::DropMode default_drop_mode_v1_v2_normal = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v1_v2_battle = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v1_v2_challenge = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v3_normal = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v3_battle = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v3_challenge = Lobby::DropMode::CLIENT; + Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED; + Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED; + Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED; QuestFlagsForDifficulty quest_flag_persist_mask; - uint64_t persistent_game_idle_timeout_usecs; - bool ep3_send_function_call_enabled; - bool catch_handler_exceptions; - bool ep3_infinite_meseta; - std::vector ep3_defeat_player_meseta_rewards; - std::vector ep3_defeat_com_meseta_rewards; - uint32_t ep3_final_round_meseta_bonus; - bool ep3_jukebox_is_free; - uint32_t ep3_behavior_flags; - bool hide_download_commands; - RunShellBehavior run_shell_behavior; - BehaviorSwitch cheat_mode_behavior; + uint64_t persistent_game_idle_timeout_usecs = 0; + bool ep3_send_function_call_enabled = false; + bool catch_handler_exceptions = true; + bool ep3_infinite_meseta = false; + std::vector ep3_defeat_player_meseta_rewards = {400, 500, 600, 700, 800}; + std::vector ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; + uint32_t ep3_final_round_meseta_bonus = 300; + bool ep3_jukebox_is_free = false; + uint32_t ep3_behavior_flags = 0; + bool hide_download_commands = true; + RunShellBehavior run_shell_behavior = RunShellBehavior::DEFAULT; + BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT; std::vector> bb_private_keys; std::shared_ptr function_code_index; std::shared_ptr pc_patch_file_index; @@ -161,21 +166,19 @@ struct ServerState : public std::enable_shared_from_this { std::vector quest_F960_success_results; QuestF960Result quest_F960_failure_results; std::vector secret_lottery_results; - uint16_t bb_global_exp_multiplier; + uint16_t bb_global_exp_multiplier = 1; std::shared_ptr ep3_tournament_index; - uint16_t ep3_card_auction_points; - uint16_t ep3_card_auction_min_size; - uint16_t ep3_card_auction_max_size; + uint16_t ep3_card_auction_points = 0; + uint16_t ep3_card_auction_min_size = 0; + uint16_t ep3_card_auction_max_size = 0; struct CardAuctionPoolEntry { uint64_t probability; uint16_t card_id; uint16_t min_price; - std::string card_name; }; std::vector ep3_card_auction_pool; - std::vector> ep3_trap_card_names; std::array, 5> ep3_trap_card_ids; struct Ep3LobbyBannerEntry { uint32_t type = 1; @@ -212,26 +215,28 @@ struct ServerState : public std::enable_shared_from_this { std::unordered_set> lobbies_to_destroy; std::shared_ptr destroy_lobbies_event; std::vector> public_lobby_search_order; - std::atomic next_lobby_id; - uint8_t pre_lobby_event; - int32_t ep3_menu_song; + std::atomic next_lobby_id = 1; + uint8_t pre_lobby_event = 0; + int32_t ep3_menu_song = -1; std::map all_addresses; - uint32_t local_address; - uint32_t external_address; + uint32_t local_address = 0; + uint32_t external_address = 0; - bool proxy_allow_save_files; - bool proxy_enable_login_options; + bool proxy_allow_save_files = true; + bool proxy_enable_login_options = false; std::shared_ptr proxy_server; std::shared_ptr game_server; + ServerState(); ServerState(std::shared_ptr base, const std::string& config_filename, bool is_replay); ServerState(const ServerState&) = delete; ServerState(ServerState&&) = delete; ServerState& operator=(const ServerState&) = delete; ServerState& operator=(ServerState&&) = delete; - void init(); + void load_objects(const std::string& what); + void load_objects(const std::vector& what); void add_client_to_available_lobby(std::shared_ptr c); void remove_client_from_lobby(std::shared_ptr c); @@ -278,21 +283,24 @@ struct ServerState : public std::enable_shared_from_this { const std::string& gsl_filename = "", const std::string& bb_directory_filename = "") const; - JSON load_config() const; + void create_load_step_graph(); + void create_default_lobbies(); void collect_network_addresses(); - void parse_config(const JSON& config_json, bool is_reload); + void load_config(); void load_bb_private_keys(); void load_licenses(); void load_teams(); void load_patch_indexes(); void load_battle_params(); void load_level_table(); - void load_item_name_index(); - void load_item_tables(); - static std::shared_ptr load_word_select_table_from_system(); + void load_text_index(); + static std::shared_ptr create_item_name_index_for_version( + Version version, std::shared_ptr pmt, std::shared_ptr text_index); + void load_item_name_indexes(); + void load_drop_tables(); + void load_item_definitions(); void load_word_select_table(); void load_ep3_data(); - void resolve_ep3_card_names(); void load_quest_index(); void compile_functions(); void load_dol_files(); diff --git a/src/StepGraph.cc b/src/StepGraph.cc new file mode 100644 index 00000000..376da425 --- /dev/null +++ b/src/StepGraph.cc @@ -0,0 +1,71 @@ +#include "StepGraph.hh" + +using namespace std; + +void StepGraph::add_step(const string& name, const vector& depends_on_names, function&& execute) { + auto new_step = make_shared(Step{.execute = std::move(execute)}); + this->steps.emplace(name, new_step); + + for (const auto& depends_on_name : depends_on_names) { + auto upstream_step = this->steps.at(depends_on_name); + upstream_step->downstream_dependencies.emplace_back(new_step); + } +} + +void StepGraph::run(const string& start_step_name) { + vector start_step_names({start_step_name}); + this->run(start_step_names); +} + +void StepGraph::run(const vector& start_step_names) { + // Collect all steps to run + deque> steps_to_visit; + try { + for (const auto& start_step_name : start_step_names) { + steps_to_visit.emplace_back(this->steps.at(start_step_name)); + } + } catch (const out_of_range&) { + throw runtime_error("invalid step name"); + } + unordered_set> steps_to_run; + while (!steps_to_visit.empty()) { + auto step = std::move(steps_to_visit.front()); + steps_to_visit.pop_front(); + if (steps_to_run.emplace(step).second) { + for (const auto& downstream_step : step->downstream_dependencies) { + steps_to_visit.emplace_back(downstream_step); + } + } + } + + // Topological sort: repeatedly take all steps that are not a downstream + // dependency of any other step in the set + vector> steps_order; + steps_order.reserve(steps_to_run.size()); + while (!steps_to_run.empty()) { + unordered_set> candidate_steps = steps_to_run; + for (const auto& step : steps_to_run) { + for (const auto& downstream_step : step->downstream_dependencies) { + candidate_steps.erase(downstream_step); + } + } + if (candidate_steps.empty()) { + throw logic_error("dependency graph contains a cycle"); + } + for (const auto& step : candidate_steps) { + steps_to_run.erase(step); + steps_order.emplace_back(step); + } + } + + // Run the steps in order + uint64_t run_id = ++this->last_run_id; + for (auto step : steps_order) { + if (step->last_run_id < run_id) { + step->last_run_id = run_id; + if (step->execute) { + step->execute(); + } + } + } +} diff --git a/src/StepGraph.hh b/src/StepGraph.hh new file mode 100644 index 00000000..6ae1daad --- /dev/null +++ b/src/StepGraph.hh @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +struct StepGraph { + struct Step { + std::vector> downstream_dependencies; + std::function execute; + uint64_t last_run_id = 0; + }; + + std::unordered_map> steps; + uint64_t last_run_id = 0; + + StepGraph() = default; + + void add_step(const std::string& name, const std::vector& depends_on_names, std::function&& execute); + void run(const std::string& start_step); + void run(const std::vector& start_steps); +};