From 7426c5ad1f8ca612395144e0e4877bad1bf89c5d Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 15 Mar 2023 14:47:06 -0700 Subject: [PATCH] make reloading more stable and add more options --- README.md | 9 +- src/ItemParameterTable.hh | 3 +- src/Main.cc | 313 +--------------------------------- src/ServerShell.cc | 52 ++++-- src/ServerState.cc | 349 +++++++++++++++++++++++++++++++++++++- src/ServerState.hh | 21 ++- 6 files changed, 411 insertions(+), 336 deletions(-) diff --git a/README.md b/README.md index 569e831d..a371de9e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Current known issues / missing features / things to do: - Find any remaining mismatches in enemy IDs / experience (Episode 1 is mostly fixed now, except for Dark Falz) - Replace enemy list with quest layout when loading a quest - Implement all remaining player_use_item cases (there are many!) + - Handle mag feeding and evolution properly - Implement trade window - Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on other versions). - PSOX is not tested at all. @@ -141,7 +142,7 @@ Episode 3 download quests consist only of a .bin file - there is no correspondin When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats. -If you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quests` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end. +Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quests` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end. All quests, including those originally in GCI or DLQ format, are treated as online quests unless their filenames specify the dl category. newserv allows players to download all quests, even those in non-download categories. @@ -175,6 +176,8 @@ Episode 3 state and game data is stored in the system/ep3 directory. The files i There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndex.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress .bin or .mnm files before editing them, but you don't need to compress the files again to use them - just put the .bind or .mnmd file in the maps directory and newserv will make it available. +Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3` in the interactive shell to make the changes take effect without restarting the server. + ### Client patch directories If you're not playing PSO Blue Burst on newserv, you can skip these steps. @@ -187,6 +190,8 @@ For BB clients, newserv reads some files out of the patch data to implement game Specifically, the patch-bb directory should contain at least the data.gsl file and all map_*.dat files from the version of PSOBB that you want to play on newserv. You can copy these files out of the client's data directory from a clean installation, and put them in system/patch-bb/data. +Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patches` in the interactive shell to make the changes take effect without restarting the server. + ### Memory patches and DOL files Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available. @@ -202,6 +207,8 @@ You can put memory patches in the system/ppc directory with filenames like Patch You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary. +Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server. + I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence. ### Using newserv as a proxy diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index b7f09f91..e1d76316 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -51,8 +51,7 @@ public: struct Mag { ItemBase base; - uint8_t feed_table; - uint8_t unknown_a1; + le_uint16_t feed_table; uint8_t photon_blast; uint8_t activation; uint8_t on_pb_full; diff --git a/src/Main.cc b/src/Main.cc index d57c2d9b..06916f04 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -37,20 +37,6 @@ bool use_terminal_colors = false; -static vector parse_port_configuration( - shared_ptr json) { - vector ret; - for (const auto& item_json_it : json->as_dict()) { - auto item_list = item_json_it.second->as_list(); - PortConfiguration& pc = ret.emplace_back(); - pc.name = item_json_it.first; - pc.port = item_list[0]->as_int(); - pc.version = version_for_name(item_list[1]->as_string().c_str()); - pc.behavior = server_behavior_for_name(item_list[2]->as_string().c_str()); - } - return ret; -} - template vector parse_int_vector(shared_ptr o) { vector ret; @@ -62,174 +48,6 @@ vector parse_int_vector(shared_ptr o) { -void populate_state_from_config(shared_ptr s, - shared_ptr config_json) { - const auto& d = config_json->as_dict(); - - s->name = decode_sjis(d.at("ServerName")->as_string()); - - try { - s->username = d.at("User")->as_string(); - if (s->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"); - } - s->username = user_from_env; - } - } catch (const out_of_range&) { } - - s->set_port_configuration(parse_port_configuration(d.at("PortConfiguration"))); - - auto local_address_str = d.at("LocalAddress")->as_string(); - try { - s->local_address = s->all_addresses.at(local_address_str); - string addr_str = string_for_address(s->local_address); - config_log.info("Added local address: %s (%s)", addr_str.c_str(), - local_address_str.c_str()); - } catch (const out_of_range&) { - s->local_address = address_for_string(local_address_str.c_str()); - config_log.info("Added local address: %s", local_address_str.c_str()); - } - s->all_addresses.emplace("", s->local_address); - - auto external_address_str = d.at("ExternalAddress")->as_string(); - try { - s->external_address = s->all_addresses.at(external_address_str); - string addr_str = string_for_address(s->external_address); - config_log.info("Added external address: %s (%s)", addr_str.c_str(), - external_address_str.c_str()); - } catch (const out_of_range&) { - s->external_address = address_for_string(external_address_str.c_str()); - config_log.info("Added external address: %s", external_address_str.c_str()); - } - s->all_addresses.emplace("", s->external_address); - - try { - s->dns_server_port = d.at("DNSServerPort")->as_int(); - } catch (const out_of_range&) { - s->dns_server_port = 0; - } - - try { - for (const auto& item : d.at("IPStackListen")->as_list()) { - s->ip_stack_addresses.emplace_back(item->as_string()); - } - } catch (const out_of_range&) { } - try { - s->ip_stack_debug = d.at("IPStackDebug")->as_bool(); - } catch (const out_of_range&) { } - - try { - s->allow_unregistered_users = d.at("AllowUnregisteredUsers")->as_bool(); - } catch (const out_of_range&) { - s->allow_unregistered_users = true; - } - - try { - s->item_tracking_enabled = d.at("EnableItemTracking")->as_bool(); - } catch (const out_of_range&) { - s->item_tracking_enabled = true; - } - - try { - s->episode_3_send_function_call_enabled = d.at("EnableEpisode3SendFunctionCall")->as_bool(); - } catch (const out_of_range&) { - s->episode_3_send_function_call_enabled = false; - } - - try { - s->catch_handler_exceptions = d.at("CatchHandlerExceptions")->as_bool(); - } catch (const out_of_range&) { - s->catch_handler_exceptions = true; - } - - try { - s->proxy_allow_save_files = d.at("ProxyAllowSaveFiles")->as_bool(); - } catch (const out_of_range&) { - s->proxy_allow_save_files = true; - } - try { - s->proxy_enable_login_options = d.at("ProxyEnableLoginOptions")->as_bool(); - } catch (const out_of_range&) { - s->proxy_enable_login_options = false; - } - - try { - s->ep3_behavior_flags = d.at("Episode3BehaviorFlags")->as_int(); - } catch (const out_of_range&) { - s->ep3_behavior_flags = 0; - } - - try { - s->ep3_card_auction_points = d.at("CardAuctionPoints")->as_int(); - } catch (const out_of_range&) { - s->ep3_card_auction_points = 0; - } - try { - auto i = d.at("CardAuctionSize"); - if (i->is_int()) { - s->ep3_card_auction_min_size = i->as_int(); - s->ep3_card_auction_max_size = s->ep3_card_auction_min_size; - } else { - s->ep3_card_auction_min_size = i->as_list().at(0)->as_int(); - s->ep3_card_auction_max_size = i->as_list().at(1)->as_int(); - } - } catch (const out_of_range&) { - s->ep3_card_auction_min_size = 0; - s->ep3_card_auction_max_size = 0; - } - - try { - for (const auto& it : d.at("CardAuctionPool")->as_dict()) { - const auto& card_name = it.first; - const auto& card_cfg_json = it.second->as_list(); - s->ep3_card_auction_pool.emplace(card_name, make_pair( - card_cfg_json.at(0)->as_int(), card_cfg_json.at(1)->as_int())); - } - } catch (const out_of_range&) { } - - shared_ptr log_levels_json; - try { - log_levels_json = d.at("LogLevels"); - } catch (const out_of_range&) { } - if (log_levels_json.get()) { - set_log_levels_from_json(log_levels_json); - } - - for (const string& filename : list_directory("system/blueburst/keys")) { - if (!ends_with(filename, ".nsk")) { - continue; - } - s->bb_private_keys.emplace_back(new PSOBBEncryption::KeyFile( - 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", s->bb_private_keys.size()); - - try { - bool run_shell = d.at("RunInteractiveShell")->as_bool(); - s->run_shell_behavior = run_shell ? - ServerState::RunShellBehavior::ALWAYS : - ServerState::RunShellBehavior::NEVER; - } catch (const out_of_range&) { } - - try { - auto v = d.at("LobbyEvent"); - uint8_t event = v->is_int() ? v->as_int() : event_for_name(v->as_string()); - s->pre_lobby_event = event; - for (const auto& l : s->all_lobbies()) { - l->event = event; - } - } catch (const out_of_range&) { } - - try { - s->ep3_menu_song = d.at("Episode3MenuSong")->as_int(); - } catch (const out_of_range&) { } -} - - - void drop_privileges(const string& username) { if ((getuid() != 0) || (getgid() != 0)) { throw runtime_error(string_printf( @@ -1022,6 +840,7 @@ int main(int argc, char** argv) { case Behavior::REPLAY_LOG: case Behavior::RUN_SERVER: { + bool is_replay = behavior == Behavior::REPLAY_LOG; signal(SIGPIPE, SIG_IGN); if (isatty(fileno(stderr))) { @@ -1029,135 +848,7 @@ int main(int argc, char** argv) { } shared_ptr base(event_base_new(), event_base_free); - shared_ptr state(new ServerState()); - - config_log.info("Reading network addresses"); - state->all_addresses = get_local_addresses(); - for (const auto& it : state->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()); - } - - config_log.info("Loading configuration"); - auto config_json = JSONObject::parse(load_file(config_filename)); - populate_state_from_config(state, config_json); - - if (behavior != Behavior::REPLAY_LOG) { - config_log.info("Loading license list"); - state->license_manager.reset(new LicenseManager("system/licenses.nsi")); - } else { - state->license_manager.reset(new LicenseManager()); - } - - if (isdir("system/patch-pc")) { - config_log.info("Indexing PSO PC patch files"); - state->pc_patch_file_index.reset(new PatchFileIndex("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"); - state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb")); - try { - auto gsl_file = state->bb_patch_file_index->get("./data/data.gsl"); - state->bb_data_gsl.reset(new GSLArchive(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"); - } - - config_log.info("Loading battle parameters"); - state->battle_params.reset(new BattleParamsIndex( - state->load_bb_file("BattleParamEntry_on.dat"), - state->load_bb_file("BattleParamEntry_lab_on.dat"), - state->load_bb_file("BattleParamEntry_ep4_on.dat"), - state->load_bb_file("BattleParamEntry.dat"), - state->load_bb_file("BattleParamEntry_lab.dat"), - state->load_bb_file("BattleParamEntry_ep4.dat"))); - - config_log.info("Loading level table"); - state->level_table.reset(new LevelTable( - state->load_bb_file("PlyLevelTbl.prs"), true)); - - config_log.info("Loading rare item table"); - state->rare_item_set.reset(new RELRareItemSet( - state->load_bb_file("ItemRT.rel"))); - - // Note: These files don't exist in BB, so we use the GC versions of them - // instead. This doesn't include Episode 4 of course, so we use Episode 1 - // parameters for Episode 4 implicitly. - { - config_log.info("Loading common item tables"); - shared_ptr pt_data(new string(load_file( - "system/blueburst/ItemPT_GC.gsl"))); - state->common_item_set.reset(new CommonItemSet(pt_data)); - - shared_ptr armor_data(new string(load_file( - "system/blueburst/ArmorRandom_GC.rel"))); - state->armor_random_set.reset(new ArmorRandomSet(armor_data)); - - shared_ptr tool_data(new string(load_file( - "system/blueburst/ToolRandom_GC.rel"))); - state->tool_random_set.reset(new ToolRandomSet(tool_data)); - - const char* filenames[4] = { - "system/blueburst/WeaponRandomNormal_GC.rel", - "system/blueburst/WeaponRandomHard_GC.rel", - "system/blueburst/WeaponRandomVeryHard_GC.rel", - "system/blueburst/WeaponRandomUltimate_GC.rel", - }; - for (size_t z = 0; z < 4; z++) { - shared_ptr weapon_data(new string(load_file(filenames[z]))); - state->weapon_random_sets[z].reset(new WeaponRandomSet(weapon_data)); - } - } - - config_log.info("Loading item definition table"); - { - shared_ptr data(new string(prs_decompress(load_file( - "system/blueburst/ItemPMT.prs")))); - state->item_parameter_table.reset(new ItemParameterTable(data)); - } - - config_log.info("Collecting Episode 3 data"); - state->ep3_data_index.reset(new Episode3::DataIndex( - "system/ep3", state->ep3_behavior_flags)); - - const string& tournament_state_filename = "system/ep3/tournament-state.json"; - try { - state->ep3_tournament_index.reset(new Episode3::TournamentIndex( - state->ep3_data_index, tournament_state_filename)); - config_log.info("Loaded Episode 3 tournament state"); - } catch (const exception& e) { - config_log.warning("Cannot load Episode 3 tournament state: %s", e.what()); - state->ep3_tournament_index.reset(new Episode3::TournamentIndex( - state->ep3_data_index, tournament_state_filename, true)); - } - - config_log.info("Collecting quest metadata"); - state->quest_index.reset(new QuestIndex("system/quests")); - - if (behavior != Behavior::REPLAY_LOG) { - config_log.info("Compiling client functions"); - state->function_code_index.reset(new FunctionCodeIndex("system/ppc")); - config_log.info("Loading DOL files"); - state->dol_file_index.reset(new DOLFileIndex("system/dol")); - } else { - state->function_code_index.reset(new FunctionCodeIndex()); - state->dol_file_index.reset(new DOLFileIndex()); - } - - config_log.info("Creating menus"); - state->create_menus(config_json); - - if (behavior == Behavior::REPLAY_LOG) { - state->allow_saving = false; - state->license_manager->set_autosave(false); - config_log.info("Saving disabled because this is a replay session"); - } + shared_ptr state(new ServerState(config_filename, is_replay)); shared_ptr dns_server; if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) { diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 84f4e2c1..0e04b7dc 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -103,9 +103,23 @@ Server commands:\n\ exit (or ctrl+d)\n\ Shut down the server.\n\ reload ...\n\ - Reload data. can be licenses, quests, functions, programs, or ep3.\n\ - Reloading will not affect items that are in use; for example, if a client\'s\n\ - license is deleted by reloading, they will not be disconnected immediately.\n\ + Reload various parts of the server configuration. 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 the Episode 3 card definitions and maps\n\ + quests - reindex all quests\n\ + functions - recompile all client-side functions\n\ + dol-files - reindex all DOL files\n\ + config - reload some fields from config.json\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\ + the battle parameters, so if these are changed without restarting, clients\n\ + may see (for example) EXP messages inconsistent with the amounts of EXP\n\ + actually received.\n\ add-license \n\ Add a license to the server. is some subset of the following:\n\ bb-username= (BB username)\n\ @@ -251,21 +265,27 @@ session with ID 17205AE4, run the command `on 17205AE4 sc 1D 00 04 00`.\n\ } for (const string& type : types) { if (type == "licenses") { - shared_ptr lm(new LicenseManager("system/licenses.nsi")); - this->state->license_manager = lm; + this->state->load_licenses(); + } 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_tables(); + } else if (type == "ep3") { + this->state->load_ep3_data(); } else if (type == "quests") { - shared_ptr qi(new QuestIndex("system/quests")); - this->state->quest_index = qi; + this->state->load_quest_index(); } else if (type == "functions") { - shared_ptr fci(new FunctionCodeIndex("system/ppc")); - this->state->function_code_index = fci; - } else if (type == "programs") { - shared_ptr dfi(new DOLFileIndex("system/dol")); - this->state->dol_file_index = dfi; - } else if (type == "programs") { - shared_ptr data_index(new Episode3::DataIndex( - "system/ep3", this->state->ep3_behavior_flags)); - this->state->ep3_data_index = data_index; + auto config_json = this->state->load_config(); + this->state->compile_functions(); + this->state->create_menus(config_json); + } else if (type == "dol-files") { + auto config_json = this->state->load_config(); + this->state->load_dol_files(); + this->state->create_menus(config_json); } else { throw invalid_argument("incorrect data type"); } diff --git a/src/ServerState.cc b/src/ServerState.cc index 71d51767..4ffc0a6b 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -5,6 +5,7 @@ #include #include +#include "Compression.hh" #include "FileContentsCache.hh" #include "IPStackSimulator.hh" #include "Loggers.hh" @@ -16,8 +17,10 @@ using namespace std; -ServerState::ServerState() - : dns_server_port(0), +ServerState::ServerState(const char* 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_saving(true), @@ -69,13 +72,33 @@ ServerState::ServerState() } // Annoyingly, the CARD lobbies should be searched first, but are sent at the - // end of the lobby list command, so we have to change t he search order + // end of the lobby list command, so we have to change the search order // manually here. this->public_lobby_search_order_ep3 = this->public_lobby_search_order_non_v1; this->public_lobby_search_order_ep3.insert( this->public_lobby_search_order_ep3.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); + this->load_licenses(); + this->load_patch_indexes(); + this->load_battle_params(); + this->load_level_table(); + this->load_item_tables(); + this->load_ep3_data(); + this->load_quest_index(); + this->compile_functions(); + this->load_dol_files(); + this->create_menus(config); + + if (this->is_replay) { + this->allow_saving = false; + config_log.info("Saving disabled because this is a replay session"); + } } void ServerState::add_client_to_available_lobby(shared_ptr c) { @@ -363,8 +386,15 @@ void ServerState::set_port_configuration( void ServerState::create_menus(shared_ptr config_json) { + config_log.info("Creating menus"); const auto& d = config_json->as_dict(); + this->main_menu.clear(); + this->proxy_destinations_menu_dc.clear(); + this->proxy_destinations_menu_pc.clear(); + this->proxy_destinations_menu_gc.clear(); + this->proxy_destinations_menu_xb.clear(); + shared_ptr> information_menu_v2(new vector()); shared_ptr> information_menu_v3(new vector()); shared_ptr> information_contents(new vector()); @@ -562,3 +592,316 @@ shared_ptr ServerState::load_bb_file( 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()); + } +} + +shared_ptr ServerState::load_config() const { + config_log.info("Loading configuration"); + return JSONObject::parse(load_file(this->config_filename)); +} + +static vector parse_port_configuration( + shared_ptr json) { + vector ret; + for (const auto& item_json_it : json->as_dict()) { + auto item_list = item_json_it.second->as_list(); + PortConfiguration& pc = ret.emplace_back(); + pc.name = item_json_it.first; + pc.port = item_list[0]->as_int(); + pc.version = version_for_name(item_list[1]->as_string().c_str()); + pc.behavior = server_behavior_for_name(item_list[2]->as_string().c_str()); + } + return ret; +} + +void ServerState::parse_config(shared_ptr config_json) { + config_log.info("Parsing configuration"); + const auto& d = config_json->as_dict(); + + this->name = decode_sjis(d.at("ServerName")->as_string()); + + try { + this->username = d.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(d.at("PortConfiguration"))); + + auto local_address_str = d.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.emplace("", this->local_address); + + auto external_address_str = d.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.emplace("", this->external_address); + + try { + this->dns_server_port = d.at("DNSServerPort")->as_int(); + } catch (const out_of_range&) { + this->dns_server_port = 0; + } + + try { + for (const auto& item : d.at("IPStackListen")->as_list()) { + this->ip_stack_addresses.emplace_back(item->as_string()); + } + } catch (const out_of_range&) { } + try { + this->ip_stack_debug = d.at("IPStackDebug")->as_bool(); + } catch (const out_of_range&) { } + + try { + this->allow_unregistered_users = d.at("AllowUnregisteredUsers")->as_bool(); + } catch (const out_of_range&) { + this->allow_unregistered_users = true; + } + + try { + this->item_tracking_enabled = d.at("EnableItemTracking")->as_bool(); + } catch (const out_of_range&) { + this->item_tracking_enabled = true; + } + + try { + this->episode_3_send_function_call_enabled = d.at("EnableEpisode3SendFunctionCall")->as_bool(); + } catch (const out_of_range&) { + this->episode_3_send_function_call_enabled = false; + } + + try { + this->catch_handler_exceptions = d.at("CatchHandlerExceptions")->as_bool(); + } catch (const out_of_range&) { + this->catch_handler_exceptions = true; + } + + try { + this->proxy_allow_save_files = d.at("ProxyAllowSaveFiles")->as_bool(); + } catch (const out_of_range&) { + this->proxy_allow_save_files = true; + } + try { + this->proxy_enable_login_options = d.at("ProxyEnableLoginOptions")->as_bool(); + } catch (const out_of_range&) { + this->proxy_enable_login_options = false; + } + + try { + this->ep3_behavior_flags = d.at("Episode3BehaviorFlags")->as_int(); + } catch (const out_of_range&) { + this->ep3_behavior_flags = 0; + } + + try { + this->ep3_card_auction_points = d.at("CardAuctionPoints")->as_int(); + } catch (const out_of_range&) { + this->ep3_card_auction_points = 0; + } + try { + auto i = d.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->as_list().at(0)->as_int(); + this->ep3_card_auction_max_size = i->as_list().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 : d.at("CardAuctionPool")->as_dict()) { + const auto& card_name = it.first; + const auto& card_cfg_json = it.second->as_list(); + this->ep3_card_auction_pool.emplace(card_name, make_pair( + card_cfg_json.at(0)->as_int(), card_cfg_json.at(1)->as_int())); + } + } catch (const out_of_range&) { } + + shared_ptr log_levels_json; + try { + log_levels_json = d.at("LogLevels"); + } catch (const out_of_range&) { } + if (log_levels_json.get()) { + set_log_levels_from_json(log_levels_json); + } + + for (const string& filename : list_directory("system/blueburst/keys")) { + if (!ends_with(filename, ".nsk")) { + continue; + } + this->bb_private_keys.emplace_back(new PSOBBEncryption::KeyFile( + 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()); + + try { + bool run_shell = d.at("RunInteractiveShell")->as_bool(); + this->run_shell_behavior = run_shell ? + ServerState::RunShellBehavior::ALWAYS : + ServerState::RunShellBehavior::NEVER; + } catch (const out_of_range&) { } + + try { + auto v = d.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&) { } + + try { + this->ep3_menu_song = d.at("Episode3MenuSong")->as_int(); + } catch (const out_of_range&) { } +} + +void ServerState::load_licenses() { + config_log.info("Loading license list"); + this->license_manager.reset(new LicenseManager("system/licenses.nsi")); + if (this->is_replay) { + this->license_manager->set_autosave(false); + } +} + +void ServerState::load_patch_indexes() { + if (isdir("system/patch-pc")) { + config_log.info("Indexing PSO PC patch files"); + this->pc_patch_file_index.reset(new PatchFileIndex("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.reset(new PatchFileIndex("system/patch-bb")); + try { + auto gsl_file = this->bb_patch_file_index->get("./data/data.gsl"); + this->bb_data_gsl.reset(new GSLArchive(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.reset(new BattleParamsIndex( + 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.reset(new LevelTable( + this->load_bb_file("PlyLevelTbl.prs"), true)); +} + +void ServerState::load_item_tables() { + config_log.info("Loading rare item table"); + this->rare_item_set.reset(new RELRareItemSet( + this->load_bb_file("ItemRT.rel"))); + + // Note: These files don't exist in BB, so we use the GC versions of them + // instead. This doesn't include Episode 4 of course, so we use Episode 1 + // parameters for Episode 4 implicitly. + config_log.info("Loading common item tables"); + shared_ptr pt_data(new string(load_file( + "system/blueburst/ItemPT_GC.gsl"))); + this->common_item_set.reset(new CommonItemSet(pt_data)); + + shared_ptr armor_data(new string(load_file( + "system/blueburst/ArmorRandom_GC.rel"))); + this->armor_random_set.reset(new ArmorRandomSet(armor_data)); + + shared_ptr tool_data(new string(load_file( + "system/blueburst/ToolRandom_GC.rel"))); + this->tool_random_set.reset(new ToolRandomSet(tool_data)); + + const char* filenames[4] = { + "system/blueburst/WeaponRandomNormal_GC.rel", + "system/blueburst/WeaponRandomHard_GC.rel", + "system/blueburst/WeaponRandomVeryHard_GC.rel", + "system/blueburst/WeaponRandomUltimate_GC.rel", + }; + for (size_t z = 0; z < 4; z++) { + shared_ptr weapon_data(new string(load_file(filenames[z]))); + this->weapon_random_sets[z].reset(new WeaponRandomSet(weapon_data)); + } + + config_log.info("Loading item definition table"); + shared_ptr data(new string(prs_decompress(load_file( + "system/blueburst/ItemPMT.prs")))); + this->item_parameter_table.reset(new ItemParameterTable(data)); +} + +void ServerState::load_ep3_data() { + config_log.info("Collecting Episode 3 data"); + this->ep3_data_index.reset(new Episode3::DataIndex( + "system/ep3", this->ep3_behavior_flags)); + + const string& tournament_state_filename = "system/ep3/tournament-state.json"; + try { + this->ep3_tournament_index.reset(new Episode3::TournamentIndex( + this->ep3_data_index, tournament_state_filename)); + config_log.info("Loaded Episode 3 tournament state"); + } catch (const exception& e) { + config_log.warning("Cannot load Episode 3 tournament state: %s", e.what()); + this->ep3_tournament_index.reset(new Episode3::TournamentIndex( + this->ep3_data_index, tournament_state_filename, true)); + } +} + +void ServerState::load_quest_index() { + config_log.info("Collecting quest metadata"); + this->quest_index.reset(new QuestIndex("system/quests")); +} + +void ServerState::compile_functions() { + config_log.info("Compiling client functions"); + this->function_code_index.reset(new FunctionCodeIndex("system/ppc")); +} + +void ServerState::load_dol_files() { + config_log.info("Loading DOL files"); + this->dol_file_index.reset(new DOLFileIndex("system/dol")); +} diff --git a/src/ServerState.hh b/src/ServerState.hh index b24f6a17..2a758cc8 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -43,6 +43,9 @@ struct ServerState { NEVER, }; + std::string config_filename; + bool is_replay; + std::u16string name; std::unordered_map> name_to_port_config; std::unordered_map> number_to_port_config; @@ -120,7 +123,7 @@ struct ServerState { std::shared_ptr game_server; std::shared_ptr client_options_cache; - ServerState(); + ServerState(const char* config_filename, bool is_replay); void add_client_to_available_lobby(std::shared_ptr c); void remove_client_from_lobby(std::shared_ptr c); @@ -153,10 +156,22 @@ struct ServerState { void set_port_configuration( const std::vector& port_configs); - void create_menus(std::shared_ptr config_json); - std::shared_ptr load_bb_file( const std::string& patch_index_filename, const std::string& gsl_filename = "", const std::string& bb_directory_filename = "") const; + + std::shared_ptr load_config() const; + void collect_network_addresses(); + void parse_config(std::shared_ptr config_json); + void load_licenses(); + void load_patch_indexes(); + void load_battle_params(); + void load_level_table(); + void load_item_tables(); + void load_ep3_data(); + void load_quest_index(); + void compile_functions(); + void load_dol_files(); + void create_menus(std::shared_ptr config_json); };