diff --git a/CMakeLists.txt b/CMakeLists.txt index d2f94e8d..0dfc0353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,10 +27,12 @@ link_directories(${LOCAL_LIB_DIR}) find_path (LIBEVENT_INCLUDE_DIR NAMES event.h) find_library (LIBEVENT_LIBRARY NAMES event) find_library (LIBEVENT_CORE NAMES event_core) +find_library (LIBEVENT_PTHREADS NAMES event_pthreads) set (LIBEVENT_INCLUDE_DIRS ${LIBEVENT_INCLUDE_DIR}) set (LIBEVENT_LIBRARIES ${LIBEVENT_LIBRARY} - ${LIBEVENT_CORE}) + ${LIBEVENT_CORE} + ${LIBEVENT_PTHREADS}) find_package(phosg REQUIRED) find_package(Iconv REQUIRED) @@ -81,6 +83,7 @@ set(SOURCES src/Episode3/RulerServer.cc src/Episode3/Server.cc src/Episode3/Tournament.cc + src/EventUtils.cc src/FileContentsCache.cc src/FunctionCompiler.cc src/GSLArchive.cc @@ -121,9 +124,7 @@ set(SOURCES src/Server.cc src/ServerShell.cc src/ServerState.cc - src/Shell.cc src/StaticGameData.cc - src/StepGraph.cc src/TeamIndex.cc src/Text.cc src/TextIndex.cc diff --git a/src/CatSession.cc b/src/CatSession.cc index be1de755..cddd981b 100644 --- a/src/CatSession.cc +++ b/src/CatSession.cc @@ -31,21 +31,19 @@ using namespace std; +CatSession::exit_shell::exit_shell() : runtime_error("shell exited") {} + CatSession::CatSession( shared_ptr base, const struct sockaddr_storage& remote, Version version, shared_ptr bb_key_file) - : Shell(base), - log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level), - channel( - version, - 1, - CatSession::dispatch_on_channel_input, - CatSession::dispatch_on_channel_error, - this, - "CatSession"), + : log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level), + base(base), + read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST, CatSession::dispatch_read_stdin, this), event_free), + channel(version, 1, CatSession::dispatch_on_channel_input, CatSession::dispatch_on_channel_error, this, "CatSession"), bb_key_file(bb_key_file) { + if (remote.ss_family != AF_INET) { throw runtime_error("remote is not AF_INET"); } @@ -64,6 +62,14 @@ CatSession::CatSession( reinterpret_cast(&remote), sizeof(struct sockaddr_in)) != 0) { throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); } + + event_add(this->read_event.get(), nullptr); + this->poll.add(0, POLLIN); +} + +void CatSession::execute_command(const std::string& command) { + string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES); + send_command_with_header(this->channel, full_cmd.data(), full_cmd.size()); } void CatSession::dispatch_on_channel_input( @@ -129,9 +135,54 @@ void CatSession::on_channel_error(short events) { } } -void CatSession::print_prompt() {} - -void CatSession::execute_command(const std::string& command) { - string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES); - send_command_with_header(this->channel, full_cmd.data(), full_cmd.size()); +void CatSession::dispatch_read_stdin(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->read_stdin(); +} + +void CatSession::read_stdin() { + bool any_command_read = false; + for (;;) { + auto poll_result = this->poll.poll(); + short fd_events = 0; + try { + fd_events = poll_result.at(0); + } catch (const out_of_range&) { + } + + if (!(fd_events & POLLIN)) { + break; + } + + string command(2048, '\0'); + if (!fgets(command.data(), command.size(), stdin)) { + if (!any_command_read) { + // ctrl+d probably; we should exit + fputc('\n', stderr); + event_base_loopexit(this->base.get(), nullptr); + return; + } else { + break; // probably not EOF; just no more commands for now + } + } + + // trim the extra data off the string + size_t len = strlen(command.c_str()); + if (len == 0) { + break; + } + if (command[len - 1] == '\n') { + len--; + } + command.resize(len); + any_command_read = true; + + try { + execute_command(command); + } catch (const exit_shell&) { + event_base_loopexit(this->base.get(), nullptr); + return; + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + } + } } diff --git a/src/CatSession.hh b/src/CatSession.hh index d13c1611..f3bcbad8 100644 --- a/src/CatSession.hh +++ b/src/CatSession.hh @@ -14,28 +14,41 @@ #include "PSOEncryption.hh" #include "PSOProtocol.hh" #include "ServerState.hh" -#include "Shell.hh" -class CatSession : public Shell { +class CatSession { public: CatSession( std::shared_ptr base, const struct sockaddr_storage& remote, Version version, std::shared_ptr bb_key_file); + CatSession(const CatSession&) = delete; + CatSession(CatSession&&) = delete; + CatSession& operator=(const CatSession&) = delete; + CatSession& operator=(CatSession&&) = delete; virtual ~CatSession() = default; protected: PrefixedLogger log; + std::shared_ptr base; + std::unique_ptr read_event; + Poll poll; + Channel channel; std::shared_ptr bb_key_file; - virtual void print_prompt(); + class exit_shell : public std::runtime_error { + public: + exit_shell(); + ~exit_shell() = default; + }; + virtual void execute_command(const std::string& command); - static void dispatch_on_channel_input( - Channel& ch, uint16_t command, uint32_t flag, std::string& msg); + static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx); + static void dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg); static void dispatch_on_channel_error(Channel& ch, short events); void on_channel_input(uint16_t command, uint32_t flag, std::string& msg); void on_channel_error(short events); + void read_stdin(); }; diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index dcd73f88..fb975353 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -2004,6 +2004,15 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) for (size_t z = 0; z < this->dialogue_sets.size(); z++) { this->dialogue_sets[z] = map.dialogue_sets[z].sub<8>(0); } + + // TODO: It'd be nice to rewrite start_tile_definitions, since it seems NTE + // always expects team A to be represented by 3 and 4, and team B to be + // represented by 7 and 8. + + // TODO: NTE also expects team A to always be facing up, and B to always be + // facing down, so it would be nice to automatically rotate the map to make + // that the case. However, we'd also have to fix up the camera zones and + // camera specs, and the spec structure is not (yet) fully understood. } MapDefinitionTrial::operator MapDefinition() const { diff --git a/src/EventUtils.cc b/src/EventUtils.cc new file mode 100644 index 00000000..629cc423 --- /dev/null +++ b/src/EventUtils.cc @@ -0,0 +1,20 @@ +#include "EventUtils.hh" + +#include + +#include +#include +#include +#include + +static void dispatch_forward_to_event_thread(evutil_socket_t, short, void* ctx) { + auto* fn = reinterpret_cast*>(ctx); + (*fn)(); + delete fn; +} + +void forward_to_event_thread(std::shared_ptr base, std::function&& fn) { + struct timeval tv = {0, 0}; + std::function* new_fn = new std::function(std::move(fn)); + event_base_once(base.get(), -1, EV_TIMEOUT, dispatch_forward_to_event_thread, new_fn, &tv); +} diff --git a/src/EventUtils.hh b/src/EventUtils.hh new file mode 100644 index 00000000..ba0cc80c --- /dev/null +++ b/src/EventUtils.hh @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include +#include + +void forward_to_event_thread(std::shared_ptr base, std::function&& fn); diff --git a/src/Main.cc b/src/Main.cc index 44e12ec2..685d2223 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -1127,7 +1128,8 @@ Action a_disassemble_set_data_table( Action a_check_set_data_table( "check-set-data-tables", nullptr, +[](Arguments&) { ServerState s; - s.load_objects_and_upstream_dependents("set_data_tables"); + s.load_patch_indexes(false); + s.load_set_data_tables(false); static_game_data_log.min_level = LogLevel::DISABLED; auto get_file_data = [&](Version version, const string& filename) -> shared_ptr { @@ -1330,7 +1332,7 @@ Action a_assemble_all_patches( Edition). The output files are saved in system/client-functions.\n", +[](Arguments&) { ServerState s; - s.load_objects_and_upstream_dependents("functions"); + s.compile_functions(false); auto process_code = +[](shared_ptr code, uint32_t checksum_addr, @@ -1556,7 +1558,9 @@ Action a_print_word_select_table( option is given, prints the token table sorted by canonical name.\n", +[](Arguments& args) { ServerState s; - s.load_objects_and_upstream_dependents("word_select_table"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_word_select_table(false); Version v; try { v = get_cli_version(args); @@ -1613,7 +1617,10 @@ Action a_convert_rare_item_set( auto version = get_cli_version(args); ServerState s; - s.load_objects_and_upstream_dependents("item_name_indexes"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_item_definitions(false); + s.load_item_name_indexes(false); string input_filename = args.get(1, false); if (input_filename.empty() || (input_filename == "-")) { @@ -1668,7 +1675,10 @@ Action a_describe_item( auto version = get_cli_version(args); ServerState s; - s.load_objects_and_upstream_dependents("item_name_indexes"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_item_definitions(false); + s.load_item_name_indexes(false); auto name_index = s.item_name_index(version); ItemData item = name_index->parse_item_description(description); @@ -1728,7 +1738,10 @@ Action a_describe_item( Action a_name_all_items( "name-all-items", nullptr, +[](Arguments&) { ServerState s; - s.load_objects_and_upstream_dependents("item_name_indexes"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_item_definitions(false); + s.load_item_name_indexes(false); set all_primary_identifiers; for (const auto& index : s.item_name_indexes) { @@ -1773,7 +1786,10 @@ Action a_name_all_items( Action a_print_item_parameter_tables( "print-item-tables", nullptr, +[](Arguments&) { ServerState s; - s.load_objects_and_upstream_dependents("item_name_indexes"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_item_definitions(false); + s.load_item_name_indexes(false); for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { const auto& index = s.item_name_indexes.at(v_s); if (index) { @@ -1793,7 +1809,7 @@ Action a_show_ep3_cards( bool one_line = args.get("one-line"); ServerState s; - s.load_objects_and_upstream_dependents("ep3_data"); + s.load_ep3_cards(false); unique_ptr text_english; try { @@ -1843,8 +1859,9 @@ Action a_generate_ep3_cards_html( bool is_nte = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); ServerState s; - s.load_objects_and_upstream_dependents("ep3_data"); - s.load_objects_and_upstream_dependents("text_index"); + s.load_patch_indexes(false); + s.load_text_index(false); + s.load_ep3_cards(false); shared_ptr text_english; try { @@ -2007,7 +2024,8 @@ Action a_show_ep3_maps( config_log.info("Collecting Episode 3 data"); ServerState s; - s.load_objects_and_upstream_dependents("ep3_data"); + s.load_ep3_cards(false); + s.load_ep3_maps(false); auto map_ids = s.ep3_map_index->all_numbers(); log_info("%zu maps", map_ids.size()); @@ -2031,7 +2049,8 @@ Action a_show_battle_params( in a human-readable format.\n", +[](Arguments&) { ServerState s; - s.load_objects_and_upstream_dependents("battle_params"); + s.load_patch_indexes(false); + s.load_battle_params(false); fprintf(stdout, "Episode 1 multi\n"); s.battle_params->get_table(false, Episode::EP1).print(stdout); @@ -2073,7 +2092,8 @@ Action a_find_rare_enemy_seeds( ServerState s("system/config.json"); shared_ptr vq; if (!quest_name.empty()) { - s.load_objects_and_upstream_dependents("quest_index"); + s.load_config(); + s.load_quest_index(false); auto q = s.quest_index(version)->get(quest_name); if (!q) { throw runtime_error("quest does not exist"); @@ -2083,11 +2103,11 @@ Action a_find_rare_enemy_seeds( throw runtime_error("quest version does not exist"); } } else if (version == Version::BB_V4) { - s.load_objects_and_upstream_dependents("config"); + s.load_config(); } else if (version == Version::PC_V2) { - s.load_objects_and_upstream_dependents("patch_indexes"); + s.load_patch_indexes(false); } else { - s.load_objects_and_upstream_dependents("map_file_caches"); + s.clear_map_file_caches(); } shared_ptr rare_rates; @@ -2265,7 +2285,8 @@ Action a_diff_dol_files( Action a_replay_ep3_battle_commands( "replay-ep3-battle-commands", nullptr, +[](Arguments& args) { ServerState s; - s.load_objects_and_upstream_dependents("ep3_data"); + s.load_ep3_cards(false); + s.load_ep3_maps(false); auto random_crypt = make_shared(args.get("seed", 0, Arguments::IntFormat::HEX)); Episode3::Server::Options options = { @@ -2296,6 +2317,15 @@ Action a_run_server_replay_log( config_log.info("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()); } +#ifdef PHOSG_WINDOWS + int evthread_ret = evthread_use_windows_threads(); +#else + int evthread_ret = evthread_use_pthreads(); +#endif + if (evthread_ret) { + throw runtime_error("failed to setup libevent threads"); + } + if (!isdir("system/players")) { config_log.info("Players directory does not exist; creating it"); mkdir("system/players", 0755); @@ -2319,7 +2349,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->load_objects_and_downstream_dependents("all"); + state->load_all(); shared_ptr dns_server; if (state->dns_server_port && !is_replay) { @@ -2334,7 +2364,7 @@ Action a_run_server_replay_log( config_log.info("DNS server is disabled"); } - shared_ptr shell; + shared_ptr shell; shared_ptr replay_session; shared_ptr ip_stack_simulator; if (is_replay) { @@ -2423,11 +2453,12 @@ Action a_run_server_replay_log( if (should_run_shell) { should_run_shell = !replay_session.get(); } - if (should_run_shell) { - shell = make_shared(base, state); - } config_log.info("Ready"); + if (should_run_shell) { + shell = make_shared(state); + } + event_base_dispatch(base.get()); if (replay_session) { // If in a replay session, run the event loop for a bit longer to make diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 06483eec..1d67145b 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -65,8 +65,10 @@ static shared_ptr proxy_options_menu_for_client(shared_ptrversion())) { + add_flag_option(ProxyOptionsMenuItemID::SWITCH_ASSIST, Client::Flag::SWITCH_ASSIST_ENABLED, + "Switch assist", "Automatically try\nto unlock 2-player\ndoors when you step\non both switches\nsequentially"); + } if ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) || c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) { if (!is_ep3(c->version())) { add_flag_option(ProxyOptionsMenuItemID::INFINITE_HP, Client::Flag::INFINITE_HP_ENABLED, diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index c170d904..7a1466fc 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -6,7 +6,6 @@ #include "Loggers.hh" #include "Server.hh" -#include "Shell.hh" using namespace std; @@ -474,9 +473,6 @@ ReplaySession::ReplaySession( while (!feof(input_log)) { line_num++; string line = fgets(input_log); - if (starts_with(line, Shell::PROMPT)) { - line = line.substr(Shell::PROMPT.size()); - } if (ends_with(line, "\n")) { line.resize(line.size() - 1); } diff --git a/src/Server.hh b/src/Server.hh index f794f167..c8736f2d 100644 --- a/src/Server.hh +++ b/src/Server.hh @@ -30,8 +30,7 @@ public: void disconnect_client(std::shared_ptr c); std::shared_ptr get_client() const; - std::vector> get_clients_by_identifier( - const std::string& ident) const; + std::vector> get_clients_by_identifier(const std::string& ident) const; std::shared_ptr get_base() const; inline std::shared_ptr get_state() const { @@ -69,8 +68,7 @@ private: evutil_socket_t fd, struct sockaddr* address, int socklen, void* ctx); static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); - void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, - struct sockaddr* address, int socklen); + void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* address, int socklen); void on_listen_error(struct evconnlistener* listener); static void on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 2c78d803..517370da 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -14,19 +14,105 @@ using namespace std; -ServerShell::ServerShell( - shared_ptr base, - shared_ptr state) - : Shell(base), - state(state) {} +struct CommandArgs { + shared_ptr s; + shared_ptr shell; + string command; + string args; + string session_name; +}; -void ServerShell::print_prompt() { - fwritex(stdout, Shell::PROMPT); - fflush(stdout); +struct CommandDefinition; + +static std::vector commands_by_order; +static std::unordered_map commands_by_name; + +struct CommandDefinition { + + const char* name; + const char* help_text; + bool run_on_event_thread; + void (*run)(CommandArgs&); + + CommandDefinition(const char* name, const char* help_text, bool run_on_event_thread, void (*run)(CommandArgs&)) + : name(name), + help_text(help_text), + run_on_event_thread(run_on_event_thread), + run(run) { + commands_by_order.emplace_back(this); + commands_by_name.emplace(this->name, this); + } + + static void dispatch(CommandArgs& args) { + const CommandDefinition* def = nullptr; + try { + def = commands_by_name.at(args.command); + } catch (const out_of_range&) { + } + if (!def) { + fprintf(stderr, "FAILED: no such command; try 'help'\n"); + } else if (def->run_on_event_thread) { + args.s->forward_to_event_thread([def, args]() { + CommandArgs local_args = args; + try { + def->run(local_args); + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + } + }); + } else { + def->run(args); + } + } +}; + +ServerShell::exit_shell::exit_shell() : runtime_error("shell exited") {} + +ServerShell::ServerShell(shared_ptr state) + : state(state), + th(&ServerShell::thread_fn, this) {} + +ServerShell::~ServerShell() { + if (this->th.joinable()) { + this->th.join(); + } } -shared_ptr ServerShell::get_proxy_session( - const string& name) { +void ServerShell::thread_fn() { + for (;;) { + fprintf(stdout, "newserv> "); + fflush(stdout); + string command = fgets(stdin); + + // If command is empty (not even a \n), it's probably EOF + if (command.empty()) { + fputc('\n', stderr); + event_base_loopexit(this->state->base.get(), nullptr); + return; + } + + strip_trailing_whitespace(command); + + try { + // Find the entry in the command table and run the command + size_t command_end = skip_non_whitespace(command, 0); + size_t args_begin = skip_whitespace(command, command_end); + CommandArgs args; + args.s = this->state; + args.shell = this->shared_from_this(); + args.command = command.substr(0, command_end); + args.args = command.substr(args_begin); + CommandDefinition::dispatch(args); + } catch (const exit_shell&) { + event_base_loopexit(this->state->base.get(), nullptr); + return; + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + } + } +} + +shared_ptr ServerShell::get_proxy_session(const string& name) { if (!this->state->proxy_server.get()) { throw runtime_error("the proxy server is disabled"); } @@ -35,26 +121,6 @@ shared_ptr ServerShell::get_proxy_session( : this->state->proxy_server->get_session_by_name(name); } -static void set_boolean(bool* target, const string& args) { - if (args == "on") { - *target = true; - } else if (args == "off") { - *target = false; - } else { - throw invalid_argument("argument must be \"on\" or \"off\""); - } -} - -static void set_flag(Client::Config& config, Client::Flag flag, const string& args) { - if (args == "on") { - config.set_flag(flag); - } else if (args == "off") { - config.clear_flag(flag); - } else { - throw invalid_argument("argument must be \"on\" or \"off\""); - } -} - static string get_quoted_string(string& s) { string ret; char end_char = (s.at(0) == '\"') ? '\"' : ' '; @@ -81,73 +147,131 @@ static string get_quoted_string(string& s) { return ret; } -void ServerShell::execute_command(const string& command) { - // Find the entry in the command table and run the command - size_t command_end = skip_non_whitespace(command, 0); - size_t args_begin = skip_whitespace(command, command_end); - string command_name = command.substr(0, command_end); - string command_args = command.substr(args_begin); +CommandDefinition c_nop1( + "", nullptr, false, +[](CommandArgs&) {}); +CommandDefinition c_nop2( + "//", nullptr, false, +[](CommandArgs&) {}); +CommandDefinition c_nop3( + "#", nullptr, false, +[](CommandArgs&) {}); - string session_name; - if (command_name == "on") { - size_t session_name_end = skip_non_whitespace(command_args, 0); - size_t command_begin = skip_whitespace(command_args, session_name_end); - command_end = skip_non_whitespace(command_args, command_begin); - args_begin = skip_whitespace(command_args, command_end); - session_name = command_args.substr(0, session_name_end); - command_name = command_args.substr(command_begin, command_end - command_begin); - command_args = command_args.substr(args_begin); - } - - if (command_name == "" || starts_with(command_name, "//")) { - // Do nothing - - } else if (command_name == "exit") { - throw exit_shell(); - - } else if (command_name == "help") { - fprintf(stderr, "\ -General commands:\n\ - help\n\ - You\'re reading it now.\n\ - exit (or ctrl+d)\n\ - Shut down the server.\n\ - on SESSION COMMAND [ARGS...]\n\ +CommandDefinition c_help( + "help", "help\n\ + You\'re reading it now.", + false, + +[](CommandArgs&) { + fputs("Commands:\n", stderr); + for (const auto& def : commands_by_order) { + if (def->help_text) { + fprintf(stderr, " %s\n", def->help_text); + } + } + }); +CommandDefinition c_exit( + "exit", "exit (or ctrl+d)\n\ + Shut down the server.", + false, + +[](CommandArgs&) { + throw ServerShell::exit_shell(); + }); +CommandDefinition c_on( + "on", "on SESSION COMMAND [ARGS...]\n\ Run a command on a specific game server client or proxy server session.\n\ Without this prefix, commands that affect a single client or session will\n\ work only if there's exactly one connected client or open session. SESSION\n\ may be a client ID (e.g. C-3), a player name, a license serial number, or\n\ a BB account username. For proxy commands, SESSION should be the session\n\ ID, which generally is the same as the player\'s serial number and appears\n\ - after \"LinkedSession:\" in the log output.\n\ -\n\ -Server commands:\n\ - reload ITEM [ITEM...]\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\ + after \"LinkedSession:\" in the log output.", + false, + +[](CommandArgs& args) { + size_t session_name_end = skip_non_whitespace(args.args, 0); + size_t command_begin = skip_whitespace(args.args, session_name_end); + size_t command_end = skip_non_whitespace(args.args, command_begin); + size_t args_begin = skip_whitespace(args.args, command_end); + args.session_name = args.args.substr(0, session_name_end); + args.command = args.args.substr(command_begin, command_end - command_begin); + args.args = args.args.substr(args_begin); + CommandDefinition::dispatch(args); + }); + +CommandDefinition c_reload( + "reload", "reload ITEM [ITEM...]\n\ + Reload various parts of the server configuration. The items are:\n\ battle-params - reload the BB enemy stats files\n\ - bb-private-keys - reload BB private keys\n\ + bb-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\ + ep3-cards - reload Episode 3 card definitions\n\ + ep3-maps - reload Episode 3 maps (not download quests)\n\ + ep3-tournaments - reload Episode 3 tournament state\n\ functions - recompile all client-side patches and functions\n\ item-definitions - reload item definitions files\n\ + item-name-index - regenerate item name list\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\ + patch-files - reindex the PC and BB patch directories\n\ + quests - reindex all quests (including Episode3 download quests)\n\ + set-tables - reload set data tables\n\ teams - reindex all BB teams\n\ text-index - reload in-game text\n\ - word-select-table - regenerate the Word Select translation table\n\ + word-select - 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\ 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 PARAMETERS...\n\ + actually received.", + false, + +[](CommandArgs& args) { + auto types = split(args.args, ' '); + for (const auto& type : types) { + if (type == "bb-keys") { + args.s->load_bb_private_keys(true); + } else if (type == "licenses") { + args.s->load_licenses(true); + } else if (type == "patch-files") { + args.s->load_patch_indexes(true); + } else if (type == "ep3-cards") { + args.s->load_ep3_cards(true); + } else if (type == "ep3-maps") { + args.s->load_ep3_maps(true); + } else if (type == "ep3-tournaments") { + args.s->load_ep3_tournament_state(true); + } else if (type == "functions") { + args.s->compile_functions(true); + } else if (type == "dol-files") { + args.s->load_dol_files(true); + } else if (type == "set-tables") { + args.s->load_set_data_tables(true); + } else if (type == "battle-params") { + args.s->load_battle_params(true); + } else if (type == "level-table") { + args.s->load_level_table(true); + } else if (type == "text-index") { + args.s->load_text_index(true); + } else if (type == "word-select") { + args.s->load_word_select_table(true); + } else if (type == "item-definitions") { + args.s->load_item_definitions(true); + } else if (type == "item-name-index") { + args.s->load_item_name_indexes(true); + } else if (type == "drop-tables") { + args.s->load_drop_tables(true); + } else if (type == "config") { + args.s->forward_to_event_thread([s = args.s]() { s->load_config(); }); + } else if (type == "teams") { + args.s->load_teams(true); + } else if (type == "quests") { + args.s->load_quest_index(true); + } else { + throw runtime_error("invalid data type: " + type); + } + } + }); + +CommandDefinition c_add_license( + "add-license", "add-license PARAMETERS...\n\ Add a license to the server. is some subset of the following:\n\ bb-username= (BB username)\n\ bb-password= (BB password)\n\ @@ -173,219 +297,12 @@ Server commands:\n\ There are also shorthands for some general privilege levels:\n\ flags=moderator = 00000007\n\ flags=admin = 000000FF\n\ - flags=root = 7FFFFFFF\n\ - update-license SERIAL-NUMBER PARAMETERS...\n\ - Update an existing license. specifies which license to\n\ - update. The options in are the same as for the add-license\n\ - command.\n\ - delete-license SERIAL-NUMBER\n\ - Delete a license from the server.\n\ - list-licenses\n\ - List all licenses registered on the server.\n\ - set-allow-unregistered-users on|off\n\ - Enable or disable allowing unregistered users on the server. Disabling this\n\ - does not disconnect unregistered users who are already connected.\n\ - set-event EVENT\n\ - Set the event in all lobbies, and in the main menu before joining a lobby.\n\ - EVENT can be none, xmas, val, easter, hallo, sonic, newyear, summer, white,\n\ - wedding, fall, s-spring, s-summer, or spring.\n\ - set-ep3-menu-song SONG-NUM\n\ - Set the song that plays in the main menu for Episode III clients. If an\n\ - event is also set, the event's visuals appear but this song still plays.\n\ - Song numbers are 0 through 51; the default song is -1.\n\ - announce MESSAGE\n\ - Send an announcement message to all players.\n\ - create-tournament TOURNAMENT-NAME MAP-NAME NUM-TEAMS [OPTIONS...]\n\ - Create an Episode 3 tournament. Quotes are required around the tournament\n\ - and map names, unless the names contain no spaces.\n\ - OPTIONS may include:\n\ - 2v2: Set team size to 2 players (default is 1 without this option)\n\ - no-coms: Don\'t add any COM teams to the tournament bracket\n\ - shuffle: Shuffle entries when starting the tournament\n\ - resize: If the tournament is less than half full when it starts, reduce\n\ - the number of rounds to fit the existing entries\n\ - dice=A-B: Set minimum and maximum dice rolls\n\ - dice=A-B:C-D: Set minimum and maximum dice rolls for ATK dice (A-B) and\n\ - DEF dice (C-D) separately\n\ - dice=A-B:C-D:E-F: Set minimum and maximum dice rolls for ATK dice, DEF\n\ - dice, and solo vs. 2P ATK and DEF dice (E-F) separately\n\ - dice=A-B:C-D:E-F:G-H: Set minimum and maximum dice rolls for ATK dice,\n\ - DEF dice, solo vs. 2P ATK (E-F) and DEF (G-H) dice separately\n\ - overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\ - phase-time-limit=N: Set phase time limit (in seconds)\n\ - allowed-cards=ALL/N/NR/NRS: Set ranks of allowed cards\n\ - deck-shuffle=ON/OFF: Enable/disable deck shuffle\n\ - deck-loop=ON/OFF: Enable/disable deck loop\n\ - hp=N: Set Story Character initial HP\n\ - hp-type=TEAM/PLAYER/COMMON: Set team HP type\n\ - allow-assists=ON/OFF: Enable/disable assist cards\n\ - dialogue=ON/OFF: Enable/disable dialogue\n\ - dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\ - dice-boost=ON/OFF: Enable/disable dice boost\n\ - delete-tournament TOURNAMENT-NAME\n\ - Delete a tournament. Quotes are required around the tournament name unless\n\ - the name contains no spaces.\n\ - list-tournaments\n\ - List the names and numbers of all existing tournaments.\n\ - start-tournament TOURNAMENT-NAME\n\ - End registration for a tournament and allow matches to begin. Quotes are\n\ - required around the tournament name unless the name contains no spaces.\n\ - describe-tournament TOURNAMENT-NAME\n\ - Show the current state of a tournament. Quotes are required around the\n\ - tournament name unless the name contains no spaces.\n\ -\n\ -Proxy session commands:\n\ - sc DATA\n\ - Send a command to the client. This command also can be used to send data to\n\ - a client on the game server.\n\ - ss DATA\n\ - Send a command to the server.\n\ - show-slots\n\ - Show the player names, Guild Card numbers, and client IDs of all players in\n\ - the current lobby or game.\n\ - c TEXT\n\ - chat TEXT\n\ - Send a chat message to the server.\n\ - wchat DATA\n\ - Send a chat message with private_flags on Episode 3.\n\ - dchat DATA\n\ - Send a chat message to the server with arbitrary data in it.\n\ - info-board TEXT\n\ - Set your info board contents. This will affect the current session only,\n\ - and will not be saved for future sessions.\n\ - info-board-data DATA\n\ - Set your info board contents with arbitrary data. Like the above, affects\n\ - the current session only.\n\ - marker COLOR-ID\n\ - Change your lobby marker color.\n\ - warp FLOOR-ID\n\ - warpme FLOOR-ID\n\ - Send yourself to a specific floor.\n\ - warpall FLOOR-ID\n\ - Send everyone to a specific floor.\n\ - set-override-section-id [SECTION-ID]\n\ - Override the section ID for games you create or join. This affects the\n\ - active drop chart if you are the leader of the game and the server doesn't\n\ - override drops entirely. If no argument is given, clears the override.\n\ - set-override-event [EVENT]\n\ - Override the lobby event for all lobbies and games you join. This applies\n\ - only to you; other players do not see this override. If no argument is\n\ - given, clears the override.\n\ - set-override-lobby-number [NUMBER]\n\ - Override the lobby type for all lobbies you join. This applies only to you;\n\ - other players do not see this override. If no argument is given, clears the\n\ - override.\n\ - set-chat-filter on|off\n\ - Enable or disable chat filtering (enabled by default). Chat filtering\n\ - applies newserv\'s standard character replacements to chat messages; for\n\ - example, $ becomes a tab character and # becomes a newline.\n\ - set-infinite-hp on|off\n\ - set-infinite-tp on|off\n\ - Enable or disable infinite HP or TP. When infinite HP is enabled, attacks\n\ - that would kill you in one hit will still do so.\n\ - set-switch-assist on|off\n\ - Enable or disable switch assist. When switch assist is on, the server will\n\ - remember the last \"enable switch\" command that you sent, and will send it\n\ - back to you (and to the remote server, if you\'re in a proxy session) when\n\ - you step on another switch. Using this, you can unlock some doors that\n\ - require two players to stand on switches by touching both switches\n\ - yourself.\n\ - set-save-files on|off\n\ - Enable or disable saving of game files (disabled by default). When this is\n\ - on, any file that the remote server sends to the client will be saved to\n\ - the current directory. This includes data like quests, Episode 3 card\n\ - definitions, and GBA games.\n\ - set-block-function-calls [RETURN-VALUE]\n\ - Enable blocking of function calls from the server. When enabled, the proxy\n\ - responds as if the function was called (with the given return value), but\n\ - does not send the code to the client. To stop blocking function calls, omit\n\ - the return value.\n\ - create-item DATA\n\ - Create an item as if the client had run the $item command.\n\ - set-next-item DATA\n\ - Set the next item to be dropped.\n\ - close-idle-sessions\n\ - Close all proxy sessions that don\'t have a client and server connected.\n\ -"); + flags=root = 7FFFFFFF", + true, + +[](CommandArgs& args) { + auto l = args.s->license_index->create_license(); - // SERVER COMMANDS - - } else if (command_name == "reload") { - auto types = split(command_args, ' '); - if (types.empty()) { - throw invalid_argument("no data type given"); - } - for (auto& type : types) { - for (char& ch : type) { - if (ch == '-') { - ch = '_'; - } - } - } - this->state->load_objects_and_downstream_dependents(types); - - } else if (command_name == "add-license") { - auto l = this->state->license_index->create_license(); - - for (const string& token : split(command_args, ' ')) { - if (starts_with(token, "bb-username=")) { - l->bb_username = token.substr(12); - } else if (starts_with(token, "bb-password=")) { - l->bb_password = token.substr(12); - } else if (starts_with(token, "xb-gamertag=")) { - l->xb_gamertag = token.substr(12); - } else if (starts_with(token, "xb-user-id=")) { - l->xb_user_id = stoull(token.substr(11), nullptr, 16); - } else if (starts_with(token, "xb-account-id=")) { - l->xb_account_id = stoull(token.substr(14), nullptr, 16); - } else if (starts_with(token, "gc-password=")) { - l->gc_password = token.substr(12); - } else if (starts_with(token, "access-key=")) { - l->access_key = token.substr(11); - } else if (starts_with(token, "serial=")) { - l->serial_number = stoul(token.substr(7), nullptr, 0); - - } else if (starts_with(token, "flags=")) { - string mask = token.substr(6); - if (mask == "normal") { - l->flags = 0; - } else if (mask == "mod") { - l->replace_all_flags(License::Flag::MODERATOR); - } else if (mask == "admin") { - l->replace_all_flags(License::Flag::ADMINISTRATOR); - } else if (mask == "root") { - l->replace_all_flags(License::Flag::ROOT); - } else { - l->flags = stoul(mask, nullptr, 16); - } - - } else { - throw invalid_argument("incorrect field: " + token); - } - } - - if (!l->serial_number) { - throw invalid_argument("license does not contain serial number"); - } - - l->save(); - this->state->license_index->add(l); - fprintf(stderr, "license added\n"); - - } else if (command_name == "update-license") { - auto tokens = split(command_args, ' '); - if (tokens.size() < 2) { - throw runtime_error("not enough arguments"); - } - uint32_t serial_number = stoul(tokens[0]); - tokens.erase(tokens.begin()); - auto orig_l = this->state->license_index->get(serial_number); - auto l = this->state->license_index->create_license(); - *l = *orig_l; - - this->state->license_index->remove(orig_l->serial_number); - try { - for (const string& token : tokens) { + for (const string& token : split(args.args, ' ')) { if (starts_with(token, "bb-username=")) { l->bb_username = token.substr(12); } else if (starts_with(token, "bb-password=")) { @@ -425,436 +342,550 @@ Proxy session commands:\n\ if (!l->serial_number) { throw invalid_argument("license does not contain serial number"); } - } catch (const exception&) { - this->state->license_index->add(orig_l); - throw; - } - l->save(); - this->state->license_index->add(l); - fprintf(stderr, "license updated\n"); + l->save(); + args.s->license_index->add(l); + fprintf(stderr, "license added\n"); + }); - } else if (command_name == "delete-license") { - uint32_t serial_number = stoul(command_args); - auto l = this->state->license_index->get(serial_number); - l->delete_file(); - this->state->license_index->remove(l->serial_number); - fprintf(stderr, "license deleted\n"); +CommandDefinition c_update_license( + "update-license", "update-license SERIAL-NUMBER PARAMETERS...\n\ + Update an existing license. specifies which license to\n\ + update. The options in are the same as for the add-license\n\ + command.", + true, + +[](CommandArgs& args) { + auto tokens = split(args.args, ' '); + if (tokens.size() < 2) { + throw runtime_error("not enough arguments"); + } + uint32_t serial_number = stoul(tokens[0]); + tokens.erase(tokens.begin()); + auto orig_l = args.s->license_index->get(serial_number); + auto l = args.s->license_index->create_license(); + *l = *orig_l; - } else if (command_name == "list-licenses") { - for (const auto& l : this->state->license_index->all()) { - string s = l->str(); - fprintf(stderr, "%s\n", s.c_str()); - } + args.s->license_index->remove(orig_l->serial_number); + try { + for (const string& token : tokens) { + if (starts_with(token, "bb-username=")) { + l->bb_username = token.substr(12); + } else if (starts_with(token, "bb-password=")) { + l->bb_password = token.substr(12); + } else if (starts_with(token, "xb-gamertag=")) { + l->xb_gamertag = token.substr(12); + } else if (starts_with(token, "xb-user-id=")) { + l->xb_user_id = stoull(token.substr(11), nullptr, 16); + } else if (starts_with(token, "xb-account-id=")) { + l->xb_account_id = stoull(token.substr(14), nullptr, 16); + } else if (starts_with(token, "gc-password=")) { + l->gc_password = token.substr(12); + } else if (starts_with(token, "access-key=")) { + l->access_key = token.substr(11); + } else if (starts_with(token, "serial=")) { + l->serial_number = stoul(token.substr(7), nullptr, 0); - } else if (command_name == "set-allow-unregistered-users") { - set_boolean(&this->state->allow_unregistered_users, command_args); - fprintf(stderr, "unregistered users are now %s\n", this->state->allow_unregistered_users ? "allowed" : "disallowed"); - - } else if (command_name == "set-allow-pc-nte") { - set_boolean(&this->state->allow_pc_nte, command_args); - fprintf(stderr, "PC NTE is now %s\n", this->state->allow_pc_nte ? "allowed" : "disallowed"); - - } else if (command_name == "set-event") { - uint8_t event_id = event_for_name(command_args); - if (event_id == 0xFF) { - throw invalid_argument("invalid event"); - } - - this->state->pre_lobby_event = event_id; - for (const auto& l : this->state->all_lobbies()) { - l->event = event_id; - } - send_change_event(this->state, event_id); - - } else if (command_name == "set-ep3-menu-song") { - this->state->ep3_menu_song = stoul(command_args, nullptr, 0); - - } else if (command_name == "announce") { - send_text_message(this->state, command_args); - - } else if (command_name == "create-tournament") { - string name = get_quoted_string(command_args); - string map_name = get_quoted_string(command_args); - auto map = this->state->ep3_map_index->for_name(map_name); - uint32_t num_teams = stoul(get_quoted_string(command_args), nullptr, 0); - Episode3::Rules rules; - rules.set_defaults(); - uint8_t flags = Episode3::Tournament::Flag::HAS_COM_TEAMS; - if (!command_args.empty()) { - auto tokens = split(command_args, ' '); - for (auto& token : tokens) { - token = tolower(token); - if (token == "2v2") { - flags |= Episode3::Tournament::Flag::IS_2V2; - } else if (token == "no-coms") { - flags &= (~Episode3::Tournament::Flag::HAS_COM_TEAMS); - } else if (token == "shuffle") { - flags |= Episode3::Tournament::Flag::SHUFFLE_ENTRIES; - } else if (token == "resize") { - flags |= Episode3::Tournament::Flag::RESIZE_ON_START; - } else if (starts_with(token, "dice=")) { - auto parse_range_c = +[](const string& s) -> uint8_t { - auto tokens = split(s, '-'); - if (tokens.size() != 2) { - throw runtime_error("dice spec must be of the form MIN-MAX"); + } else if (starts_with(token, "flags=")) { + string mask = token.substr(6); + if (mask == "normal") { + l->flags = 0; + } else if (mask == "mod") { + l->replace_all_flags(License::Flag::MODERATOR); + } else if (mask == "admin") { + l->replace_all_flags(License::Flag::ADMINISTRATOR); + } else if (mask == "root") { + l->replace_all_flags(License::Flag::ROOT); + } else { + l->flags = stoul(mask, nullptr, 16); } - return (stoul(tokens[0]) << 4) | (stoul(tokens[1]) & 0x0F); - }; - auto parse_range_p = +[](const string& s) -> pair { - auto tokens = split(s, '-'); - if (tokens.size() != 2) { - throw runtime_error("dice spec must be of the form MIN-MAX"); - } - return make_pair(stoul(tokens[0]), stoul(tokens[1])); - }; - auto subtokens = split(token.substr(5), ':'); - if (subtokens.size() < 1) { - throw runtime_error("no dice ranges specified in dice= option"); + } else { + throw invalid_argument("incorrect field: " + token); } - auto atk_range = parse_range_p(tokens[0]); - rules.min_dice_value = atk_range.first; - rules.max_dice_value = atk_range.second; - if (subtokens.size() >= 2) { - rules.def_dice_value_range = parse_range_c(tokens[1]); - if (subtokens.size() >= 3) { - rules.atk_dice_value_range_2v1 = parse_range_c(tokens[2]); - if (subtokens.size() == 3) { - rules.def_dice_value_range_2v1 = rules.atk_dice_value_range_2v1; - } else if (subtokens.size() == 4) { - rules.def_dice_value_range_2v1 = parse_range_c(tokens[3]); + } + + if (!l->serial_number) { + throw invalid_argument("license does not contain serial number"); + } + } catch (const exception&) { + args.s->license_index->add(orig_l); + throw; + } + + l->save(); + args.s->license_index->add(l); + fprintf(stderr, "license updated\n"); + }); +CommandDefinition c_delete_license( + "delete-license", "delete-license SERIAL-NUMBER\n\ + Delete a license from the server.", + true, + +[](CommandArgs& args) { + uint32_t serial_number = stoul(args.args); + auto l = args.s->license_index->get(serial_number); + l->delete_file(); + args.s->license_index->remove(l->serial_number); + fprintf(stderr, "license deleted\n"); + }); +CommandDefinition c_list_licenses( + "list-licenses", "list-licenses\n\ + List all licenses registered on the server.", + true, + +[](CommandArgs& args) { + for (const auto& l : args.s->license_index->all()) { + string s = l->str(); + fprintf(stderr, "%s\n", s.c_str()); + } + }); + +CommandDefinition c_announce( + "announce", "announce MESSAGE\n\ + Send an announcement message to all players.", + true, + +[](CommandArgs& args) { + send_text_message(args.s, args.args); + }); + +CommandDefinition c_create_tournament( + "create-tournament", "create-tournament TOURNAMENT-NAME MAP-NAME NUM-TEAMS [OPTIONS...]\n\ + Create an Episode 3 tournament. Quotes are required around the tournament\n\ + and map names, unless the names contain no spaces.\n\ + OPTIONS may include:\n\ + 2v2: Set team size to 2 players (default is 1 without this option)\n\ + no-coms: Don\'t add any COM teams to the tournament bracket\n\ + shuffle: Shuffle entries when starting the tournament\n\ + resize: If the tournament is less than half full when it starts, reduce\n\ + the number of rounds to fit the existing entries\n\ + dice=A-B: Set minimum and maximum dice rolls\n\ + dice=A-B:C-D: Set minimum and maximum dice rolls for ATK dice (A-B) and\n\ + DEF dice (C-D) separately\n\ + dice=A-B:C-D:E-F: Set minimum and maximum dice rolls for ATK dice, DEF\n\ + dice, and solo vs. 2P ATK and DEF dice (E-F) separately\n\ + dice=A-B:C-D:E-F:G-H: Set minimum and maximum dice rolls for ATK dice,\n\ + DEF dice, solo vs. 2P ATK (E-F) and DEF (G-H) dice separately\n\ + overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\ + phase-time-limit=N: Set phase time limit (in seconds)\n\ + allowed-cards=ALL/N/NR/NRS: Set ranks of allowed cards\n\ + deck-shuffle=ON/OFF: Enable/disable deck shuffle\n\ + deck-loop=ON/OFF: Enable/disable deck loop\n\ + hp=N: Set Story Character initial HP\n\ + hp-type=TEAM/PLAYER/COMMON: Set team HP type\n\ + allow-assists=ON/OFF: Enable/disable assist cards\n\ + dialogue=ON/OFF: Enable/disable dialogue\n\ + dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\ + dice-boost=ON/OFF: Enable/disable dice boost", + true, + +[](CommandArgs& args) { + string name = get_quoted_string(args.args); + string map_name = get_quoted_string(args.args); + auto map = args.s->ep3_map_index->for_name(map_name); + uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0); + Episode3::Rules rules; + rules.set_defaults(); + uint8_t flags = Episode3::Tournament::Flag::HAS_COM_TEAMS; + if (!args.args.empty()) { + auto tokens = split(args.args, ' '); + for (auto& token : tokens) { + token = tolower(token); + if (token == "2v2") { + flags |= Episode3::Tournament::Flag::IS_2V2; + } else if (token == "no-coms") { + flags &= (~Episode3::Tournament::Flag::HAS_COM_TEAMS); + } else if (token == "shuffle") { + flags |= Episode3::Tournament::Flag::SHUFFLE_ENTRIES; + } else if (token == "resize") { + flags |= Episode3::Tournament::Flag::RESIZE_ON_START; + } else if (starts_with(token, "dice=")) { + auto parse_range_c = +[](const string& s) -> uint8_t { + auto tokens = split(s, '-'); + if (tokens.size() != 2) { + throw runtime_error("dice spec must be of the form MIN-MAX"); + } + return (stoul(tokens[0]) << 4) | (stoul(tokens[1]) & 0x0F); + }; + auto parse_range_p = +[](const string& s) -> pair { + auto tokens = split(s, '-'); + if (tokens.size() != 2) { + throw runtime_error("dice spec must be of the form MIN-MAX"); + } + return make_pair(stoul(tokens[0]), stoul(tokens[1])); + }; + + auto subtokens = split(token.substr(5), ':'); + if (subtokens.size() < 1) { + throw runtime_error("no dice ranges specified in dice= option"); + } + auto atk_range = parse_range_p(tokens[0]); + rules.min_dice_value = atk_range.first; + rules.max_dice_value = atk_range.second; + if (subtokens.size() >= 2) { + rules.def_dice_value_range = parse_range_c(tokens[1]); + if (subtokens.size() >= 3) { + rules.atk_dice_value_range_2v1 = parse_range_c(tokens[2]); + if (subtokens.size() == 3) { + rules.def_dice_value_range_2v1 = rules.atk_dice_value_range_2v1; + } else if (subtokens.size() == 4) { + rules.def_dice_value_range_2v1 = parse_range_c(tokens[3]); + } else { + throw runtime_error("too many range specs given"); + } } else { - throw runtime_error("too many range specs given"); + rules.atk_dice_value_range_2v1 = 0; + rules.def_dice_value_range_2v1 = 0; } } else { + rules.def_dice_value_range = 0; rules.atk_dice_value_range_2v1 = 0; rules.def_dice_value_range_2v1 = 0; } + } else if (starts_with(token, "overall-time-limit=")) { + uint32_t limit = stoul(token.substr(19)); + if (limit > 600) { + throw runtime_error("overall-time-limit must be 600 or fewer minutes"); + } + if (limit % 5) { + throw runtime_error("overall-time-limit must be a multiple of 5 minutes"); + } + rules.overall_time_limit = limit; + } else if (starts_with(token, "phase-time-limit=")) { + rules.phase_time_limit = stoul(token.substr(17)); + } else if (starts_with(token, "hp=")) { + rules.char_hp = stoul(token.substr(3)); + } else if (token == "allowed-cards=all") { + rules.allowed_cards = Episode3::AllowedCards::ALL; + } else if (token == "allowed-cards=n") { + rules.allowed_cards = Episode3::AllowedCards::N_ONLY; + } else if (token == "allowed-cards=nr") { + rules.allowed_cards = Episode3::AllowedCards::N_R_ONLY; + } else if (token == "allowed-cards=nrs") { + rules.allowed_cards = Episode3::AllowedCards::N_R_S_ONLY; + } else if (token == "deck-shuffle=on") { + rules.disable_deck_shuffle = 0; + } else if (token == "deck-shuffle=off") { + rules.disable_deck_shuffle = 1; + } else if (token == "deck-loop=on") { + rules.disable_deck_loop = 0; + } else if (token == "deck-loop=off") { + rules.disable_deck_loop = 1; + } else if (token == "allow-assists=on") { + rules.no_assist_cards = 0; + } else if (token == "allow-assists=off") { + rules.no_assist_cards = 1; + } else if (token == "dialogue=on") { + rules.disable_dialogue = 0; + } else if (token == "dialogue=off") { + rules.disable_dialogue = 1; + } else if (token == "dice-boost=on") { + rules.disable_dice_boost = 0; + } else if (token == "dice-boost=off") { + rules.disable_dice_boost = 1; + } else if (token == "hp-type=player") { + rules.hp_type = Episode3::HPType::DEFEAT_PLAYER; + } else if (token == "hp-type=team") { + rules.hp_type = Episode3::HPType::DEFEAT_TEAM; + } else if (token == "hp-type=common") { + rules.hp_type = Episode3::HPType::COMMON_HP; + } else if (token == "dice-exchange=atk") { + rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_ATK; + } else if (token == "dice-exchange=def") { + rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_DEF; + } else if (token == "dice-exchange=none") { + rules.dice_exchange_mode = Episode3::DiceExchangeMode::NONE; } else { - rules.def_dice_value_range = 0; - rules.atk_dice_value_range_2v1 = 0; - rules.def_dice_value_range_2v1 = 0; + throw runtime_error("invalid rules option: " + token); } - } else if (starts_with(token, "overall-time-limit=")) { - uint32_t limit = stoul(token.substr(19)); - if (limit > 600) { - throw runtime_error("overall-time-limit must be 600 or fewer minutes"); - } - if (limit % 5) { - throw runtime_error("overall-time-limit must be a multiple of 5 minutes"); - } - rules.overall_time_limit = limit; - } else if (starts_with(token, "phase-time-limit=")) { - rules.phase_time_limit = stoul(token.substr(17)); - } else if (starts_with(token, "hp=")) { - rules.char_hp = stoul(token.substr(3)); - } else if (token == "allowed-cards=all") { - rules.allowed_cards = Episode3::AllowedCards::ALL; - } else if (token == "allowed-cards=n") { - rules.allowed_cards = Episode3::AllowedCards::N_ONLY; - } else if (token == "allowed-cards=nr") { - rules.allowed_cards = Episode3::AllowedCards::N_R_ONLY; - } else if (token == "allowed-cards=nrs") { - rules.allowed_cards = Episode3::AllowedCards::N_R_S_ONLY; - } else if (token == "deck-shuffle=on") { - rules.disable_deck_shuffle = 0; - } else if (token == "deck-shuffle=off") { - rules.disable_deck_shuffle = 1; - } else if (token == "deck-loop=on") { - rules.disable_deck_loop = 0; - } else if (token == "deck-loop=off") { - rules.disable_deck_loop = 1; - } else if (token == "allow-assists=on") { - rules.no_assist_cards = 0; - } else if (token == "allow-assists=off") { - rules.no_assist_cards = 1; - } else if (token == "dialogue=on") { - rules.disable_dialogue = 0; - } else if (token == "dialogue=off") { - rules.disable_dialogue = 1; - } else if (token == "dice-boost=on") { - rules.disable_dice_boost = 0; - } else if (token == "dice-boost=off") { - rules.disable_dice_boost = 1; - } else if (token == "hp-type=player") { - rules.hp_type = Episode3::HPType::DEFEAT_PLAYER; - } else if (token == "hp-type=team") { - rules.hp_type = Episode3::HPType::DEFEAT_TEAM; - } else if (token == "hp-type=common") { - rules.hp_type = Episode3::HPType::COMMON_HP; - } else if (token == "dice-exchange=atk") { - rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_ATK; - } else if (token == "dice-exchange=def") { - rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_DEF; - } else if (token == "dice-exchange=none") { - rules.dice_exchange_mode = Episode3::DiceExchangeMode::NONE; - } else { - throw runtime_error("invalid rules option: " + token); } } - } - if (rules.check_and_reset_invalid_fields()) { - fprintf(stderr, "warning: some rules were invalid and reset to defaults\n"); - } - auto tourn = this->state->ep3_tournament_index->create_tournament( - name, map, rules, num_teams, flags); - fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str()); + if (rules.check_and_reset_invalid_fields()) { + fprintf(stderr, "warning: some rules were invalid and reset to defaults\n"); + } + auto tourn = args.s->ep3_tournament_index->create_tournament(name, map, rules, num_teams, flags); + fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str()); + }); - } else if (command_name == "delete-tournament") { - string name = get_quoted_string(command_args); - if (this->state->ep3_tournament_index->delete_tournament(name)) { - fprintf(stderr, "tournament deleted\n"); - } else { - fprintf(stderr, "no such tournament exists\n"); - } - - } else if (command_name == "list-tournaments") { - for (const auto& it : this->state->ep3_tournament_index->all_tournaments()) { - fprintf(stderr, " %s\n", it.second->get_name().c_str()); - } - - } else if (command_name == "start-tournament") { - string name = get_quoted_string(command_args); - auto tourn = this->state->ep3_tournament_index->get_tournament(name); - if (tourn) { - tourn->start(); - this->state->ep3_tournament_index->save(); - tourn->send_all_state_updates(); - send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str()); - fprintf(stderr, "tournament started\n"); - } else { - fprintf(stderr, "no such tournament exists\n"); - } - - } else if (command_name == "describe-tournament") { - string name = get_quoted_string(command_args); - auto tourn = this->state->ep3_tournament_index->get_tournament(name); - if (tourn) { - tourn->print_bracket(stderr); - } else { - fprintf(stderr, "no such tournament exists\n"); - } - - // PROXY COMMANDS - - } else if ((command_name == "sc") || (command_name == "ss")) { - string data = parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES); - if (data.size() == 0) { - throw invalid_argument("no data given"); - } - data.resize((data.size() + 3) & (~3)); - - shared_ptr ses; - try { - ses = this->get_proxy_session(session_name); - } catch (const exception&) { - } - - if (ses.get()) { - if (command_name[1] == 's') { - ses->server_channel.send(data); +CommandDefinition c_delete_tournament( + "delete-tournament", "delete-tournament TOURNAMENT-NAME\n\ + Delete a tournament. Quotes are required around the tournament name unless\n\ + the name contains no spaces.", + true, + +[](CommandArgs& args) { + string name = get_quoted_string(args.args); + if (args.s->ep3_tournament_index->delete_tournament(name)) { + fprintf(stderr, "tournament deleted\n"); } else { - ses->client_channel.send(data); + fprintf(stderr, "no such tournament exists\n"); } - - } else { - shared_ptr c; - if (session_name.empty()) { - c = this->state->game_server->get_client(); + }); +CommandDefinition c_list_tournaments( + "list-tournaments", "list-tournaments\n\ + List the names and numbers of all existing tournaments.", + true, + +[](CommandArgs& args) { + for (const auto& it : args.s->ep3_tournament_index->all_tournaments()) { + fprintf(stderr, " %s\n", it.second->get_name().c_str()); + } + }); +CommandDefinition c_start_tournament( + "start-tournament", "start-tournament TOURNAMENT-NAME\n\ + End registration for a tournament and allow matches to begin. Quotes are\n\ + required around the tournament name unless the name contains no spaces.", + true, + +[](CommandArgs& args) { + string name = get_quoted_string(args.args); + auto tourn = args.s->ep3_tournament_index->get_tournament(name); + if (tourn) { + tourn->start(); + args.s->ep3_tournament_index->save(); + tourn->send_all_state_updates(); + send_ep3_text_message_printf(args.s, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str()); + fprintf(stderr, "tournament started\n"); } else { - auto clients = this->state->game_server->get_clients_by_identifier(session_name); - if (clients.empty()) { - throw runtime_error("no such client"); - } - if (clients.size() > 1) { - throw runtime_error("multiple clients found"); - } - c = std::move(clients[0]); + fprintf(stderr, "no such tournament exists\n"); } - - if (c) { - if (command_name[1] == 's') { - on_command_with_header(c, data); - } else { - send_command_with_header(c->channel, data.data(), data.size()); - } + }); +CommandDefinition c_describe_tournament( + "describe-tournament", "describe-tournament TOURNAMENT-NAME\n\ + Show the current state of a tournament. Quotes are required around the\n\ + tournament name unless the name contains no spaces.", + true, + +[](CommandArgs& args) { + string name = get_quoted_string(args.args); + auto tourn = args.s->ep3_tournament_index->get_tournament(name); + if (tourn) { + tourn->print_bracket(stderr); } else { - throw runtime_error("no client available"); + fprintf(stderr, "no such tournament exists\n"); } - } + }); - } else if (command_name == "show-slots") { - auto ses = this->get_proxy_session(session_name); - for (size_t z = 0; z < ses->lobby_players.size(); z++) { - const auto& player = ses->lobby_players[z]; - if (player.guild_card_number) { - fprintf(stderr, " %zu: %" PRIu32 " => %s (%c, %s, %s)\n", - z, player.guild_card_number, player.name.c_str(), - char_for_language_code(player.language), - name_for_char_class(player.char_class), - name_for_section_id(player.section_id)); - } else { - fprintf(stderr, " %zu: (no player)\n", z); - } - } +void f_sc_ss(CommandArgs& args) { + string data = parse_data_string(args.args, nullptr, ParseDataFlags::ALLOW_FILES); + if (data.size() == 0) { + throw invalid_argument("no data given"); + } + data.resize((data.size() + 3) & (~3)); - } else if ((command_name == "c") || (command_name == "chat") || (command_name == "dchat")) { - auto ses = this->get_proxy_session(session_name); - bool is_dchat = (command_name == "dchat"); + shared_ptr ses; + try { + ses = args.shell->get_proxy_session(args.session_name); + } catch (const exception&) { + } - if (!is_dchat && uses_utf16(ses->version())) { - send_chat_message_from_client(ses->server_channel, command_args, 0); + if (ses.get()) { + if (args.command[1] == 's') { + ses->server_channel.send(data); } else { - string data(8, '\0'); - data.push_back('\x09'); - data.push_back('E'); - if (is_dchat) { - data += parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES); - } else { - data += command_args; - data.push_back('\0'); - } - data.resize((data.size() + 3) & (~3)); - ses->server_channel.send(0x06, 0x00, data); + ses->client_channel.send(data); } - } else if ((command_name == "wc") || (command_name == "wchat")) { - auto ses = this->get_proxy_session(session_name); - if (!is_ep3(ses->version())) { - throw runtime_error("wchat can only be used on Episode 3"); - } - string data(8, '\0'); - data.push_back('\x40'); // private_flags: visible to all - data.push_back('\x09'); - data.push_back('E'); - data += command_args; - data.push_back('\0'); - data.resize((data.size() + 3) & (~3)); - ses->server_channel.send(0x06, 0x00, data); - - } else if (command_name == "marker") { - auto ses = this->get_proxy_session(session_name); - ses->server_channel.send(0x89, stoul(command_args)); - - } else if ((command_name == "warp") || (command_name == "warpme")) { - auto ses = this->get_proxy_session(session_name); - - uint8_t floor = stoul(command_args); - send_warp(ses->client_channel, ses->lobby_client_id, floor, true); - - } else if (command_name == "warpall") { - auto ses = this->get_proxy_session(session_name); - - uint8_t floor = stoul(command_args); - send_warp(ses->client_channel, ses->lobby_client_id, floor, false); - send_warp(ses->server_channel, ses->lobby_client_id, floor, false); - - } else if ((command_name == "info-board") || (command_name == "info-board-data")) { - auto ses = this->get_proxy_session(session_name); - - string data; - if (command_name == "info-board-data") { - data += parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES); - } else { - data += command_args; - } - data.push_back('\0'); - data.resize((data.size() + 3) & (~3)); - - ses->server_channel.send(0xD9, 0x00, data); - - } else if (command_name == "set-override-section-id") { - auto ses = this->get_proxy_session(session_name); - if (command_args.empty()) { - ses->config.override_section_id = 0xFF; - } else { - ses->config.override_section_id = section_id_for_name(command_args); - } - - } else if (command_name == "set-override-event") { - auto ses = this->get_proxy_session(session_name); - if (command_args.empty()) { - ses->config.override_lobby_event = 0xFF; - } else { - ses->config.override_lobby_event = event_for_name(command_args); - if (!is_v1_or_v2(ses->version())) { - ses->client_channel.send(0xDA, ses->config.override_lobby_event); - } - } - - } else if (command_name == "set-override-lobby-number") { - auto ses = this->get_proxy_session(session_name); - if (command_args.empty()) { - ses->config.override_lobby_number = 0x80; - } else { - ses->config.override_lobby_number = lobby_type_for_name(command_args); - } - - } else if (command_name == "set-challenge-rank-title") { - auto ses = this->get_proxy_session(session_name); - ses->challenge_rank_title_override = command_args; - - } else if (command_name == "set-challenge-rank-color") { - auto ses = this->get_proxy_session(session_name); - ses->challenge_rank_color_override = stoul(command_args, nullptr, 16); - - } else if (command_name == "set-infinite-hp") { - auto ses = this->get_proxy_session(session_name); - set_flag(ses->config, Client::Flag::INFINITE_HP_ENABLED, command_args); - - } else if (command_name == "set-infinite-tp") { - auto ses = this->get_proxy_session(session_name); - set_flag(ses->config, Client::Flag::INFINITE_TP_ENABLED, command_args); - - } else if (command_name == "set-switch-assist") { - auto ses = this->get_proxy_session(session_name); - set_flag(ses->config, Client::Flag::SWITCH_ASSIST_ENABLED, command_args); - - } else if (command_name == "set-save-files" && this->state->proxy_allow_save_files) { - auto ses = this->get_proxy_session(session_name); - set_flag(ses->config, Client::Flag::PROXY_SAVE_FILES, command_args); - - } else if (command_name == "set-block-function-calls") { - auto ses = this->get_proxy_session(session_name); - set_flag(ses->config, Client::Flag::PROXY_BLOCK_FUNCTION_CALLS, command_args); - - } else if ((command_name == "create-item") || (command_name == "set-next-item")) { - auto ses = this->get_proxy_session(session_name); - - if (ses->version() == Version::BB_V4) { - throw runtime_error("proxy session is BB"); - } - if (!ses->is_in_game) { - throw runtime_error("proxy session is not in a game"); - } - if (ses->lobby_client_id != ses->leader_client_id) { - throw runtime_error("proxy session is not game leader"); - } - - auto s = ses->require_server_state(); - ItemData item = s->parse_item_description(ses->version(), command_args); - item.id = random_object() | 0x80000000; - - if (command_name == "set-next-item") { - ses->next_drop_item = item; - - string name = s->describe_item(ses->version(), ses->next_drop_item, true); - send_text_message(ses->client_channel, "$C7Next drop:\n" + name); - - } else { - send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z); - send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z); - - string name = s->describe_item(ses->version(), ses->next_drop_item, true); - send_text_message(ses->client_channel, "$C7Item created:\n" + name); - } - - } else if (command_name == "close-idle-sessions") { - size_t count = this->state->proxy_server->delete_disconnected_sessions(); - fprintf(stderr, "%zu sessions closed\n", count); - } else { - throw invalid_argument("unknown command; try \'help\'"); + shared_ptr c; + if (args.session_name.empty()) { + c = args.s->game_server->get_client(); + } else { + auto clients = args.s->game_server->get_clients_by_identifier(args.session_name); + if (clients.empty()) { + throw runtime_error("no such client"); + } + if (clients.size() > 1) { + throw runtime_error("multiple clients found"); + } + c = std::move(clients[0]); + } + + if (c) { + if (args.command[1] == 's') { + on_command_with_header(c, data); + } else { + send_command_with_header(c->channel, data.data(), data.size()); + } + } else { + throw runtime_error("no client available"); + } } } + +CommandDefinition c_sc("sc", "sc DATA\n\ + Send a command to the client. This command also can be used to send data to\n\ + a client on the game server.", + true, f_sc_ss); +CommandDefinition c_ss("ss", "ss DATA\n\ + Send a command to the remote server.", + true, f_sc_ss); + +CommandDefinition c_show_slots( + "show-slots", "show-slots\n\ + Show the player names, Guild Card numbers, and client IDs of all players in\n\ + the current lobby or game.", + true, + +[](CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + for (size_t z = 0; z < ses->lobby_players.size(); z++) { + const auto& player = ses->lobby_players[z]; + if (player.guild_card_number) { + fprintf(stderr, " %zu: %" PRIu32 " => %s (%c, %s, %s)\n", + z, player.guild_card_number, player.name.c_str(), + char_for_language_code(player.language), + name_for_char_class(player.char_class), + name_for_section_id(player.section_id)); + } else { + fprintf(stderr, " %zu: (no player)\n", z); + } + } + }); + +void fn_chat(CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + bool is_dchat = (args.command == "dchat"); + + if (!is_dchat && uses_utf16(ses->version())) { + send_chat_message_from_client(ses->server_channel, args.args, 0); + } else { + string data(8, '\0'); + data.push_back('\x09'); + data.push_back('E'); + if (is_dchat) { + data += parse_data_string(args.args, nullptr, ParseDataFlags::ALLOW_FILES); + } else { + data += args.args; + data.push_back('\0'); + } + data.resize((data.size() + 3) & (~3)); + ses->server_channel.send(0x06, 0x00, data); + } +} +CommandDefinition c_c("c", "c TEXT", true, fn_chat); +CommandDefinition c_chat("chat", "chat TEXT\n\ + Send a chat message to the server.", + true, fn_chat); +CommandDefinition c_dchat("dchat", "dchat DATA\n\ + Send a chat message to the server with arbitrary data in it.", + true, fn_chat); + +void fn_wchat(CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + if (!is_ep3(ses->version())) { + throw runtime_error("wchat can only be used on Episode 3"); + } + string data(8, '\0'); + data.push_back('\x40'); // private_flags: visible to all + data.push_back('\x09'); + data.push_back('E'); + data += args.args; + data.push_back('\0'); + data.resize((data.size() + 3) & (~3)); + ses->server_channel.send(0x06, 0x00, data); +} +CommandDefinition c_wc("wc", "wc TEXT", true, fn_wchat); +CommandDefinition c_wchat("wchat", "wchat TEXT\n\ + Send a chat message with private_flags on Episode 3.", + true, fn_wchat); + +CommandDefinition c_marker( + "marker", "marker COLOR-ID\n\ + Change your lobby marker color.", + true, +[](CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + ses->server_channel.send(0x89, stoul(args.args)); + }); + +void fn_warp(CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + + uint8_t floor = stoul(args.args); + send_warp(ses->client_channel, ses->lobby_client_id, floor, true); + if (args.command == "warpall") { + send_warp(ses->server_channel, ses->lobby_client_id, floor, false); + } +} +CommandDefinition c_warp("warp", "warp FLOOR-ID", true, fn_warp); +CommandDefinition c_warpme("warpme", "warpme FLOOR-ID\n\ + Send yourself to a specific floor.", + true, fn_warp); +CommandDefinition c_warpall("warpall", "warpall FLOOR-ID\n\ + Send everyone to a specific floor.", + true, fn_warp); + +void fn_info_board(CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + + string data; + if (args.command == "info-board-data") { + data += parse_data_string(args.args, nullptr, ParseDataFlags::ALLOW_FILES); + } else { + data += args.args; + } + data.push_back('\0'); + data.resize((data.size() + 3) & (~3)); + + ses->server_channel.send(0xD9, 0x00, data); +} +CommandDefinition c_info_board("info-board", "info-board TEXT\n\ + Set your info board contents. This will affect the current session only,\n\ + and will not be saved for future sessions.", + true, fn_info_board); +CommandDefinition c_info_board_data("info-board-data", "info-board-data DATA\n\ + Set your info board contents with arbitrary data. Like the above, affects\n\ + the current session only.", + true, fn_info_board); + +CommandDefinition c_set_challenge_rank_title( + "set-challenge-rank-title", "set-challenge-rank-title TEXT\n\ + Set the player\'s override Challenge rank text.", + true, +[](CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + ses->challenge_rank_title_override = args.args; + }); +CommandDefinition c_set_challenge_rank_color( + "set-challenge-rank-color", "set-challenge-rank-color RRGGBBAA\n\ + Set the player\'s override Challenge rank color.", + true, +[](CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + ses->challenge_rank_color_override = stoul(args.args, nullptr, 16); + }); + +void fn_create_item(CommandArgs& args) { + auto ses = args.shell->get_proxy_session(args.session_name); + + if (ses->version() == Version::BB_V4) { + throw runtime_error("proxy session is BB"); + } + if (!ses->is_in_game) { + throw runtime_error("proxy session is not in a game"); + } + if (ses->lobby_client_id != ses->leader_client_id) { + throw runtime_error("proxy session is not game leader"); + } + + auto s = ses->require_server_state(); + ItemData item = s->parse_item_description(ses->version(), args.args); + item.id = random_object() | 0x80000000; + + if (args.command == "set-next-item") { + ses->next_drop_item = item; + + string name = s->describe_item(ses->version(), ses->next_drop_item, true); + send_text_message(ses->client_channel, "$C7Next drop:\n" + name); + + } else { + send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z); + send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z); + + string name = s->describe_item(ses->version(), ses->next_drop_item, true); + send_text_message(ses->client_channel, "$C7Item created:\n" + name); + } +} +CommandDefinition c_create_item("create-item", "create-item DATA\n\ + Create an item as if the client had run the $item command.", + true, fn_create_item); +CommandDefinition c_set_next_item("set-next-item", "set-next-item DATA\n\ + Set the next item to be dropped.", + true, fn_create_item); + +CommandDefinition c_close_idle_sessions( + "close-idle-sessions", "close-idle-sessions\n\ + Close all proxy sessions that don\'t have a client and server connected.", + true, +[](CommandArgs& args) { + size_t count = args.s->proxy_server->delete_disconnected_sessions(); + fprintf(stderr, "%zu sessions closed\n", count); + }); diff --git a/src/ServerShell.hh b/src/ServerShell.hh index e423c945..a9ebf93a 100644 --- a/src/ServerShell.hh +++ b/src/ServerShell.hh @@ -6,27 +6,28 @@ #include #include "ProxyServer.hh" -#include "Shell.hh" +#include "ServerState.hh" -#define SHELL_PROMPT "newserv> " - -class ServerShell : public Shell { +class ServerShell : public std::enable_shared_from_this { public: - ServerShell( - std::shared_ptr base, - std::shared_ptr state); - virtual ~ServerShell() = default; + class exit_shell : public std::runtime_error { + public: + exit_shell(); + ~exit_shell() = default; + }; + + explicit ServerShell(std::shared_ptr state); ServerShell(const ServerShell&) = delete; ServerShell(ServerShell&&) = delete; ServerShell& operator=(const ServerShell&) = delete; ServerShell& operator=(ServerShell&&) = delete; + ~ServerShell(); + + std::shared_ptr get_proxy_session(const std::string& name); protected: std::shared_ptr state; + std::thread th; - std::shared_ptr get_proxy_session( - const std::string& name); - - virtual void print_prompt(); - virtual void execute_command(const std::string& command); + void thread_fn(); }; diff --git a/src/ServerState.cc b/src/ServerState.cc index 975354a2..de87473e 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -7,6 +7,7 @@ #include #include "Compression.hh" +#include "EventUtils.hh" #include "FileContentsCache.hh" #include "GVMEncoder.hh" #include "IPStackSimulator.hh" @@ -32,9 +33,7 @@ ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr< ServerState::ServerState(const string& config_filename) : creation_time(now()), - config_filename(config_filename) { - this->create_load_step_graph(); -} + config_filename(config_filename) {} ServerState::ServerState(shared_ptr base, const string& config_filename, bool is_replay) : creation_time(now()), @@ -42,25 +41,7 @@ ServerState::ServerState(shared_ptr base, const string& confi config_filename(config_filename), is_replay(is_replay), 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::load_objects_and_downstream_dependents(const std::string& what) { - this->load_step_graph.run(what, false); -} - -void ServerState::load_objects_and_downstream_dependents(const std::vector& what) { - this->load_step_graph.run(what, false); -} - -void ServerState::load_objects_and_upstream_dependents(const std::string& what) { - this->load_step_graph.run(what, true); -} - -void ServerState::load_objects_and_upstream_dependents(const std::vector& what) { - this->load_step_graph.run(what, true); -} + destroy_lobbies_event(this->base ? event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this) : nullptr, event_free) {} void ServerState::add_client_to_available_lobby(shared_ptr c) { shared_ptr added_to_lobby; @@ -404,10 +385,6 @@ shared_ptr ServerState::item_parameter_table_for_encod return this->item_parameter_table(is_v1(version) ? Version::PC_V2 : version); } -void ServerState::set_item_parameter_table(Version version, shared_ptr table) { - this->item_parameter_tables.at(static_cast(version)) = table; -} - shared_ptr ServerState::item_name_index(Version version) const { auto ret = this->item_name_indexes.at(static_cast(version)); if (ret == nullptr) { @@ -416,10 +393,6 @@ shared_ptr ServerState::item_name_index(Version version) co return ret; } -void ServerState::set_item_name_index(Version version, shared_ptr new_index) { - this->item_name_indexes.at(static_cast(version)) = new_index; -} - string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const { return this->item_name_index(version)->describe_item(item, include_color_codes); } @@ -784,28 +757,32 @@ void ServerState::load_config() { for (auto& trap_card_ids : this->ep3_trap_card_ids) { trap_card_ids.clear(); } - 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"); - } - 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())); + if (this->ep3_card_index) { + 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"); + } + 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())); } - 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())); } } } + } catch (const out_of_range&) { } - } catch (const out_of_range&) { + } else { + config_log.warning("Episode 3 card definitions missing; cannot set trap card IDs from config"); } if (!this->is_replay) { @@ -877,42 +854,46 @@ void ServerState::load_config() { } this->quest_F95E_results.clear(); - try { - 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()) { - difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); - } - } - } - } catch (const out_of_range&) { - } this->quest_F95F_results.clear(); - try { - for (const auto& it : json.get_list("QuestF95FResultItems")) { - auto& list = it->as_list(); - size_t price = list.at(0)->as_int(); - this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); - } - } catch (const out_of_range&) { - } this->quest_F960_success_results.clear(); this->quest_F960_failure_results = QuestF960Result(); - try { - this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4)); - for (const auto& it : json.get_list("QuestF960SuccessResultItems")) { - this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4)); - } - } catch (const out_of_range&) { - } this->secret_lottery_results.clear(); - try { - for (const auto& it : json.get_list("SecretLotteryResultItems")) { - this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string())); + if (this->item_name_index(Version::BB_V4)) { + try { + 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()) { + difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); + } + } + } + } catch (const out_of_range&) { } - } catch (const out_of_range&) { + try { + for (const auto& it : json.get_list("QuestF95FResultItems")) { + auto& list = it->as_list(); + size_t price = list.at(0)->as_int(); + this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); + } + } catch (const out_of_range&) { + } + try { + this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4)); + for (const auto& it : json.get_list("QuestF960SuccessResultItems")) { + this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4)); + } + } catch (const out_of_range&) { + } + try { + for (const auto& it : json.get_list("SecretLotteryResultItems")) { + this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string())); + } + } catch (const out_of_range&) { + } + } else { + config_log.warning("BB item name index is missing; cannot load quest reward lists from config"); } this->bb_global_exp_multiplier = json.get_int("BBGlobalEXPMultiplier", 1); @@ -958,6 +939,7 @@ void ServerState::load_config() { const auto& l = this->find_lobby(z + 1); if (l && l->check_flag(Lobby::Flag::DEFAULT)) { l->event = event; + send_change_event(l, l->event); } } } catch (const out_of_range&) { @@ -1133,41 +1115,61 @@ void ServerState::load_config() { this->config_loaded = true; } -void ServerState::load_bb_private_keys() { +void ServerState::load_bb_private_keys(bool from_non_event_thread) { + std::vector> new_keys; for (const string& filename : list_directory("system/blueburst/keys")) { if (!ends_with(filename, ".nsk")) { continue; } - this->bb_private_keys.emplace_back(make_shared( + new_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()); + + auto set = [s = this->shared_from_this(), new_keys = std::move(new_keys)]() { + s->bb_private_keys = std::move(new_keys); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_licenses() { +void ServerState::load_licenses(bool from_non_event_thread) { config_log.info("Indexing licenses"); - this->license_index = this->is_replay ? make_shared() : make_shared(); + shared_ptr new_index = this->is_replay ? make_shared() : make_shared(); + + auto set = [s = this->shared_from_this(), new_index = std::move(new_index)]() { + s->license_index = std::move(new_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_teams() { +void ServerState::load_teams(bool from_non_event_thread) { config_log.info("Indexing teams"); - this->team_index = make_shared("system/teams", this->team_reward_defs_json); + shared_ptr new_index = make_shared("system/teams", this->team_reward_defs_json); + + auto set = [s = this->shared_from_this(), new_index = std::move(new_index)]() { + s->team_index = std::move(new_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_patch_indexes() { +void ServerState::load_patch_indexes(bool from_non_event_thread) { + shared_ptr bb_data_gsl; + shared_ptr pc_patch_file_index; + shared_ptr bb_patch_file_index; + if (isdir("system/patch-pc")) { config_log.info("Indexing PSO PC patch files"); - this->pc_patch_file_index = make_shared("system/patch-pc"); + 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"); + 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); + auto gsl_file = bb_patch_file_index->get("./data/data.gsl"); + 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"); @@ -1175,6 +1177,16 @@ void ServerState::load_patch_indexes() { } else { config_log.info("PSO BB patch files not present"); } + + auto set = [s = this->shared_from_this(), + bb_data_gsl = std::move(bb_data_gsl), + pc_patch_file_index = std::move(pc_patch_file_index), + bb_patch_file_index = std::move(bb_patch_file_index)]() { + s->bb_data_gsl = std::move(bb_data_gsl); + s->pc_patch_file_index = std::move(pc_patch_file_index); + s->bb_patch_file_index = std::move(bb_patch_file_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } void ServerState::clear_map_file_caches() { @@ -1184,21 +1196,25 @@ void ServerState::clear_map_file_caches() { } } -void ServerState::load_set_data_tables() { +void ServerState::load_set_data_tables(bool from_non_event_thread) { config_log.info("Loading set data tables"); - std::array, NUM_VERSIONS> set_data_tables; - auto load_table = [this](Version version) -> void { + std::array, NUM_VERSIONS> new_tables; + std::array, NUM_VERSIONS> new_tables_ep1_ult; + std::shared_ptr new_table_bb_solo; + std::shared_ptr new_table_bb_solo_ep1_ult; + + auto load_table = [&](Version version) -> void { auto data = this->load_map_file(version, "SetDataTableOn.rel"); - this->set_data_tables[static_cast(version)] = make_shared(version, *data); + new_tables[static_cast(version)] = make_shared(version, *data); if (!is_v1(version) && (version != Version::PC_NTE)) { auto data_ep1_ult = this->load_map_file(version, "SetDataTableOnUlti.rel"); - this->set_data_tables_ep1_ult[static_cast(version)] = make_shared(version, *data_ep1_ult); + new_tables_ep1_ult[static_cast(version)] = make_shared(version, *data_ep1_ult); } }; - this->set_data_tables[static_cast(Version::DC_NTE)] = make_shared(); - this->set_data_tables[static_cast(Version::DC_V1_11_2000_PROTOTYPE)] = make_shared(); + new_tables[static_cast(Version::DC_NTE)] = make_shared(); + new_tables[static_cast(Version::DC_V1_11_2000_PROTOTYPE)] = make_shared(); load_table(Version::DC_V1); load_table(Version::DC_V2); load_table(Version::PC_NTE); @@ -1209,29 +1225,51 @@ void ServerState::load_set_data_tables() { load_table(Version::BB_V4); auto bb_solo_data = this->load_map_file(Version::BB_V4, "SetDataTableOff.rel"); - this->bb_solo_set_data_table = make_shared(Version::BB_V4, *bb_solo_data); + new_table_bb_solo = make_shared(Version::BB_V4, *bb_solo_data); auto bb_solo_data_ep1_ult = this->load_map_file(Version::BB_V4, "SetDataTableOffUlti.rel"); - this->bb_solo_set_data_table_ep1_ult = make_shared(Version::BB_V4, *bb_solo_data_ep1_ult); + new_table_bb_solo_ep1_ult = make_shared(Version::BB_V4, *bb_solo_data_ep1_ult); + + auto set = [s = this->shared_from_this(), + new_tables = std::move(new_tables), + new_tables_ep1_ult = std::move(new_tables_ep1_ult), + new_table_bb_solo = std::move(new_table_bb_solo), + new_table_bb_solo_ep1_ult = std::move(new_table_bb_solo_ep1_ult)]() { + s->set_data_tables = std::move(new_tables); + s->set_data_tables_ep1_ult = std::move(new_tables_ep1_ult); + s->bb_solo_set_data_table = std::move(new_table_bb_solo); + s->bb_solo_set_data_table_ep1_ult = std::move(new_table_bb_solo_ep1_ult); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_battle_params() { +void ServerState::load_battle_params(bool from_non_event_thread) { config_log.info("Loading battle parameters"); - this->battle_params = make_shared( + auto new_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")); + + auto set = [s = this->shared_from_this(), new_battle_params = std::move(new_battle_params)]() { + s->battle_params = std::move(new_battle_params); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_level_table() { +void ServerState::load_level_table(bool from_non_event_thread) { config_log.info("Loading level table"); - this->level_table = make_shared(*this->load_bb_file("PlyLevelTbl.prs"), true); + auto new_table = make_shared(*this->load_bb_file("PlyLevelTbl.prs"), true); + + auto set = [s = this->shared_from_this(), new_table = std::move(new_table)]() { + s->level_table = std::move(new_table); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_text_index() { - this->text_index = make_shared("system/text-sets", [&](Version version, const string& filename) -> shared_ptr { +void ServerState::load_text_index(bool from_non_event_thread) { + auto new_index = make_shared("system/text-sets", [&](Version version, const string& filename) -> shared_ptr { try { if (version == Version::BB_V4) { return this->load_bb_file(filename); @@ -1244,9 +1282,14 @@ void ServerState::load_text_index() { return nullptr; } }); + + auto set = [s = this->shared_from_this(), new_index = std::move(new_index)]() { + s->text_index = std::move(new_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_word_select_table() { +void ServerState::load_word_select_table(bool from_non_event_thread) { config_log.info("Loading Word Select table"); vector> name_alias_lists; @@ -1299,11 +1342,16 @@ void ServerState::load_word_select_table() { WordSelectSet bb_v4_ws(load_file("system/text-sets/bb-v4/ws_data.bin"), Version::BB_V4, bb_unitxt_collection, false); config_log.info("(Word select) Generating table"); - this->word_select_table = make_shared( + auto new_table = make_shared( dc_nte_ws, dc_112000_ws, dc_v1_ws, dc_v2_ws, pc_nte_ws, pc_v2_ws, gc_nte_ws, gc_v3_ws, gc_ep3_nte_ws, gc_ep3_ws, xb_v3_ws, bb_v4_ws, name_alias_lists); + + auto set = [s = this->shared_from_this(), new_table = std::move(new_table)]() { + s->word_select_table = std::move(new_table); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } shared_ptr ServerState::create_item_name_index_for_version( @@ -1334,16 +1382,24 @@ shared_ptr ServerState::create_item_name_index_for_version( } } -void ServerState::load_item_name_indexes() { +void ServerState::load_item_name_indexes(bool from_non_event_thread) { + std::array, NUM_VERSIONS> new_indexes; + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { Version v = static_cast(v_s); config_log.info("Generating item name index for %s", name_for_enum(v)); - this->set_item_name_index(v, this->create_item_name_index_for_version(v, this->item_parameter_table(v), this->text_index)); + new_indexes[v_s] = this->create_item_name_index_for_version(v, this->item_parameter_table(v), this->text_index); } + + auto set = [s = this->shared_from_this(), new_indexes = std::move(new_indexes)]() { + s->item_name_indexes = std::move(new_indexes); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_drop_tables() { +void ServerState::load_drop_tables(bool from_non_event_thread) { 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-")) { @@ -1387,25 +1443,25 @@ void ServerState::load_drop_tables() { new_rare_item_sets.emplace(basename, make_shared(load_file(path), 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-pc-v2.afs")); auto pt_data_v2 = make_shared(load_file("system/item-tables/ItemPT-pc-v2.afs")); - this->common_item_set_v2 = make_shared(pt_data_v2, ct_data_v2); + auto new_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-v3.gsl")); - this->common_item_set_v3_v4 = make_shared(pt_data_v3_v4, true); + auto new_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-v3.rel")); - this->armor_random_set = make_shared(armor_data); + auto new_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-v3.rel")); - this->tool_random_set = make_shared(tool_data); + auto new_tool_random_set = make_shared(tool_data); config_log.info("Loading weapon tables"); + std::array, 4> new_weapon_random_sets; const char* filenames[4] = { "system/item-tables/WeaponRandomNormal-gc-v3.rel", "system/item-tables/WeaponRandomHard-gc-v3.rel", @@ -1414,34 +1470,59 @@ void ServerState::load_drop_tables() { }; 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); + new_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-v3.rel")); - this->tekker_adjustment_set = make_shared(tekker_data); + auto new_tekker_adjustment_set = make_shared(tekker_data); + + auto set = [s = this->shared_from_this(), + new_rare_item_sets = std::move(new_rare_item_sets), + new_common_item_set_v2 = std::move(new_common_item_set_v2), + new_common_item_set_v3_v4 = std::move(new_common_item_set_v3_v4), + new_armor_random_set = std::move(new_armor_random_set), + new_tool_random_set = std::move(new_tool_random_set), + new_weapon_random_sets = std::move(new_weapon_random_sets), + new_tekker_adjustment_set = std::move(new_tekker_adjustment_set)]() { + s->rare_item_sets = std::move(new_rare_item_sets); + s->common_item_set_v2 = std::move(new_common_item_set_v2); + s->common_item_set_v3_v4 = std::move(new_common_item_set_v3_v4); + s->armor_random_set = std::move(new_armor_random_set); + s->tool_random_set = std::move(new_tool_random_set); + s->weapon_random_sets = std::move(new_weapon_random_sets); + s->tekker_adjustment_set = std::move(new_tekker_adjustment_set); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_item_definitions() { +void ServerState::load_item_definitions(bool from_non_event_thread) { + std::array, NUM_VERSIONS> new_item_parameter_tables; for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { Version v = static_cast(v_s); string path = string_printf("system/item-tables/ItemPMT-%s.prs", file_path_token_for_version(v)); config_log.info("Loading item definition table %s", path.c_str()); auto data = make_shared(prs_decompress(load_file(path))); - this->set_item_parameter_table(v, make_shared(data, v)); + new_item_parameter_tables[v_s] = make_shared(data, v); } // TODO: We should probably load the tables for other versions too. config_log.info("Loading mag evolution table"); auto mag_data = make_shared(prs_decompress(load_file("system/item-tables/ItemMagEdit-bb-v4.prs"))); - this->mag_evolution_table = make_shared(mag_data); + auto new_mag_evolution_table = make_shared(mag_data); + + auto set = [s = this->shared_from_this(), + new_item_parameter_tables = std::move(new_item_parameter_tables), + new_mag_evolution_table = std::move(new_mag_evolution_table)]() { + s->item_parameter_tables = std::move(new_item_parameter_tables); + s->mag_evolution_table = std::move(new_mag_evolution_table); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_ep3_data() { - config_log.info("Collecting Episode 3 maps"); - this->ep3_map_index = make_shared("system/ep3/maps"); +void ServerState::load_ep3_cards(bool from_non_event_thread) { config_log.info("Loading Episode 3 card definitions"); - this->ep3_card_index = make_shared( + auto new_ep3_card_index = make_shared( "system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd", "system/ep3/card-text.mnr", @@ -1449,7 +1530,7 @@ void ServerState::load_ep3_data() { "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( + auto new_ep3_card_index_trial = make_shared( "system/ep3/card-definitions-trial.mnr", "system/ep3/card-definitions-trial.mnrd", "system/ep3/card-text-trial.mnr", @@ -1457,39 +1538,76 @@ void ServerState::load_ep3_data() { "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"); + auto new_ep3_com_deck_index = make_shared("system/ep3/com-decks.json"); + auto set = [s = this->shared_from_this(), + new_ep3_card_index = std::move(new_ep3_card_index), + new_ep3_card_index_trial = std::move(new_ep3_card_index_trial), + new_ep3_com_deck_index = std::move(new_ep3_com_deck_index)]() { + s->ep3_card_index = std::move(new_ep3_card_index); + s->ep3_card_index_trial = std::move(new_ep3_card_index_trial); + s->ep3_com_deck_index = std::move(new_ep3_com_deck_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); +} + +void ServerState::load_ep3_maps(bool from_non_event_thread) { + config_log.info("Collecting Episode 3 maps"); + auto new_ep3_map_index = make_shared("system/ep3/maps"); + + auto set = [s = this->shared_from_this(), new_ep3_map_index = std::move(new_ep3_map_index)]() { + s->ep3_map_index = std::move(new_ep3_map_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); +} + +void ServerState::load_ep3_tournament_state(bool from_non_event_thread) { + config_log.info("Loading Episode 3 tournament state"); const string& tournament_state_filename = "system/ep3/tournament-state.json"; - this->ep3_tournament_index = make_shared( + auto new_ep3_tournament_index = make_shared( this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename); - shared_ptr s; - try { - s = this->shared_from_this(); - } catch (const bad_weak_ptr&) { - } - if (s) { - this->ep3_tournament_index->link_all_clients(s); - } - - config_log.info("Loaded Episode 3 tournament state"); + auto set = [s = this->shared_from_this(), + new_ep3_tournament_index = std::move(new_ep3_tournament_index)]() { + s->ep3_tournament_index = std::move(new_ep3_tournament_index); + s->ep3_tournament_index->link_all_clients(s); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_quest_index() { +void ServerState::load_quest_index(bool from_non_event_thread) { config_log.info("Collecting quests"); - this->default_quest_index = make_shared("system/quests", this->quest_category_index, false); + auto new_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); + auto new_ep3_download_quest_index = make_shared("system/ep3/maps-download", this->quest_category_index, true); + + auto set = [s = this->shared_from_this(), + new_default_quest_index = std::move(new_default_quest_index), + new_ep3_download_quest_index = std::move(new_ep3_download_quest_index)]() { + s->default_quest_index = std::move(new_default_quest_index); + s->ep3_download_quest_index = std::move(new_ep3_download_quest_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::compile_functions() { +void ServerState::compile_functions(bool from_non_event_thread) { config_log.info("Compiling client functions"); - this->function_code_index = make_shared("system/client-functions"); + auto new_function_code_index = make_shared("system/client-functions"); + + auto set = [s = this->shared_from_this(), new_function_code_index = std::move(new_function_code_index)]() { + s->function_code_index = std::move(new_function_code_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_dol_files() { +void ServerState::load_dol_files(bool from_non_event_thread) { config_log.info("Loading DOL files"); - this->dol_file_index = make_shared("system/dol"); + auto new_dol_file_index = make_shared("system/dol"); + + auto set = [s = this->shared_from_this(), new_dol_file_index = std::move(new_dol_file_index)]() { + s->dol_file_index = std::move(new_dol_file_index); + }; + this->forward_or_call(from_non_event_thread, std::move(set)); } void ServerState::create_default_lobbies() { @@ -1537,86 +1655,27 @@ void ServerState::create_default_lobbies() { } } -void ServerState::create_load_step_graph() { - this->load_step_graph.add_step("all", {}, nullptr); - - // 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: map_file_caches - this->load_step_graph.add_step("map_file_caches", {"all"}, bind(&ServerState::clear_map_file_caches, this)); - - // In: none - // Out: pc_patch_file_index, bb_patch_file_index, bb_data_gsl - this->load_step_graph.add_step("patch_indexes", {"all", "map_file_caches"}, bind(&ServerState::load_patch_indexes, this)); - - // In: none - // Out: ep3_map_index, ep3_card_index, ep3_card_index_trial, ep3_com_deck_index, ep3_tournament_index - 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: set_data_tables - this->load_step_graph.add_step("set_data_tables", {"all", "patch_indexes"}, bind(&ServerState::load_set_data_tables, 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)); +void ServerState::load_all() { + this->collect_network_addresses(); + this->load_bb_private_keys(false); + this->load_licenses(false); + this->clear_map_file_caches(); + this->load_patch_indexes(false); + this->load_ep3_cards(false); + this->load_ep3_maps(false); + this->load_ep3_tournament_state(false); + this->compile_functions(false); + this->load_dol_files(false); + this->create_default_lobbies(); + this->load_set_data_tables(false); + this->load_battle_params(false); + this->load_level_table(false); + this->load_text_index(false); + this->load_word_select_table(false); + this->load_item_definitions(false); + this->load_item_name_indexes(false); + this->load_drop_tables(false); + this->load_config(); + this->load_teams(false); + this->load_quest_index(false); } diff --git a/src/ServerState.hh b/src/ServerState.hh index b6389929..82b03f63 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -15,6 +15,7 @@ #include "CommonItemSet.hh" #include "Episode3/DataIndexes.hh" #include "Episode3/Tournament.hh" +#include "EventUtils.hh" #include "FunctionCompiler.hh" #include "GSLArchive.hh" #include "ItemNameIndex.hh" @@ -25,7 +26,6 @@ #include "Menu.hh" #include "PlayerFilesManager.hh" #include "Quest.hh" -#include "StepGraph.hh" #include "TeamIndex.hh" #include "WordSelectTable.hh" @@ -69,8 +69,6 @@ struct ServerState : public std::enable_shared_from_this { 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; @@ -245,11 +243,6 @@ struct ServerState : public std::enable_shared_from_this { ServerState& operator=(const ServerState&) = delete; ServerState& operator=(ServerState&&) = delete; - void load_objects_and_downstream_dependents(const std::string& what); - void load_objects_and_downstream_dependents(const std::vector& what); - void load_objects_and_upstream_dependents(const std::string& what); - void load_objects_and_upstream_dependents(const std::vector& what); - void add_client_to_available_lobby(std::shared_ptr c); void remove_client_from_lobby(std::shared_ptr c); bool change_client_lobby( @@ -308,29 +301,45 @@ struct ServerState : public std::enable_shared_from_this { std::pair parse_port_spec(const JSON& json) const; std::vector parse_port_configuration(const JSON& json) const; - void create_load_step_graph(); + inline void forward_to_event_thread(std::function&& fn) { + ::forward_to_event_thread(this->base, std::move(fn)); + } + inline void forward_or_call(bool from_non_event_thread, std::function&& fn) { + if (from_non_event_thread) { + ::forward_to_event_thread(this->base, std::move(fn)); + } else { + fn(); + } + } + + // The following functions may only be called from a non-event thread if they + // take a from_non_event_thread argument; any function that does not have this + // argument must be called only from the event thread. void create_default_lobbies(); void collect_network_addresses(); void load_config(); - void load_bb_private_keys(); - void load_licenses(); - void load_teams(); - void load_patch_indexes(); + void load_bb_private_keys(bool from_non_event_thread); + void load_licenses(bool from_non_event_thread); + void load_teams(bool from_non_event_thread); + void load_patch_indexes(bool from_non_event_thread); void clear_map_file_caches(); - void load_battle_params(); - void load_level_table(); - void load_text_index(); + void load_battle_params(bool from_non_event_thread); + void load_level_table(bool from_non_event_thread); + void load_text_index(bool from_non_event_thread); 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_set_data_tables(); - void load_word_select_table(); - void load_ep3_data(); - void load_quest_index(); - void compile_functions(); - void load_dol_files(); + void load_item_name_indexes(bool from_non_event_thread); + void load_drop_tables(bool from_non_event_thread); + void load_item_definitions(bool from_non_event_thread); + void load_set_data_tables(bool from_non_event_thread); + void load_word_select_table(bool from_non_event_thread); + void load_ep3_cards(bool from_non_event_thread); + void load_ep3_maps(bool from_non_event_thread); + void load_ep3_tournament_state(bool from_non_event_thread); + void load_quest_index(bool from_non_event_thread); + void compile_functions(bool from_non_event_thread); + void load_dol_files(bool from_non_event_thread); + void load_all(); void enqueue_destroy_lobbies(); static void dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx); diff --git a/src/Shell.cc b/src/Shell.cc deleted file mode 100644 index e9a2217e..00000000 --- a/src/Shell.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include "Shell.hh" - -#include -#include -#include - -#include - -using namespace std; - -const std::string Shell::PROMPT("newserv> "); - -Shell::exit_shell::exit_shell() : runtime_error("shell exited") {} - -Shell::Shell(std::shared_ptr base) - : base(base), - read_event( - event_new(this->base.get(), 0, EV_READ | EV_PERSIST, &Shell::dispatch_read_stdin, this), - event_free), - prompt_event( - event_new(this->base.get(), 0, EV_TIMEOUT, &Shell::dispatch_print_prompt, this), - event_free) { - event_add(this->read_event.get(), nullptr); - - // Schedule an event to print the prompt as soon as the event loop starts - // running. We do this so the prompt appears after any initialization - // messages that come after starting the shell - struct timeval tv = {0, 0}; - event_add(this->prompt_event.get(), &tv); - - this->poll.add(0, POLLIN); -} - -void Shell::dispatch_print_prompt(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->print_prompt(); -} - -void Shell::print_prompt() { - // Default behavior: no prompt -} - -void Shell::dispatch_read_stdin(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->read_stdin(); -} - -void Shell::read_stdin() { - bool any_command_read = false; - for (;;) { - auto poll_result = this->poll.poll(); - short fd_events = 0; - try { - fd_events = poll_result.at(0); - } catch (const out_of_range&) { - } - - if (!(fd_events & POLLIN)) { - break; - } - - string command(2048, '\0'); - if (!fgets(command.data(), command.size(), stdin)) { - if (!any_command_read) { - // ctrl+d probably; we should exit - fputc('\n', stderr); - event_base_loopexit(this->base.get(), nullptr); - return; - } else { - break; // probably not EOF; just no more commands for now - } - } - - // trim the extra data off the string - size_t len = strlen(command.c_str()); - if (len == 0) { - break; - } - if (command[len - 1] == '\n') { - len--; - } - command.resize(len); - any_command_read = true; - - try { - execute_command(command); - } catch (const exit_shell&) { - event_base_loopexit(this->base.get(), nullptr); - return; - } catch (const exception& e) { - fprintf(stderr, "FAILED: %s\n", e.what()); - } - } - - this->print_prompt(); -} diff --git a/src/Shell.hh b/src/Shell.hh deleted file mode 100644 index 8202bf8d..00000000 --- a/src/Shell.hh +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "ServerState.hh" - -class Shell { -public: - Shell(std::shared_ptr base); - virtual ~Shell() = default; - Shell(const Shell&) = delete; - Shell(Shell&&) = delete; - Shell& operator=(const Shell&) = delete; - Shell& operator=(Shell&&) = delete; - - static const std::string PROMPT; - -protected: - std::shared_ptr base; - std::unique_ptr read_event; - std::unique_ptr prompt_event; - Poll poll; - - class exit_shell : public std::runtime_error { - public: - exit_shell(); - ~exit_shell() = default; - }; - - static void dispatch_print_prompt(evutil_socket_t fd, short events, void* ctx); - static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx); - virtual void print_prompt(); - void read_stdin(); - virtual void execute_command(const std::string& command) = 0; -}; diff --git a/src/StepGraph.cc b/src/StepGraph.cc deleted file mode 100644 index a78bfcc2..00000000 --- a/src/StepGraph.cc +++ /dev/null @@ -1,83 +0,0 @@ -#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(); - new_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); - new_step->upstream_dependencies.emplace_back(upstream_step); - } -} - -void StepGraph::run(const string& start_step_name, bool run_upstreams) { - vector start_step_names({start_step_name}); - this->run(start_step_names, run_upstreams); -} - -void StepGraph::run(const vector& start_step_names, bool run_upstreams) { - // 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) { - if (run_upstreams) { - for (const auto& w_other_step : step->upstream_dependencies) { - auto other_step = w_other_step.lock(); - if (!other_step) { - throw runtime_error("upstream step is deleted"); - } - steps_to_visit.emplace_back(other_step); - } - } else { - for (const auto& other_step : step->downstream_dependencies) { - steps_to_visit.emplace_back(other_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 deleted file mode 100644 index 9bb00fba..00000000 --- a/src/StepGraph.hh +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -struct StepGraph { - struct Step { - std::vector> downstream_dependencies; - std::vector> upstream_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, bool run_upstreams); - void run(const std::vector& start_steps, bool run_upstreams); -};