#include "ServerState.hh" #include #include #include #include #include "Compression.hh" #include "FileContentsCache.hh" #include "GVMEncoder.hh" #include "IPStackSimulator.hh" #include "Loggers.hh" #include "NetworkAddresses.hh" #include "SendCommands.hh" #include "Text.hh" using namespace std; ServerState::ServerState(shared_ptr base, const string& config_filename, bool is_replay) : config_filename(config_filename), is_replay(is_replay), dns_server_port(0), ip_stack_debug(false), allow_unregistered_users(false), allow_dc_pc_games(false), allow_gc_xb_games(true), item_tracking_enabled(true), enable_drops_behavior(BehaviorSwitch::ON_BY_DEFAULT), use_server_item_tables_behavior(BehaviorSwitch::OFF_BY_DEFAULT), 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), 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)), 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) {} void ServerState::init() { 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(); 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_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_TRIAL_EDITION); 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->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::add_client_to_available_lobby(shared_ptr c) { shared_ptr added_to_lobby; if (c->preferred_lobby_id >= 0) { try { auto l = this->find_lobby(c->preferred_lobby_id); if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) { l->add_client(c); added_to_lobby = l; } } catch (const out_of_range&) { } } if (!added_to_lobby.get()) { for (const auto& l : this->public_lobby_search_order) { try { if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) { l->add_client(c); added_to_lobby = l; break; } } catch (const out_of_range&) { } } } if (!added_to_lobby) { added_to_lobby = this->create_lobby(); added_to_lobby->set_flag(Lobby::Flag::PUBLIC); added_to_lobby->set_flag(Lobby::Flag::IS_OVERFLOW); added_to_lobby->block = 100; added_to_lobby->name = "Overflow"; added_to_lobby->max_clients = 12; added_to_lobby->event = this->pre_lobby_event; added_to_lobby->allow_version(c->version()); added_to_lobby->add_client(c); } // Send a join message to the joining player, and notifications to all others this->send_lobby_join_notifications(added_to_lobby, c); } void ServerState::remove_client_from_lobby(shared_ptr c) { auto l = c->lobby.lock(); if (l) { l->remove_client(c); if (!l->check_flag(Lobby::Flag::PERSISTENT) && (l->count_clients() == 0)) { this->remove_lobby(l->lobby_id); } else { send_player_leave_notification(l, c->lobby_client_id); } } } bool ServerState::change_client_lobby( shared_ptr c, shared_ptr new_lobby, bool send_join_notification, ssize_t required_client_id) { uint8_t old_lobby_client_id = c->lobby_client_id; auto current_lobby = c->lobby.lock(); try { if (current_lobby) { current_lobby->move_client_to_lobby(new_lobby, c, required_client_id); } else { new_lobby->add_client(c, required_client_id); } } catch (const out_of_range&) { return false; } if (current_lobby) { if (!current_lobby->check_flag(Lobby::Flag::PERSISTENT) && (current_lobby->count_clients() == 0)) { this->remove_lobby(current_lobby->lobby_id); } else { send_player_leave_notification(current_lobby, old_lobby_client_id); } } if (send_join_notification) { this->send_lobby_join_notifications(new_lobby, c); } return true; } void ServerState::send_lobby_join_notifications(shared_ptr l, shared_ptr joining_client) { for (auto& other_client : l->clients) { if (!other_client) { continue; } else if (other_client == joining_client) { send_join_lobby(joining_client, l); } else { send_player_join_notification(other_client, l, joining_client); } } for (auto& watcher_l : l->watcher_lobbies) { for (auto& watcher_c : watcher_l->clients) { if (!watcher_c) { continue; } send_player_join_notification(watcher_c, watcher_l, joining_client); } } } shared_ptr ServerState::find_lobby(uint32_t lobby_id) { try { return this->id_to_lobby.at(lobby_id); } catch (const out_of_range&) { return nullptr; } } vector> ServerState::all_lobbies() { vector> ret; for (auto& it : this->id_to_lobby) { ret.emplace_back(it.second); } return ret; } shared_ptr ServerState::create_lobby() { while (this->id_to_lobby.count(this->next_lobby_id)) { this->next_lobby_id++; } auto l = make_shared(this->shared_from_this(), this->next_lobby_id++); this->id_to_lobby.emplace(l->lobby_id, l); l->log.info("Created lobby"); return l; } void ServerState::remove_lobby(uint32_t lobby_id) { auto lobby_it = this->id_to_lobby.find(lobby_id); if (lobby_it == this->id_to_lobby.end()) { throw logic_error("attempted to remove nonexistent lobby"); } auto l = lobby_it->second; if (l->count_clients() != 0) { throw logic_error("attempted to delete lobby with clients in it"); } if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { auto primary_l = l->watched_lobby.lock(); if (primary_l) { primary_l->log.info("Unlinking watcher lobby %" PRIX32, l->lobby_id); primary_l->watcher_lobbies.erase(l); } else { l->log.info("No watched lobby to unlink"); } l->watched_lobby.reset(); } else { send_ep3_disband_watcher_lobbies(l); } l->log.info("Deleted lobby"); this->id_to_lobby.erase(lobby_it); } shared_ptr ServerState::find_client(const string* identifier, uint64_t serial_number, shared_ptr l) { if ((serial_number == 0) && identifier) { try { serial_number = stoull(*identifier, nullptr, 0); } catch (const exception&) { } } if (l) { try { return l->find_client(identifier, serial_number); } catch (const out_of_range&) { } } for (auto& other_l : this->all_lobbies()) { if (l == other_l) { continue; // don't bother looking again } try { return other_l->find_client(identifier, serial_number); } catch (const out_of_range&) { } } throw out_of_range("client not found"); } uint32_t ServerState::connect_address_for_client(shared_ptr c) const { if (c->channel.is_virtual_connection) { if (c->channel.remote_addr.ss_family != AF_INET) { throw logic_error("virtual connection is missing remote IPv4 address"); } const auto* sin = reinterpret_cast(&c->channel.remote_addr); return IPStackSimulator::connect_address_for_remote_address( ntohl(sin->sin_addr.s_addr)); } else { // TODO: we can do something smarter here, like use the sockname to find // out which interface the client is connected to, and return that address if (is_local_address(c->channel.remote_addr)) { return this->local_address; } else { return this->external_address; } } } shared_ptr ServerState::information_menu_for_version(Version version) const { if (is_v1_or_v2(version)) { return this->information_menu_v2; } else if (is_v3(version)) { return this->information_menu_v3; } throw out_of_range("no information menu exists for this version"); } shared_ptr ServerState::proxy_destinations_menu_for_version(Version version) const { switch (version) { case Version::DC_NTE: case Version::DC_V1_11_2000_PROTOTYPE: case Version::DC_V1: case Version::DC_V2: return this->proxy_destinations_menu_dc; case Version::PC_V2: return this->proxy_destinations_menu_pc; case Version::GC_NTE: case Version::GC_V3: case Version::GC_EP3_TRIAL_EDITION: case Version::GC_EP3: return this->proxy_destinations_menu_gc; case Version::XB_V3: return this->proxy_destinations_menu_xb; default: throw out_of_range("no proxy destinations menu exists for this version"); } } const vector>& ServerState::proxy_destinations_for_version(Version version) const { switch (version) { case Version::DC_NTE: case Version::DC_V1_11_2000_PROTOTYPE: case Version::DC_V1: case Version::DC_V2: case Version::GC_NTE: return this->proxy_destinations_dc; case Version::PC_V2: return this->proxy_destinations_pc; case Version::GC_V3: case Version::GC_EP3_TRIAL_EDITION: case Version::GC_EP3: return this->proxy_destinations_gc; case Version::XB_V3: return this->proxy_destinations_xb; default: throw out_of_range("no proxy destinations menu exists for this version"); } } shared_ptr ServerState::item_parameter_table_for_version(Version version) const { switch (version) { case Version::DC_NTE: case Version::DC_V1_11_2000_PROTOTYPE: case Version::DC_V1: case Version::DC_V2: case Version::PC_V2: return this->item_parameter_table_v2; case Version::GC_NTE: case Version::GC_V3: case Version::GC_EP3_TRIAL_EDITION: case Version::GC_EP3: case Version::XB_V3: return this->item_parameter_table_v3; case Version::BB_V4: return this->item_parameter_table_v4; default: throw out_of_range("no item parameter table exists for this version"); } } string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const { return this->item_name_index->describe_item( version, item, include_color_codes ? this->item_parameter_table_for_version(version) : nullptr); } void ServerState::set_port_configuration( const vector& port_configs) { this->name_to_port_config.clear(); this->number_to_port_config.clear(); bool any_port_is_pc_console_detect = false; for (const auto& pc : port_configs) { auto spc = make_shared(pc); if (!this->name_to_port_config.emplace(spc->name, spc).second) { // Note: This is a logic_error instead of a runtime_error because // port_configs comes from a JSON map, so the names should already all be // unique. In contrast, the user can define port configurations with the // same number while still writing valid JSON, so only one of these cases // can reasonably occur as a result of user behavior. throw logic_error("duplicate name in port configuration"); } if (!this->number_to_port_config.emplace(spc->port, spc).second) { throw runtime_error("duplicate number in port configuration"); } if (spc->behavior == ServerBehavior::PC_CONSOLE_DETECT) { any_port_is_pc_console_detect = true; } } if (any_port_is_pc_console_detect) { if (!this->name_to_port_config.count("pc-login")) { throw runtime_error("pc-login port is not defined, but some ports use the pc_console_detect behavior"); } if (!this->name_to_port_config.count("console-login")) { throw runtime_error("console-login port is not defined, but some ports use the pc_console_detect behavior"); } } } shared_ptr ServerState::load_bb_file( const string& patch_index_filename, const string& gsl_filename, const string& bb_directory_filename) const { if (this->bb_patch_file_index) { // First, look in the patch tree's data directory string patch_index_path = "./data/" + patch_index_filename; try { auto ret = this->bb_patch_file_index->get(patch_index_path)->load_data(); static_game_data_log.info("Loaded %s from file in BB patch tree", patch_index_path.c_str()); return ret; } catch (const out_of_range&) { static_game_data_log.info("%s missing from BB patch tree", patch_index_path.c_str()); } } if (this->bb_data_gsl) { // Second, look in the patch tree's data.gsl file const string& effective_gsl_filename = gsl_filename.empty() ? patch_index_filename : gsl_filename; try { // TODO: It's kinda not great that we copy the data here; find a way to // avoid doing this (also in the below case) auto ret = make_shared(this->bb_data_gsl->get_copy(effective_gsl_filename)); static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", effective_gsl_filename.c_str()); return ret; } catch (const out_of_range&) { static_game_data_log.info("%s missing from data.gsl in BB patch tree", effective_gsl_filename.c_str()); } // Third, look in data.gsl without the filename extension size_t dot_offset = effective_gsl_filename.rfind('.'); if (dot_offset != string::npos) { string no_ext_gsl_filename = effective_gsl_filename.substr(0, dot_offset); try { auto ret = make_shared(this->bb_data_gsl->get_copy(no_ext_gsl_filename)); static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", no_ext_gsl_filename.c_str()); return ret; } catch (const out_of_range&) { static_game_data_log.info("%s missing from data.gsl in BB patch tree", no_ext_gsl_filename.c_str()); } } } // Finally, look in system/blueburst const string& effective_bb_directory_filename = bb_directory_filename.empty() ? patch_index_filename : bb_directory_filename; static FileContentsCache cache(10 * 60 * 1000 * 1000); // 10 minutes try { auto ret = cache.get_or_load("system/blueburst/" + effective_bb_directory_filename); static_game_data_log.info("Loaded %s", effective_bb_directory_filename.c_str()); return ret.file->data; } catch (const exception& e) { static_game_data_log.info("%s missing from system/blueburst", effective_bb_directory_filename.c_str()); static_game_data_log.error("%s not found in any source", patch_index_filename.c_str()); throw cannot_open_file(patch_index_filename); } } 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()) { const auto& item_list = item_json_it.second; PortConfiguration& pc = ret.emplace_back(); pc.name = item_json_it.first; pc.port = item_list->at(0).as_int(); pc.version = enum_for_name(item_list->at(1).as_string().c_str()); pc.behavior = enum_for_name(item_list->at(2).as_string().c_str()); } return ret; } void ServerState::parse_config(const JSON& json, bool is_reload) { config_log.info("Parsing configuration"); auto parse_behavior_switch = [&](const string& json_key, BehaviorSwitch default_value) -> ServerState::BehaviorSwitch { try { string behavior = json.get_string(json_key); if (behavior == "Off") { return ServerState::BehaviorSwitch::OFF; } else if (behavior == "OffByDefault") { return ServerState::BehaviorSwitch::OFF_BY_DEFAULT; } else if (behavior == "OnByDefault") { return ServerState::BehaviorSwitch::ON_BY_DEFAULT; } else if (behavior == "On") { return ServerState::BehaviorSwitch::ON; } else { throw runtime_error("invalid value for " + json_key); } } catch (const out_of_range&) { return default_value; } }; this->name = json.at("ServerName").as_string(); if (!is_reload) { try { this->username = json.at("User").as_string(); if (this->username == "$SUDO_USER") { const char* user_from_env = getenv("SUDO_USER"); if (!user_from_env) { throw runtime_error("configuration specifies $SUDO_USER, but variable is not defined"); } this->username = user_from_env; } } catch (const out_of_range&) { } this->set_port_configuration(parse_port_configuration(json.at("PortConfiguration"))); this->dns_server_port = json.get_int("DNSServerPort", this->dns_server_port); try { for (const auto& item : json.at("IPStackListen").as_list()) { if (item->is_int()) { this->ip_stack_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); } else { this->ip_stack_addresses.emplace_back(item->as_string()); } } } catch (const out_of_range&) { } try { for (const auto& item : json.at("PPPStackListen").as_list()) { if (item->is_int()) { this->ppp_stack_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); } else { this->ppp_stack_addresses.emplace_back(item->as_string()); } } } catch (const out_of_range&) { } } auto local_address_str = json.at("LocalAddress").as_string(); try { this->local_address = this->all_addresses.at(local_address_str); string addr_str = string_for_address(this->local_address); config_log.info("Added local address: %s (%s)", addr_str.c_str(), local_address_str.c_str()); } catch (const out_of_range&) { this->local_address = address_for_string(local_address_str.c_str()); config_log.info("Added local address: %s", local_address_str.c_str()); } this->all_addresses.erase(""); this->all_addresses.emplace("", this->local_address); auto external_address_str = json.at("ExternalAddress").as_string(); try { this->external_address = this->all_addresses.at(external_address_str); string addr_str = string_for_address(this->external_address); config_log.info("Added external address: %s (%s)", addr_str.c_str(), external_address_str.c_str()); } catch (const out_of_range&) { this->external_address = address_for_string(external_address_str.c_str()); config_log.info("Added external address: %s", external_address_str.c_str()); } this->all_addresses.erase(""); this->all_addresses.emplace("", this->external_address); this->ip_stack_debug = json.get_bool("IPStackDebug", this->ip_stack_debug); this->allow_unregistered_users = json.get_bool("AllowUnregisteredUsers", this->allow_unregistered_users); this->item_tracking_enabled = json.get_bool("EnableItemTracking", this->item_tracking_enabled); this->enable_drops_behavior = parse_behavior_switch("ItemDropMode", this->enable_drops_behavior); this->use_server_item_tables_behavior = parse_behavior_switch("UseServerItemTables", this->use_server_item_tables_behavior); this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", this->cheat_mode_behavior); this->ep3_send_function_call_enabled = json.get_bool("EnableEpisode3SendFunctionCall", this->ep3_send_function_call_enabled); this->catch_handler_exceptions = json.get_bool("CatchHandlerExceptions", this->catch_handler_exceptions); auto parse_int_list = +[](const JSON& json) -> vector { vector ret; for (const auto& item : json.as_list()) { ret.emplace_back(item->as_int()); } return ret; }; this->ep3_infinite_meseta = json.get_bool("Episode3InfiniteMeseta", this->ep3_infinite_meseta); this->ep3_defeat_player_meseta_rewards = parse_int_list(json.get("Episode3DefeatPlayerMeseta", JSON::list())); this->ep3_defeat_com_meseta_rewards = parse_int_list(json.get("Episode3DefeatCOMMeseta", JSON::list())); this->ep3_final_round_meseta_bonus = json.get_int("Episode3FinalRoundMesetaBonus", this->ep3_final_round_meseta_bonus); this->ep3_jukebox_is_free = json.get_bool("Episode3JukeboxIsFree", this->ep3_jukebox_is_free); this->ep3_behavior_flags = json.get_int("Episode3BehaviorFlags", this->ep3_behavior_flags); this->ep3_card_auction_points = json.get_int("CardAuctionPoints", this->ep3_card_auction_points); this->proxy_allow_save_files = json.get_bool("ProxyAllowSaveFiles", this->proxy_allow_save_files); this->proxy_enable_login_options = json.get_bool("ProxyEnableLoginOptions", this->proxy_enable_login_options); try { const auto& i = json.at("CardAuctionSize"); if (i.is_int()) { this->ep3_card_auction_min_size = i.as_int(); this->ep3_card_auction_max_size = this->ep3_card_auction_min_size; } else { this->ep3_card_auction_min_size = i.at(0).as_int(); this->ep3_card_auction_max_size = i.at(1).as_int(); } } catch (const out_of_range&) { this->ep3_card_auction_min_size = 0; this->ep3_card_auction_max_size = 0; } try { for (const auto& it : json.get_dict("CardAuctionPool")) { 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}); } } catch (const out_of_range&) { } try { const auto& ep3_trap_cards_json = json.get_list("Episode3TrapCards"); if (!ep3_trap_cards_json.empty()) { 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()); } } } } catch (const out_of_range&) { } if (!this->is_replay) { 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); if (gvm.size() > 0x37000) { throw runtime_error(string_printf("banner %s is too large (0x%zX bytes; maximum size is 0x37000 bytes)", it->at(2).as_string().c_str(), gvm.size())); } string compressed = prs_compress_optimal(gvm.data(), gvm.size()); if (compressed.size() > 0x3800) { throw runtime_error(string_printf("banner %s cannot be compressed small enough (0x%zX bytes; maximum size is 0x3800 bytes compressed)", it->at(2).as_string().c_str(), compressed.size())); } config_log.info("Loaded Episode 3 lobby banner %s (0x%zX -> 0x%zX bytes)", it->at(2).as_string().c_str(), gvm.size(), compressed.size()); this->ep3_lobby_banners.emplace_back( Ep3LobbyBannerEntry{.type = static_cast(it->at(0).as_int()), .which = static_cast(it->at(1).as_int()), .data = std::move(compressed)}); } } { auto parse_ep3_ex_result_cmd = [&](const JSON& src) -> shared_ptr { auto ret = make_shared(); const auto& win_json = src.at("Win"); for (size_t z = 0; z < min(win_json.size(), 10); z++) { ret->win_entries[z].threshold = win_json.at(z).at(0).as_int(); ret->win_entries[z].value = win_json.at(z).at(1).as_int(); } const auto& lose_json = src.at("Lose"); for (size_t z = 0; z < min(lose_json.size(), 10); z++) { ret->lose_entries[z].threshold = lose_json.at(z).at(0).as_int(); ret->lose_entries[z].value = lose_json.at(z).at(1).as_int(); } return ret; }; const auto& categories_json = json.at("Episode3EXResultValues"); this->ep3_default_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Default")); try { this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Tournament")); } catch (const out_of_range&) { this->ep3_tournament_ex_values = this->ep3_default_ex_values; } try { this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("TournamentFinalMatch")); } catch (const out_of_range&) { this->ep3_tournament_final_round_ex_values = this->ep3_tournament_ex_values; } } try { this->quest_F95E_results.clear(); for (const auto& type_it : json.get_list("QuestF95EResultItems")) { auto& type_res = this->quest_F95E_results.emplace_back(); for (const auto& difficulty_it : type_it->as_list()) { auto& difficulty_res = type_res.emplace_back(); for (const auto& item_it : difficulty_it->as_list()) { string data = parse_data_string(item_it->as_string()); difficulty_res.emplace_back(ItemData::from_data(data)); } } } } catch (const out_of_range&) { } try { this->quest_F95F_results.clear(); for (const auto& it : json.get_list("QuestF95FResultItems")) { auto& list = it->as_list(); size_t price = list.at(0)->as_int(); string data = parse_data_string(list.at(1)->as_string()); this->quest_F95F_results.emplace_back(make_pair(price, ItemData::from_data(data))); } } catch (const out_of_range&) { } try { this->secret_lottery_results.clear(); for (const auto& it : json.get_list("SecretLotteryResultItems")) { string data = parse_data_string(it->as_string()); this->secret_lottery_results.emplace_back(ItemData::from_data(data)); } } catch (const out_of_range&) { } this->bb_global_exp_multiplier = json.get_int("BBGlobalEXPMultiplier", this->bb_global_exp_multiplier); 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&) { } } this->allow_dc_pc_games = json.get_bool("AllowDCPCGames", this->allow_dc_pc_games); this->allow_gc_xb_games = json.get_bool("AllowGCXBGames", this->allow_gc_xb_games); try { auto v = json.at("LobbyEvent"); uint8_t event = v.is_int() ? v.as_int() : event_for_name(v.as_string()); this->pre_lobby_event = event; for (const auto& l : this->all_lobbies()) { l->event = event; } } catch (const out_of_range&) { } 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())); } } config_log.info("Creating menus"); auto information_menu_v2 = make_shared(MenuID::INFORMATION, "Information"); auto information_menu_v3 = make_shared(MenuID::INFORMATION, "Information"); shared_ptr> information_contents_v2 = make_shared>(); shared_ptr> information_contents_v3 = make_shared>(); information_menu_v2->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); information_menu_v3->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); { auto blank_json = JSON::list(); const JSON& default_json = json.get("InformationMenuContents", blank_json); const JSON& v2_json = json.get("InformationMenuContentsV1V2", default_json); const JSON& v3_json = json.get("InformationMenuContentsV3", default_json); uint32_t item_id = 0; for (const auto& item : v2_json.as_list()) { string name = item->get_string(0); string short_desc = item->get_string(1); information_menu_v2->items.emplace_back(item_id, name, short_desc, 0); information_contents_v2->emplace_back(item->get_string(2)); item_id++; } item_id = 0; for (const auto& item : v3_json.as_list()) { string name = item->get_string(0); string short_desc = item->get_string(1); information_menu_v3->items.emplace_back(item_id, name, short_desc, MenuItem::Flag::REQUIRES_MESSAGE_BOXES); information_contents_v3->emplace_back(item->get_string(2)); item_id++; } } this->information_menu_v2 = information_menu_v2; this->information_menu_v3 = information_menu_v3; this->information_contents_v2 = information_contents_v2; this->information_contents_v3 = information_contents_v3; auto generate_proxy_destinations_menu = [&](vector>& ret_pds, const char* key) -> shared_ptr { auto ret = make_shared(MenuID::PROXY_DESTINATIONS, "Proxy server"); ret_pds.clear(); try { map sorted_jsons; for (const auto& it : json.at(key).as_dict()) { sorted_jsons.emplace(it.first, *it.second); } ret->items.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); ret->items.emplace_back(ProxyDestinationsMenuItemID::OPTIONS, "Options", "Set proxy session\noptions", 0); uint32_t item_id = 0; for (const auto& item : sorted_jsons) { const string& netloc_str = item.second.as_string(); const string& description = "$C7Remote server:\n$C6" + netloc_str; ret->items.emplace_back(item_id, item.first, description, 0); ret_pds.emplace_back(parse_netloc(netloc_str)); item_id++; } } catch (const out_of_range&) { } return ret; }; this->proxy_destinations_menu_dc = generate_proxy_destinations_menu(this->proxy_destinations_dc, "ProxyDestinations-DC"); this->proxy_destinations_menu_pc = generate_proxy_destinations_menu(this->proxy_destinations_pc, "ProxyDestinations-PC"); this->proxy_destinations_menu_gc = generate_proxy_destinations_menu(this->proxy_destinations_gc, "ProxyDestinations-GC"); this->proxy_destinations_menu_xb = generate_proxy_destinations_menu(this->proxy_destinations_xb, "ProxyDestinations-XB"); try { const string& netloc_str = json.get_string("ProxyDestination-Patch"); this->proxy_destination_patch = parse_netloc(netloc_str); config_log.info("Patch server proxy is enabled with destination %s", netloc_str.c_str()); for (auto& it : this->name_to_port_config) { if (is_patch(it.second->version)) { it.second->behavior = ServerBehavior::PROXY_SERVER; } } } catch (const out_of_range&) { this->proxy_destination_patch.first = ""; this->proxy_destination_patch.second = 0; } try { const string& netloc_str = json.get_string("ProxyDestination-BB"); this->proxy_destination_bb = parse_netloc(netloc_str); config_log.info("BB proxy is enabled with destination %s", netloc_str.c_str()); for (auto& it : this->name_to_port_config) { if (it.second->version == Version::BB_V4) { it.second->behavior = ServerBehavior::PROXY_SERVER; } } } catch (const out_of_range&) { this->proxy_destination_bb.first = ""; this->proxy_destination_bb.second = 0; } this->welcome_message = json.get_string("WelcomeMessage", ""); this->pc_patch_server_message = json.get_string("PCPatchServerMessage", ""); this->bb_patch_server_message = json.get_string("BBPatchServerMessage", ""); try { this->team_reward_defs_json = std::move(json.at("TeamRewards")); } catch (const out_of_range&) { } for (size_t z = 0; z < 4; z++) { shared_ptr prev = Map::DEFAULT_RARE_ENEMIES; try { string key = "RareEnemyRates-"; key += token_name_for_difficulty(z); this->rare_enemy_rates_by_difficulty[z] = make_shared(json.at(key)); prev = this->rare_enemy_rates_by_difficulty[z]; } catch (const out_of_range&) { this->rare_enemy_rates_by_difficulty[z] = prev; } } try { this->rare_enemy_rates_challenge = make_shared(json.at("RareEnemyRates-Challenge")); } catch (const out_of_range&) { this->rare_enemy_rates_challenge = Map::DEFAULT_RARE_ENEMIES; } this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_EP1; this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_EP2; this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_EP4; try { for (const auto& ep_it : json.get_dict("BBMinimumLevels")) { array levels({0, 0, 0, 0}); for (size_t z = 0; z < 4; z++) { levels[z] = ep_it.second->get_int(z) - 1; } switch (episode_for_token_name(ep_it.first)) { case Episode::EP1: this->min_levels_v4[0] = levels; break; case Episode::EP2: this->min_levels_v4[1] = levels; break; case Episode::EP4: this->min_levels_v4[2] = levels; break; default: throw runtime_error("unknown episode"); } } } catch (const out_of_range&) { } } void ServerState::load_bb_private_keys() { for (const string& filename : list_directory("system/blueburst/keys")) { if (!ends_with(filename, ".nsk")) { continue; } this->bb_private_keys.emplace_back(make_shared( load_object_file("system/blueburst/keys/" + filename))); config_log.info("Loaded Blue Burst key file: %s", filename.c_str()); } config_log.info("%zu Blue Burst key file(s) loaded", this->bb_private_keys.size()); } void ServerState::load_licenses() { config_log.info("Indexing licenses"); this->license_index = make_shared(); } 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() { if (isdir("system/patch-pc")) { config_log.info("Indexing PSO PC patch files"); this->pc_patch_file_index = make_shared("system/patch-pc"); } else { config_log.info("PSO PC patch files not present"); } if (isdir("system/patch-bb")) { config_log.info("Indexing PSO BB patch files"); this->bb_patch_file_index = make_shared("system/patch-bb"); try { auto gsl_file = this->bb_patch_file_index->get("./data/data.gsl"); this->bb_data_gsl = make_shared(gsl_file->load_data(), false); config_log.info("data.gsl found in BB patch files"); } catch (const out_of_range&) { config_log.info("data.gsl is not present in BB patch files"); } } else { config_log.info("PSO BB patch files not present"); } } void ServerState::load_battle_params() { config_log.info("Loading battle parameters"); this->battle_params = make_shared( this->load_bb_file("BattleParamEntry_on.dat"), this->load_bb_file("BattleParamEntry_lab_on.dat"), this->load_bb_file("BattleParamEntry_ep4_on.dat"), this->load_bb_file("BattleParamEntry.dat"), this->load_bb_file("BattleParamEntry_lab.dat"), this->load_bb_file("BattleParamEntry_ep4.dat")); } void ServerState::load_level_table() { config_log.info("Loading level table"); this->level_table = make_shared(this->load_bb_file("PlyLevelTbl.prs"), true); } void ServerState::load_word_select_table() { config_log.info("Loading Word Select table"); this->word_select_table = make_shared(JSON::parse(load_file("system/word-select-table.json"))); } void ServerState::load_item_tables() { config_log.info("Loading item name index"); this->item_name_index = make_shared( JSON::parse(load_file("system/item-tables/names-v2.json")), JSON::parse(load_file("system/item-tables/names-v3.json")), JSON::parse(load_file("system/item-tables/names-v4.json"))); config_log.info("Loading rare item sets"); unordered_map> new_rare_item_sets; for (const auto& filename : list_directory_sorted("system/item-tables")) { if (!starts_with(filename, "rare-table-")) { continue; } string path = "system/item-tables/" + filename; size_t ext_offset = filename.rfind('.'); string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset); if (ends_with(filename, "-v1.json")) { config_log.info("Loading v1 JSON rare item table %s", filename.c_str()); new_rare_item_sets.emplace(basename, make_shared(JSON::parse(load_file(path)), Version::DC_V1, this->item_name_index)); } else if (ends_with(filename, "-v2.json")) { config_log.info("Loading v2 JSON rare item table %s", filename.c_str()); new_rare_item_sets.emplace(basename, make_shared(JSON::parse(load_file(path)), Version::PC_V2, this->item_name_index)); } else if (ends_with(filename, "-v3.json")) { config_log.info("Loading v3 JSON rare item table %s", filename.c_str()); new_rare_item_sets.emplace(basename, make_shared(JSON::parse(load_file(path)), Version::GC_V3, this->item_name_index)); } else if (ends_with(filename, "-v4.json")) { config_log.info("Loading v4 JSON rare item table %s", filename.c_str()); new_rare_item_sets.emplace(basename, make_shared(JSON::parse(load_file(path)), Version::BB_V4, this->item_name_index)); } else if (ends_with(filename, ".afs")) { config_log.info("Loading AFS rare item table %s", filename.c_str()); auto data = make_shared(load_file(path)); new_rare_item_sets.emplace(basename, make_shared(AFSArchive(data), false)); } else if (ends_with(filename, ".gsl")) { config_log.info("Loading GSL rare item table %s", filename.c_str()); auto data = make_shared(load_file(path)); new_rare_item_sets.emplace(basename, make_shared(GSLArchive(data, false), false)); } else if (ends_with(filename, ".gslb")) { config_log.info("Loading GSL rare item table %s", filename.c_str()); auto data = make_shared(load_file(path)); new_rare_item_sets.emplace(basename, make_shared(GSLArchive(data, true), true)); } else if (ends_with(filename, ".rel")) { config_log.info("Loading REL rare item table %s", filename.c_str()); new_rare_item_sets.emplace(basename, make_shared(load_file(path), true)); } } if (!new_rare_item_sets.count("rare-table-v4")) { config_log.info("rare-table-v4 rare item set is not available; loading from BB data"); new_rare_item_sets.emplace("rare-table-v4", make_shared(load_file("system/blueburst/ItemRT.rel"), true)); } this->rare_item_sets.swap(new_rare_item_sets); config_log.info("Loading v2 common item table"); auto ct_data_v2 = make_shared(load_file("system/item-tables/ItemCT-v2.afs")); auto pt_data_v2 = make_shared(load_file("system/item-tables/ItemPT-v2.afs")); this->common_item_set_v2 = make_shared(pt_data_v2, ct_data_v2); config_log.info("Loading v3+v4 common item table"); auto pt_data_v3_v4 = make_shared(load_file("system/item-tables/ItemPT-gc-v4.gsl")); this->common_item_set_v3_v4 = make_shared(pt_data_v3_v4, true); config_log.info("Loading armor table"); auto armor_data = make_shared(load_file("system/item-tables/ArmorRandom-gc.rel")); this->armor_random_set = make_shared(armor_data); config_log.info("Loading tool table"); auto tool_data = make_shared(load_file("system/item-tables/ToolRandom-gc.rel")); this->tool_random_set = make_shared(tool_data); config_log.info("Loading weapon tables"); const char* filenames[4] = { "system/item-tables/WeaponRandomNormal-gc.rel", "system/item-tables/WeaponRandomHard-gc.rel", "system/item-tables/WeaponRandomVeryHard-gc.rel", "system/item-tables/WeaponRandomUltimate-gc.rel", }; for (size_t z = 0; z < 4; z++) { auto weapon_data = make_shared(load_file(filenames[z])); this->weapon_random_sets[z] = make_shared(weapon_data); } config_log.info("Loading tekker adjustment table"); auto tekker_data = make_shared(load_file("system/item-tables/JudgeItem-gc.rel")); this->tekker_adjustment_set = make_shared(tekker_data); config_log.info("Loading item definition tables"); auto pmt_data_v2 = make_shared(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs"))); this->item_parameter_table_v2 = make_shared(pmt_data_v2, ItemParameterTable::Version::V2); auto pmt_data_v3 = make_shared(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs"))); this->item_parameter_table_v3 = make_shared(pmt_data_v3, ItemParameterTable::Version::V3); auto pmt_data_v4 = make_shared(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs"))); this->item_parameter_table_v4 = make_shared(pmt_data_v4, ItemParameterTable::Version::V4); config_log.info("Loading mag evolution table"); auto mag_data = make_shared(prs_decompress(load_file("system/item-tables/ItemMagEdit-bb.prs"))); this->mag_evolution_table = make_shared(mag_data); } void ServerState::load_ep3_data() { config_log.info("Collecting Episode 3 maps"); this->ep3_map_index = make_shared("system/ep3/maps"); config_log.info("Loading Episode 3 card definitions"); this->ep3_card_index = make_shared( "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"); config_log.info("Loading Episode 3 trial card definitions"); this->ep3_card_index_trial = make_shared( "system/ep3/card-definitions-trial.mnr", "system/ep3/card-definitions-trial.mnrd", "system/ep3/card-text-trial.mnr", "system/ep3/card-text-trial.mnrd", "system/ep3/card-dice-text-trial.mnr", "system/ep3/card-dice-text-trial.mnrd"); config_log.info("Loading Episode 3 COM decks"); this->ep3_com_deck_index = make_shared("system/ep3/com-decks.json"); const string& tournament_state_filename = "system/ep3/tournament-state.json"; this->ep3_tournament_index = make_shared( this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename); this->ep3_tournament_index->link_all_clients(this->shared_from_this()); 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); config_log.info("Collecting Episode 3 download quests"); this->ep3_download_quest_index = make_shared("system/ep3/maps-download", this->quest_category_index, true); } void ServerState::compile_functions() { config_log.info("Compiling client functions"); this->function_code_index = make_shared("system/ppc"); } void ServerState::load_dol_files() { config_log.info("Loading 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; } shared_ptr ServerState::quest_index_for_client(shared_ptr c) const { return is_ep3(c->version()) ? this->ep3_download_quest_index : this->default_quest_index; }