#include "GameServer.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include "Loggers.hh" #include "PSOProtocol.hh" #include "ReceiveCommands.hh" using namespace std; using namespace std::placeholders; GameServer::GameServer(shared_ptr state) : Server(state->io_context, "[GameServer] "), state(state) {} void GameServer::listen( const std::string& name, const string& addr, uint16_t port, Version version, ServerBehavior behavior) { if (port == 0) { throw std::runtime_error("Listening port cannot be zero"); } asio::ip::address asio_addr = addr.empty() ? asio::ip::address_v4::any() : asio::ip::make_address(addr); auto sock = make_shared(); sock->name = name; sock->endpoint = asio::ip::tcp::endpoint(asio_addr, port); sock->version = version; sock->behavior = behavior; this->add_socket(std::move(sock)); } shared_ptr GameServer::connect_channel(shared_ptr ch, uint16_t port, ServerBehavior initial_state) { auto c = make_shared(this->shared_from_this(), ch, initial_state); this->log.info_f("Client connected: C-{:X} via TSI-{}-{}-{}", c->id, port, phosg::name_for_enum(ch->version), phosg::name_for_enum(initial_state)); asio::co_spawn(*this->io_context, this->handle_connected_client(c), asio::detached); return c; } shared_ptr GameServer::get_client() const { if (this->clients.empty()) { throw runtime_error("no clients on game server"); } if (this->clients.size() > 1) { throw runtime_error("multiple clients on game server"); } return *this->clients.begin(); } vector> GameServer::get_clients_by_identifier(const string& ident) const { int64_t account_id_hex = -1; int64_t account_id_dec = -1; try { account_id_dec = stoul(ident, nullptr, 10); } catch (const invalid_argument&) { } try { account_id_hex = stoul(ident, nullptr, 16); } catch (const invalid_argument&) { } // 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& c : this->clients) { if (c->login && c->login->account->account_id == account_id_hex) { results.emplace_back(c); continue; } if (c->login && c->login->account->account_id == account_id_dec) { results.emplace_back(c); continue; } if (c->login && c->login->xb_license && c->login->xb_license->gamertag == ident) { results.emplace_back(c); continue; } if (c->login && c->login->bb_license && c->login->bb_license->username == ident) { results.emplace_back(c); continue; } auto p = c->character(false, false); if (p && p->disp.name.eq(ident, p->inventory.language)) { results.emplace_back(c); continue; } if (c->channel->name == ident) { results.emplace_back(c); continue; } if (c->channel->name.starts_with(ident + " ")) { results.emplace_back(c); continue; } } return results; } shared_ptr GameServer::create_client(shared_ptr listen_sock, asio::ip::tcp::socket&& client_sock) { uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address()); if (this->state->banned_ipv4_ranges->check(addr)) { client_sock.close(); return nullptr; } auto channel = SocketChannel::create( this->io_context, make_unique(std::move(client_sock)), listen_sock->version, 1, "", phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::FG_GREEN); auto c = make_shared(this->shared_from_this(), channel, listen_sock->behavior); this->log.info_f("Client connected: C-{:X} via {}", c->id, listen_sock->name); return c; } asio::awaitable GameServer::handle_client_command(shared_ptr c, unique_ptr msg) { try { co_await on_command(c, std::move(msg)); } catch (const exception& e) { this->log.warning_f("Error processing client command: {}", e.what()); c->channel->disconnect(); } } asio::awaitable GameServer::handle_client(shared_ptr c) { auto g = phosg::on_close_scope(std::bind(&Client::cancel_pending_promises, c.get())); try { co_await on_connect(c); } catch (const exception& e) { this->log.warning_f("Error in client initialization: {}", e.what()); c->channel->disconnect(); } while (c->channel->connected()) { auto msg = std::make_unique(co_await c->channel->recv()); asio::co_spawn(co_await asio::this_coro::executor, this->handle_client_command(c, std::move(msg)), asio::detached); } } asio::awaitable GameServer::destroy_client(std::shared_ptr c) { this->log.info_f("Running cleanup tasks for {}", c->channel->name); // The client may not actually be disconnected yet if an uncaught exception // occurred in a handler task c->channel->disconnect(); // Close the proxy session, if any if (c->proxy_session) { if (c->proxy_session->server_channel) { c->proxy_session->server_channel->disconnect(); } c->proxy_session.reset(); } try { co_await on_disconnect(c); } catch (const exception& e) { this->log.warning_f("Error during client disconnect cleanup: {}", e.what()); } // Note: It's important to move the disconnect hooks out of the client here // because the hooks could modify c->disconnect_hooks while it's being // iterated here, which would invalidate these iterators. unordered_map> hooks = std::move(c->disconnect_hooks); for (auto h_it : hooks) { try { h_it.second(); } catch (const exception& e) { c->log.warning_f("Disconnect hook {} failed: {}", h_it.first, e.what()); } } }