diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 198a0e6e..35c5061b 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -795,6 +795,18 @@ shared_ptr ProxyServer::get_session() { return this->id_to_session.begin()->second; } +shared_ptr ProxyServer::get_session_by_name( + const std::string& name) { + try { + uint64_t session_id = stoull(name, nullptr, 16); + return this->id_to_session.at(session_id); + } catch (const invalid_argument&) { + throw runtime_error("invalid session name"); + } catch (const out_of_range&) { + throw runtime_error("no such session"); + } +} + shared_ptr ProxyServer::create_licensed_session( shared_ptr l, uint16_t local_port, GameVersion version, const ClientConfigBB& newserv_client_config) { diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index ea076090..cc63aeaf 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -152,6 +152,7 @@ public: }; std::shared_ptr get_session(); + std::shared_ptr get_session_by_name(const std::string& name); std::shared_ptr create_licensed_session( std::shared_ptr l, uint16_t local_port, diff --git a/src/Server.cc b/src/Server.cc index 9bd6f3f0..dfc83594 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -270,6 +270,54 @@ shared_ptr Server::get_client() const { return this->channel_to_client.begin()->second; } +vector> Server::get_clients_by_identifier(const string& ident) const { + int64_t serial_number_hex = -1; + int64_t serial_number_dec = -1; + try { + serial_number_dec = stoul(ident, nullptr, 10); + } catch (const invalid_argument&) { } + try { + serial_number_hex = stoul(ident, nullptr, 16); + } catch (const invalid_argument&) { } + u16string u16name = decode_sjis(ident); + + // TODO: It's kind of not great that we do a linear search here, but this is + // only used in the shell, so it should be pretty rare. + vector> results; + for (const auto& it : this->channel_to_client) { + auto c = it.second; + if (c->license && c->license->serial_number == serial_number_dec) { + results.emplace_back(move(c)); + continue; + } + if (c->license && c->license->serial_number == serial_number_hex) { + results.emplace_back(move(c)); + continue; + } + if (c->license && c->license->username == ident) { + results.emplace_back(move(c)); + continue; + } + + auto p = c->game_data.player(false); + if (p && p->disp.name == u16name) { + results.emplace_back(move(c)); + continue; + } + + if (c->channel.name == ident) { + results.emplace_back(move(c)); + continue; + } + if (starts_with(c->channel.name, ident + " ")) { + results.emplace_back(move(c)); + continue; + } + } + + return results; +} + shared_ptr Server::get_base() const { return this->base; } diff --git a/src/Server.hh b/src/Server.hh index 3e226bb9..456f63b5 100644 --- a/src/Server.hh +++ b/src/Server.hh @@ -31,6 +31,8 @@ public: GameVersion version, ServerBehavior initial_state); std::shared_ptr get_client() const; + std::vector> get_clients_by_identifier( + const std::string& ident) const; std::shared_ptr get_base() const; private: diff --git a/src/ServerShell.cc b/src/ServerShell.cc index dcd96820..b60d7ff2 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -24,11 +24,14 @@ void ServerShell::print_prompt() { fflush(stdout); } -shared_ptr ServerShell::get_proxy_session() { +shared_ptr ServerShell::get_proxy_session( + const string& name) { if (!this->state->proxy_server.get()) { throw runtime_error("the proxy server is disabled"); } - return this->state->proxy_server->get_session(); + return name.empty() + ? this->state->proxy_server->get_session() + : this->state->proxy_server->get_session_by_name(name); } static void set_boolean(bool* target, const string& args) { @@ -68,12 +71,23 @@ static string get_quoted_string(string& s) { } void ServerShell::execute_command(const string& command) { - // find the entry in the command table and run the 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); + 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 == "exit") { throw exit_shell(); @@ -144,9 +158,10 @@ Server commands:\n\ Show the current state of a tournament. The quotes are required unless the\n\ tournament name contains no spaces.\n\ \n\ -Proxy commands (these will only work when exactly one client is connected):\n\ +Proxy commands:\n\ sc \n\ - Send a command to the client.\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 \n\ Send a command to the server.\n\ chat \n\ @@ -203,6 +218,15 @@ Proxy commands (these will only work when exactly one client is connected):\n\ Set the next item to be dropped as if the client had run the $item command.\n\ close-idle-sessions\n\ Closes all sessions that don\'t have a client and server connected.\n\ +\n\ +If there are multiple clients connected, or multiple proxy sessions open, many\n\ +of the above commands will fail since they can\'t determine which session should\n\ +be affected. To specify a session, prefix the command with `on `. For\n\ +game server sessions, may be the client\'s ID (e.g. C-3), player name,\n\ +license serial number (specified in hex or in decimal), or BB account username.\n\ +For proxy sessions, may only be the session ID (which is generally the\n\ +same as the client\'s serial number). For example, to send a ping to the proxy\n\ +session with ID 17205AE4, run the command `on 17205AE4 sc 1D 00 04 00`.\n\ "); @@ -473,7 +497,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ shared_ptr proxy_session; try { - proxy_session = this->get_proxy_session(); + proxy_session = this->get_proxy_session(session_name); } catch (const exception&) { } if (proxy_session.get()) { @@ -488,12 +512,30 @@ Proxy commands (these will only work when exactly one client is connected):\n\ throw runtime_error("cannot send to server in non-proxy session"); } - auto c = this->state->game_server->get_client(); - send_command_with_header(c->channel, data.data(), data.size()); + shared_ptr c; + if (session_name.empty()) { + c = this->state->game_server->get_client(); + } 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 = move(clients[0]); + } + + if (c) { + send_command_with_header(c->channel, data.data(), data.size()); + } else { + throw runtime_error("no client available"); + } } } else if ((command_name == "chat") || (command_name == "dchat")) { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); string data(8, '\0'); data.push_back('\x09'); @@ -509,18 +551,18 @@ Proxy commands (these will only work when exactly one client is connected):\n\ session->server_channel.send(0x06, 0x00, data); } else if (command_name == "marker") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); session->server_channel.send(0x89, stoul(command_args)); } else if (command_name == "warp") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); uint8_t area = stoul(command_args); send_warp(session->client_channel, session->lobby_client_id, area); send_warp(session->server_channel, session->lobby_client_id, area); } else if ((command_name == "info-board") || (command_name == "info-board-data")) { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); string data; if (command_name == "info-board-data") { @@ -534,7 +576,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ session->server_channel.send(0xD9, 0x00, data); } else if (command_name == "set-override-section-id") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); if (command_args.empty()) { session->options.override_section_id = -1; } else { @@ -542,7 +584,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } } else if (command_name == "set-override-event") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); if (command_args.empty()) { session->options.override_lobby_event = -1; } else { @@ -556,7 +598,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } } else if (command_name == "set-override-lobby-number") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); if (command_args.empty()) { session->options.override_lobby_number = -1; } else { @@ -564,27 +606,27 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } } else if (command_name == "set-chat-filter") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); set_boolean(&session->options.enable_chat_filter, command_args); } else if (command_name == "set-infinite-hp") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); set_boolean(&session->options.infinite_hp, command_args); } else if (command_name == "set-infinite-tp") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); set_boolean(&session->options.infinite_tp, command_args); } else if (command_name == "set-switch-assist") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); set_boolean(&session->options.switch_assist, command_args); } else if (command_name == "set-save-files" && this->state->proxy_allow_save_files) { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); set_boolean(&session->options.save_files, command_args); } else if (command_name == "set-block-function-calls") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); if (command_args.empty()) { session->options.function_call_return_value = -1; } else { @@ -592,7 +634,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } } else if (command_name == "set-next-item") { - auto session = this->get_proxy_session(); + auto session = this->get_proxy_session(session_name); if (session->version == GameVersion::BB) { throw runtime_error("proxy session is BB"); diff --git a/src/ServerShell.hh b/src/ServerShell.hh index 51ec05da..91834bff 100644 --- a/src/ServerShell.hh +++ b/src/ServerShell.hh @@ -26,7 +26,8 @@ public: protected: std::shared_ptr state; - std::shared_ptr get_proxy_session(); + std::shared_ptr get_proxy_session( + const std::string& name); virtual void print_prompt(); virtual void execute_command(const std::string& command);