From 0d4b0b2279f42d6d856b071b36ba7d0c95f0b4be Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 16 Feb 2020 15:03:47 -0800 Subject: [PATCH] fix a lot of issues on psogc; add proxy module - $ann implemented - concurrency removed; server is now single-threaded, event-driven and much more stable - rare seed is no longer the game id; ids are sequential from server startup so they weren't random at all before - supports dropping privileges; now you can run it as root so it can open a sockets on low ports, then it will switch to the given user before serving any traffic - newserv now behaves like a proxy if you run it with the --proxy-destination= argument; there's also an (invisible) shell in this mode where you can inject commands to the server or client. e.g. it can always be christmas in the lobby if you do `sc DA 01 00 00` - increased the mtu on PSODolphinConfig's tap0 configuration; this seems to make the connection more stable - fixed some uninitialized memory bugs - the shell is now event-driven and now uses libevent too; unfortunately this means readline doesn't work anymore (no history and vim-like shortcuts) - made network command display consistent for input vs. output (the header appears in both cases now) - fixed bugs in some subcommand handling (the BB logic was being applied to non-BB clients erroneously, causing most item drops not to work at all) - fixed player tags in the short lobby data struct. unclear if this was actually a problem but it was inconsistent with other servers - fixed "unused" field in game join command (actually it appears to be disable_udp and should be 1, not 0) - cleaned up Server abstraction a bit - rewrote some text functions; asan was complaining about the built-in ones for some reason - added an optional welcome message --- ChatCommands.cc | 40 ++--- Client.cc | 8 +- Client.hh | 5 +- DNSServer.cc | 126 ++++++---------- DNSServer.hh | 26 ++-- FileContentsCache.cc | 16 +- FileContentsCache.hh | 2 - Items.cc | 2 +- Items.hh | 2 +- License.cc | 42 ++---- License.hh | 4 +- Lobby.cc | 60 ++------ Lobby.hh | 12 +- Main.cc | 149 ++++++++++++------ Makefile | 13 +- PSODolphinConfig.py | 2 +- PSOEncryption.hh | 2 + Player.cc | 44 ++++-- ProxyServer.cc | 344 ++++++++++++++++++++++++++++++++++++++++++ ProxyServer.hh | 63 ++++++++ ProxyShell.cc | 81 ++++++++++ ProxyShell.hh | 28 ++++ README.md | 4 +- ReceiveCommands.cc | 156 +++++++++++-------- ReceiveSubcommands.cc | 43 ++---- SendCommands.cc | 320 ++++++++++++++++++--------------------- SendCommands.hh | 13 +- Server.cc | 263 ++++++++++---------------------- Server.hh | 98 ++++++------ ServerShell.cc | 148 ++++++++++++++++++ ServerShell.hh | 25 +++ ServerState.cc | 19 +-- ServerState.hh | 5 +- Shell.cc | 212 ++++++++------------------ Shell.hh | 37 ++++- Text.cc | 29 +++- Text.hh | 2 +- Version.hh | 5 + system/config.json | 12 +- 39 files changed, 1487 insertions(+), 975 deletions(-) create mode 100644 ProxyServer.cc create mode 100644 ProxyServer.hh create mode 100644 ProxyShell.cc create mode 100644 ProxyShell.hh create mode 100644 ServerShell.cc create mode 100644 ServerShell.hh diff --git a/ChatCommands.cc b/ChatCommands.cc index b418bdad..d3181c25 100644 --- a/ChatCommands.cc +++ b/ChatCommands.cc @@ -262,7 +262,7 @@ static void command_ax(shared_ptr s, shared_ptr l, static void command_announce(shared_ptr s, shared_ptr l, shared_ptr c, const char16_t* args) { check_privileges(c, Privilege::Announce); - // TODO: implement this + send_text_message(s, args); } static void command_arrow(shared_ptr s, shared_ptr l, @@ -289,7 +289,6 @@ static void command_cheat(shared_ptr s, shared_ptr l, // if cheat mode was disabled, turn off all the cheat features that were on if (!(l->flags & LobbyFlag::CheatsEnabled)) { - rw_guard g(l->lock, true); for (size_t x = 0; x < l->max_clients; x++) { auto c = l->clients[x]; if (!c) { @@ -315,10 +314,7 @@ static void command_lobby_event(shared_ptr s, shared_ptr l, return; } - { - rw_guard g(l->lock, true); - l->event = new_event; - } + l->event = new_event; send_command(l, 0xDA, l->event, NULL, 0); } @@ -339,10 +335,7 @@ static void command_lobby_event_all(shared_ptr s, shared_ptr continue; } - { - rw_guard g(l->lock, true); - l->event = new_event; - } + l->event = new_event; send_command(l, 0xDA, new_event, NULL, 0); } } @@ -360,15 +353,11 @@ static void command_lobby_type(shared_ptr s, shared_ptr l, return; } - { - rw_guard g(l->lock, true); - l->type = new_type; - if (l->type < ((l->flags & LobbyFlag::Episode3) ? 20 : 15)) { - l->type = l->block - 1; - } + l->type = new_type; + if (l->type < ((l->flags & LobbyFlag::Episode3) ? 20 : 15)) { + l->type = l->block - 1; } - rw_guard g(l->lock, false); for (size_t x = 0; x < l->max_clients; x++) { if (l->clients[x]) { send_join_lobby(l->clients[x], l); @@ -402,10 +391,7 @@ static void command_min_level(shared_ptr s, shared_ptr l, check_is_leader(l, c); u16string buffer; - { - rw_guard g(l->lock, true); - l->min_level = stoull(encode_sjis(args)) - 1; - } + l->min_level = stoull(encode_sjis(args)) - 1; send_text_message_printf(l, "$C6Minimum level set to %" PRIu32, l->min_level + 1); } @@ -415,12 +401,9 @@ static void command_max_level(shared_ptr s, shared_ptr l, check_is_game(l, true); check_is_leader(l, c); - { - rw_guard g(l->lock, true); - l->max_level = stoull(encode_sjis(args)) - 1; - if (l->max_level >= 200) { - l->max_level = 0xFFFFFFFF; - } + l->max_level = stoull(encode_sjis(args)) - 1; + if (l->max_level >= 200) { + l->max_level = 0xFFFFFFFF; } if (l->max_level == 0xFFFFFFFF) { @@ -576,7 +559,6 @@ static void command_silence(shared_ptr s, shared_ptr l, return; } - rw_guard g(target->lock, true); target->can_chat = !target->can_chat; send_text_message_printf(l, "$C6%s %ssilenced", target->player.disp.name, target->can_chat ? "un" : ""); @@ -726,6 +708,8 @@ static void command_item(shared_ptr s, shared_ptr l, memcpy(&l->next_drop_item.data.item_data1, data.data(), 12); memcpy(&l->next_drop_item.data.item_data2, data.data() + 12, 12 - data.size()); } + + send_text_message(c, u"$C6Next drop chosen."); } diff --git a/Client.cc b/Client.cc index 9314a76c..b536894e 100644 --- a/Client.cc +++ b/Client.cc @@ -20,9 +20,9 @@ Client::Client(struct bufferevent* bev, GameVersion version, flags(flags_for_version(version, 0)), bev(bev), server_behavior(server_behavior), should_disconnect(false), play_time_begin(now()), last_recv_time(this->play_time_begin), - last_send_time(0), in_information_menu(false), area(0), lobby_id(0), - lobby_client_id(0), lobby_arrow_color(0), next_exp_value(0), - infinite_hp(false), infinite_tp(false), can_chat(true) { + last_send_time(0), area(0), lobby_id(0), lobby_client_id(0), + lobby_arrow_color(0), next_exp_value(0), infinite_hp(false), + infinite_tp(false), can_chat(true) { int fd = bufferevent_getfd(this->bev); get_socket_addresses(fd, &this->local_addr, &this->remote_addr); @@ -31,8 +31,6 @@ Client::Client(struct bufferevent* bev, GameVersion version, } bool Client::send(string&& data) { - rw_guard g(this->lock, false); - if (!this->bev) { return false; } diff --git a/Client.hh b/Client.hh index f3b65d0a..f89b4876 100644 --- a/Client.hh +++ b/Client.hh @@ -34,9 +34,7 @@ struct ClientConfigBB { }; struct Client { - rw_lock lock; - - // license & account + // license & account std::shared_ptr license; char16_t name[0x20]; ClientConfigBB config; @@ -60,7 +58,6 @@ struct Client { uint64_t play_time_begin; // time of connection (used for incrementing play time on BB) uint64_t last_recv_time; // time of last data received uint64_t last_send_time; // time of last data sent - bool in_information_menu; // lobby/positioning uint32_t area; // which area is the client in? diff --git a/DNSServer.cc b/DNSServer.cc index a7e5e5f9..f6e2751e 100644 --- a/DNSServer.cc +++ b/DNSServer.cc @@ -18,14 +18,14 @@ using namespace std; -DNSServer::DNSServer(uint32_t local_connect_address, - uint32_t external_connect_address) : - should_exit(false), local_connect_address(local_connect_address), +DNSServer::DNSServer(shared_ptr base, + uint32_t local_connect_address, uint32_t external_connect_address) : + base(base), local_connect_address(local_connect_address), external_connect_address(external_connect_address) { } DNSServer::~DNSServer() { - for (int fd : this->fds) { - close(fd); + for (const auto& it : this->fd_to_receive_event) { + close(it.first); } } @@ -42,94 +42,66 @@ void DNSServer::listen(int port) { } void DNSServer::add_socket(int fd) { - this->fds.emplace(fd); + unique_ptr e(event_new(this->base.get(), + fd, EV_READ | EV_PERSIST, &DNSServer::dispatch_on_receive_message, + this), event_free); + event_add(e.get(), NULL); + this->fd_to_receive_event.emplace(fd, move(e)); } -void DNSServer::start() { - this->t = thread(&DNSServer::run_thread, this); +void DNSServer::dispatch_on_receive_message(evutil_socket_t fd, + short events, void* ctx) { + reinterpret_cast(ctx)->on_receive_message(fd, events); } -void DNSServer::schedule_stop() { - this->should_exit = true; -} - -void DNSServer::wait_for_stop() { - this->t.join(); -} - -void DNSServer::run_thread() { - vector poll_fds; - for (int fd : this->fds) { - poll_fds.emplace_back(); - auto& pfd = poll_fds.back(); - pfd.fd = fd; - pfd.events = POLLIN; - pfd.revents = 0; - } - - while (!this->should_exit) { +void DNSServer::on_receive_message(int fd, short event) { + for (;;) { sockaddr_in remote; socklen_t remote_size = sizeof(sockaddr_in); memset(&remote, 0, remote_size); - // 10 second timeout - int num_fds = poll(poll_fds.data(), poll_fds.size(), 10000); - if (num_fds < 0) { - auto s = string_for_error(errno); - log(ERROR, "DNS server terminating due to error: %s", s.c_str()); + string input(2048, 0); + ssize_t bytes = recvfrom(fd, const_cast(input.data()), input.size(), + 0, reinterpret_cast(&remote), &remote_size); + + if (bytes < 0) { + if (errno != EAGAIN) { + log(INFO, "[DNSServer] input error %d", errno); + throw runtime_error("cannot read from udp socket"); + } break; - } - if (num_fds == 0) { - continue; - } + } else if (bytes == 0) { + break; - for (const auto& pfd : poll_fds) { - if (!(pfd.revents & POLLIN)) { - continue; + } else { // bytes > 0 + input.resize(bytes); + + uint32_t remote_address = bswap32(remote.sin_addr.s_addr); + uint32_t connect_address; + if (is_local_address(remote_address)) { + connect_address = this->local_connect_address; + } else { + connect_address = this->external_connect_address; } - string input(2048, 0); - ssize_t bytes = recvfrom(pfd.fd, const_cast(input.data()), - input.size(), 0, reinterpret_cast(&remote), &remote_size); - if (bytes > 0) { - input.resize(bytes); + if (input.size() >= 0x0C) { + string response; + size_t name_len = strlen(input.data() + 0x0C) + 1; - uint32_t remote_address = bswap32(remote.sin_addr.s_addr); - uint32_t connect_address; - if (is_local_address(remote_address)) { - connect_address = this->local_connect_address; - } else { - connect_address = this->external_connect_address; - } + uint32_t connect_address_be = bswap32(connect_address); + response.append(input.substr(0, 2)); + response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); + response.append(input.substr(12, name_len)); + response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); + response.append(reinterpret_cast(&connect_address_be), 4); - string output = this->build_response(input, connect_address); - if (!output.empty()) { - sendto(pfd.fd, output.data(), output.size(), 0, - reinterpret_cast(&remote), remote_size); - } + sendto(fd, response.data(), response.size(), 0, + reinterpret_cast(&remote), remote_size); + } else { + log(WARNING, "[DNSServer] input query too small"); + print_data(stderr, input); } } } } - - -string DNSServer::build_response(const std::string& input, - uint32_t connect_address) { - - if (input.size() < 0x0C) { - return ""; - } - - string ret; - size_t name_len = strlen(input.data() + 0x0C) + 1; - - uint32_t connect_address_be = bswap32(connect_address); - ret.append(input.substr(0, 2)); - ret.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); - ret.append(input.substr(12, name_len)); - ret.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); - ret.append(reinterpret_cast(&connect_address_be), 4); - - return ret; -} diff --git a/DNSServer.hh b/DNSServer.hh index b7c21da5..d0489681 100644 --- a/DNSServer.hh +++ b/DNSServer.hh @@ -1,14 +1,17 @@ #pragma once -#include -#include +#include + +#include +#include #include #include class DNSServer { public: - DNSServer(uint32_t local_connect_address, uint32_t external_connect_address); + DNSServer(std::shared_ptr base, + uint32_t local_connect_address, uint32_t external_connect_address); DNSServer(const DNSServer&) = delete; DNSServer(DNSServer&&) = delete; virtual ~DNSServer(); @@ -18,20 +21,13 @@ public: void listen(int port); void add_socket(int fd); - virtual void start(); - virtual void schedule_stop(); - virtual void wait_for_stop(); - private: - std::atomic should_exit; - std::thread t; - - std::set fds; - + std::shared_ptr base; + std::unordered_map> fd_to_receive_event; uint32_t local_connect_address; uint32_t external_connect_address; - void run_thread(); - static std::string build_response(const std::string& input, - uint32_t connect_address); + static void dispatch_on_receive_message(evutil_socket_t fd, short events, + void* ctx); + void on_receive_message(int fd, short event); }; diff --git a/FileContentsCache.cc b/FileContentsCache.cc index 82a800de..eba140ff 100644 --- a/FileContentsCache.cc +++ b/FileContentsCache.cc @@ -15,24 +15,18 @@ shared_ptr FileContentsCache::get(const std::string& name) { uint64_t t = now(); try { - lock_guard g(this->lock); auto& entry = this->name_to_file.at(name); if (t - entry.load_time < 300000000) { // not 5 minutes old? return it return entry.contents; } } catch (const out_of_range& e) { } - shared_ptr contents(new string(load_file(name))); - - { - lock_guard g(this->lock); - this->name_to_file.erase(name); - this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name), - forward_as_tuple(name, contents, t)); - } - - return contents; + shared_ptr contents(new string(load_file(name))); + this->name_to_file.erase(name); + this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name), + forward_as_tuple(name, contents, t)); + return contents; } shared_ptr FileContentsCache::get(const char* name) { diff --git a/FileContentsCache.hh b/FileContentsCache.hh index ffa0ead6..f505b003 100644 --- a/FileContentsCache.hh +++ b/FileContentsCache.hh @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -38,5 +37,4 @@ public: private: std::unordered_map name_to_file; - mutable std::mutex lock; }; diff --git a/Items.cc b/Items.cc index 4224e1a6..26cace29 100644 --- a/Items.cc +++ b/Items.cc @@ -141,7 +141,7 @@ using namespace std; //////////////////////////////////////////////////////////////////////////////// -void player_use_item_locked(shared_ptr l, shared_ptr c, +void player_use_item(shared_ptr l, shared_ptr c, size_t item_index) { ssize_t equipped_weapon = -1; diff --git a/Items.hh b/Items.hh index 1457fbaf..357fdee6 100644 --- a/Items.hh +++ b/Items.hh @@ -7,7 +7,7 @@ #include "Lobby.hh" #include "Client.hh" -void player_use_item_locked(std::shared_ptr l, std::shared_ptr c, +void player_use_item(std::shared_ptr l, std::shared_ptr c, size_t item_index); struct CommonItemCreator { diff --git a/License.cc b/License.cc index 967ca12b..e5ecb5ef 100644 --- a/License.cc +++ b/License.cc @@ -53,7 +53,7 @@ LicenseManager::LicenseManager(const std::string& filename) : filename(filename) } } -void LicenseManager::save_locked() const { +void LicenseManager::save() const { auto f = fopen_unique(this->filename, "wb"); for (const auto& it : this->serial_number_to_license) { fwritex(f.get(), it.second.get(), sizeof(License)); @@ -62,8 +62,6 @@ void LicenseManager::save_locked() const { shared_ptr LicenseManager::verify_pc(uint32_t serial_number, const char* access_key, const char* password) const { - rw_guard g(this->lock, false); - auto& license = this->serial_number_to_license.at(serial_number); if (strncmp(license->access_key, access_key, 8)) { throw invalid_argument("incorrect access key"); @@ -80,8 +78,6 @@ shared_ptr LicenseManager::verify_pc(uint32_t serial_number, shared_ptr LicenseManager::verify_gc(uint32_t serial_number, const char* access_key, const char* password) const { - rw_guard g(this->lock, false); - auto& license = this->serial_number_to_license.at(serial_number); if (strncmp(license->access_key, access_key, 12)) { throw invalid_argument("incorrect access key"); @@ -98,8 +94,6 @@ shared_ptr LicenseManager::verify_gc(uint32_t serial_number, shared_ptr LicenseManager::verify_bb(const char* username, const char* password) const { - rw_guard g(this->lock, false); - auto& license = this->bb_username_to_license.at(username); if (password && strcmp(license->bb_password, password)) { throw invalid_argument("incorrect password"); @@ -112,48 +106,36 @@ shared_ptr LicenseManager::verify_bb(const char* username, } size_t LicenseManager::count() const { - rw_guard g(this->lock, false); return this->serial_number_to_license.size(); } void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) { - rw_guard g(this->lock, false); this->serial_number_to_license.at(serial_number)->ban_end_time = end_time; - this->save_locked(); + this->save(); } void LicenseManager::add(shared_ptr l) { - { - rw_guard g(this->lock, true); - uint32_t serial_number = l->serial_number; - this->serial_number_to_license.emplace(serial_number, l); - if (l->username[0]) { - this->bb_username_to_license.emplace(l->username, l); - } + uint32_t serial_number = l->serial_number; + this->serial_number_to_license.emplace(serial_number, l); + if (l->username[0]) { + this->bb_username_to_license.emplace(l->username, l); } - rw_guard g(this->lock, false); - this->save_locked(); + this->save(); } void LicenseManager::remove(uint32_t serial_number) { - { - rw_guard g(this->lock, true); - auto l = this->serial_number_to_license.at(serial_number); - this->serial_number_to_license.erase(l->serial_number); - if (l->username[0]) { - this->bb_username_to_license.erase(l->username); - } + auto l = this->serial_number_to_license.at(serial_number); + this->serial_number_to_license.erase(l->serial_number); + if (l->username[0]) { + this->bb_username_to_license.erase(l->username); } - rw_guard g(this->lock, false); - this->save_locked(); + this->save(); } vector LicenseManager::snapshot() const { vector ret; - - rw_guard g(this->lock, false); for (auto it : this->serial_number_to_license) { ret.emplace_back(*it.second); } diff --git a/License.hh b/License.hh index 5f72d25d..b3aa58e2 100644 --- a/License.hh +++ b/License.hh @@ -58,9 +58,7 @@ public: std::vector snapshot() const; protected: - void save_locked() const; - - mutable rw_lock lock; + void save() const; std::string filename; std::unordered_map> bb_username_to_license; diff --git a/Lobby.cc b/Lobby.cc index 10852c5b..34489e11 100644 --- a/Lobby.cc +++ b/Lobby.cc @@ -2,6 +2,8 @@ #include +#include + #include "SendCommands.hh" #include "Text.hh" @@ -11,8 +13,9 @@ using namespace std; Lobby::Lobby() : lobby_id(0), min_level(0), max_level(0xFFFFFFFF), next_game_item_id(0), version(GameVersion::GC), section_id(0), episode(1), - difficulty(0), mode(0), event(0), block(0), type(0), leader_id(0), - max_clients(12), flags(0), loading_quest_id(0) { + difficulty(0), mode(0), rare_seed(random_object()), event(0), + block(0), type(0), leader_id(0), max_clients(12), flags(0), + loading_quest_id(0) { for (size_t x = 0; x < 12; x++) { this->next_item_id[x] = 0; @@ -27,7 +30,7 @@ bool Lobby::is_game() const { return this->flags & LobbyFlag::IsGame; } -void Lobby::reassign_leader_on_client_departure_locked(size_t leaving_client_index) { +void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) { for (size_t x = 0; x < this->max_clients; x++) { if (x == leaving_client_index) { continue; @@ -41,7 +44,6 @@ void Lobby::reassign_leader_on_client_departure_locked(size_t leaving_client_ind } bool Lobby::any_client_loading() const { - rw_guard g(this->lock, false); for (size_t x = 0; x < this->max_clients; x++) { if (!this->clients[x].get()) { continue; @@ -53,7 +55,7 @@ bool Lobby::any_client_loading() const { return false; } -size_t Lobby::count_clients_locked() const { +size_t Lobby::count_clients() const { size_t ret = 0; for (size_t x = 0; x < this->max_clients; x++) { if (this->clients[x].get()) { @@ -63,49 +65,34 @@ size_t Lobby::count_clients_locked() const { return ret; } -size_t Lobby::count_clients() const { - rw_guard g(this->lock, false); - return this->count_clients_locked(); -} - void Lobby::add_client(shared_ptr c) { - rw_guard g(this->lock, true); - this->add_client_locked(c); -} - -void Lobby::add_client_locked(shared_ptr c) { ssize_t index; - for (index = this->max_clients - 1; index >= 0; index--) { + for (index = 0; index < max_clients; index++) { if (!this->clients[index].get()) { this->clients[index] = c; break; } } - if (index < 0) { + if (index >= max_clients) { throw out_of_range("no space left in lobby"); } c->lobby_client_id = index; c->lobby_id = this->lobby_id; // if there's no one else in the lobby, set the leader id as well - if (index == this->max_clients - 1) { - for (index = this->max_clients - 2; index >= 0; index--) { + if (index == 0) { + for (index = 1; index < max_clients; index++) { if (this->clients[index].get()) { break; } } - if (index < 0) { + if (index >= max_clients) { this->leader_id = c->lobby_client_id; } } } void Lobby::remove_client(shared_ptr c) { - rw_guard g(this->lock, true); - this->remove_client_locked(c); -} - -void Lobby::remove_client_locked(shared_ptr c) { if (this->clients[c->lobby_client_id] != c) { auto other_c = this->clients[c->lobby_client_id].get(); throw logic_error(string_printf( @@ -122,7 +109,7 @@ void Lobby::remove_client_locked(shared_ptr c) { c->lobby_id = 0; } - this->reassign_leader_on_client_departure_locked(c->lobby_client_id); + this->reassign_leader_on_client_departure(c->lobby_client_id); } void Lobby::move_client_to_lobby(shared_ptr dest_lobby, @@ -131,31 +118,18 @@ void Lobby::move_client_to_lobby(shared_ptr dest_lobby, return; } - // deadlock prevention: lock the lobbies in increasing order of memory address - vector guards; - uint8_t* this_ptr = reinterpret_cast(this); - uint8_t* dest_ptr = reinterpret_cast(dest_lobby.get()); - if (this_ptr < dest_ptr) { - guards.emplace_back(this->lock, true); - guards.emplace_back(dest_lobby->lock, true); - } else { - guards.emplace_back(dest_lobby->lock, true); - guards.emplace_back(this->lock, true); - } - - if (dest_lobby->count_clients_locked() >= dest_lobby->max_clients) { + if (dest_lobby->count_clients() >= dest_lobby->max_clients) { throw out_of_range("no space left in lobby"); } - this->remove_client_locked(c); - dest_lobby->add_client_locked(c); + this->remove_client(c); + dest_lobby->add_client(c); } shared_ptr Lobby::find_client(const char16_t* identifier, uint64_t serial_number) { - rw_guard g(this->lock, false); for (size_t x = 0; x < this->max_clients; x++) { if (!this->clients[x]) { continue; @@ -190,12 +164,10 @@ uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) { void Lobby::add_item(const PlayerInventoryItem& item) { - rw_guard g(this->lock, true); this->item_id_to_floor_item.emplace(item.data.item_id, item); } void Lobby::remove_item(uint32_t item_id, PlayerInventoryItem* item) { - rw_guard g(this->lock, true); auto item_it = this->item_id_to_floor_item.find(item_id); if (item_it == this->item_id_to_floor_item.end()) { throw out_of_range("item not present"); diff --git a/Lobby.hh b/Lobby.hh index 8c544376..4011d0a9 100644 --- a/Lobby.hh +++ b/Lobby.hh @@ -24,8 +24,6 @@ enum LobbyFlag { }; struct Lobby { - mutable rw_lock lock; - uint32_t lobby_id; uint32_t min_level; @@ -46,8 +44,9 @@ struct Lobby { uint8_t episode; uint8_t difficulty; uint8_t mode; - char16_t password[36]; - char16_t name[36]; + char16_t password[0x24]; + char16_t name[0x24]; + uint32_t rare_seed; //EP3_GAME_CONFIG* ep3; // only present if this is an Episode 3 game @@ -65,15 +64,12 @@ struct Lobby { bool is_game() const; - void reassign_leader_on_client_departure_locked(size_t leaving_client_id); + void reassign_leader_on_client_departure(size_t leaving_client_id); size_t count_clients() const; - size_t count_clients_locked() const; bool any_client_loading() const; void add_client(std::shared_ptr c); - void add_client_locked(std::shared_ptr c); void remove_client(std::shared_ptr c); - void remove_client_locked(std::shared_ptr c); void move_client_to_lobby(std::shared_ptr dest_lobby, std::shared_ptr c); diff --git a/Main.cc b/Main.cc index 54464753..dd340dc5 100644 --- a/Main.cc +++ b/Main.cc @@ -1,22 +1,24 @@ #include +#include #include -#include #include #include +#include #include #include #include -#include #include "NetworkAddresses.hh" #include "SendCommands.hh" #include "DNSServer.hh" +#include "ProxyServer.hh" #include "ServerState.hh" #include "Server.hh" #include "FileContentsCache.hh" #include "Text.hh" -#include "Shell.hh" +#include "ServerShell.hh" +#include "ProxyShell.hh" using namespace std; @@ -68,6 +70,17 @@ void populate_state_from_config(shared_ptr s, s->name = decode_sjis(d.at("ServerName")->as_string()); + try { + s->username = d.at("User")->as_string(); + if (s->username == "$SUDO_USER") { + const char* user_from_env = getenv("SUDO_USER"); + if (!user_from_env) { + throw runtime_error("configuration specifies $SUDO_USER, but variable is not defined"); + } + s->username = user_from_env; + } + } catch (const out_of_range&) { } + // TODO: make this configurable s->port_configuration = default_port_to_behavior; @@ -97,10 +110,9 @@ void populate_state_from_config(shared_ptr s, s->information_menu = information_menu; s->information_contents = information_contents; - s->num_threads = d.at("Threads")->as_int(); - if (s->num_threads == 0) { - s->num_threads = thread::hardware_concurrency(); - } + try { + s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string()); + } catch (const out_of_range&) { } auto local_address_str = d.at("LocalAddress")->as_string(); s->local_address = address_for_string(local_address_str.c_str()); @@ -128,17 +140,58 @@ void populate_state_from_config(shared_ptr s, +void drop_privileges(const string& username) { + if ((getuid() != 0) || (getgid() != 0)) { + throw runtime_error(string_printf( + "newserv was not started as root; can\'t switch to user %s", + username.c_str())); + } + + struct passwd* pw = getpwnam(username.c_str()); + if (!pw) { + string error = string_for_error(errno); + throw runtime_error(string_printf("user %s not found (%s)", + username.c_str(), error.c_str())); + } + + if (setgid(pw->pw_gid) != 0) { + string error = string_for_error(errno); + throw runtime_error(string_printf("can\'t switch to group %d (%s)", + pw->pw_gid, error.c_str())); + } + if (setuid(pw->pw_uid) != 0) { + string error = string_for_error(errno); + throw runtime_error(string_printf("can\'t switch to user %d (%s)", + pw->pw_uid, error.c_str())); + } + log(INFO, "switched to user %s (%d:%d)", username.c_str(), pw->pw_uid, + pw->pw_gid); +} + + + int main(int argc, char* argv[]) { log(INFO, "fuzziqer software newserv"); - signal(SIGPIPE, SIG_IGN); - if (evthread_use_pthreads()) { - log(ERROR, "cannot enable multithreading in libevent"); + string proxy_hostname; + int proxy_port = 0; + for (size_t x = 1; x < argc; x++) { + if (!strncmp(argv[x], "--proxy-destination=", 20)) { + auto netloc = parse_netloc(&argv[x][20], 9100); + proxy_hostname = netloc.first; + proxy_port = netloc.second; + } else { + throw invalid_argument(string_printf("unknown option: %s", argv[x])); + } } - log(INFO, "creating server state"); + signal(SIGPIPE, SIG_IGN); + shared_ptr state(new ServerState()); + log(INFO, "starting network subsystem"); + shared_ptr base(event_base_new(), event_base_free); + log(INFO, "reading network addresses"); state->all_addresses = get_local_address_list(); for (uint32_t addr : state->all_addresses) { @@ -150,55 +203,65 @@ int main(int argc, char* argv[]) { auto config_json = JSONObject::load("system/config.json"); populate_state_from_config(state, config_json); - log(INFO, "loading license list"); - state->license_manager.reset(new LicenseManager("system/licenses.nsi")); - - log(INFO, "loading battle parameters"); - state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); - - log(INFO, "loading level table"); - state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); - - log(INFO, "collecting quest metadata"); - state->quest_index.reset(new QuestIndex("system/quests")); - shared_ptr dns_server; if (state->run_dns_server) { - log(INFO, "starting dns server on port 53"); - dns_server.reset(new DNSServer(state->local_address, state->external_address)); + log(INFO, "starting dns server"); + dns_server.reset(new DNSServer(base, state->local_address, + state->external_address)); dns_server->listen("", 53); - dns_server->start(); } - log(INFO, "starting game server"); - shared_ptr game_server(new Server(state)); - for (const auto& it : state->port_configuration) { - game_server->listen("", it.second.port, it.second.version, it.second.behavior); + shared_ptr proxy_server; + shared_ptr game_server; + if (proxy_port) { + log(INFO, "starting proxy"); + sockaddr_storage proxy_destination_ss = make_sockaddr_storage( + proxy_hostname, proxy_port).first; + proxy_server.reset(new ProxyServer(base, proxy_destination_ss, proxy_port)); + + } else { + log(INFO, "starting game server"); + game_server.reset(new Server(base, state)); + for (const auto& it : state->port_configuration) { + game_server->listen("", it.second.port, it.second.version, it.second.behavior); + } + + log(INFO, "loading license list"); + state->license_manager.reset(new LicenseManager("system/licenses.nsi")); + + log(INFO, "loading battle parameters"); + state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); + + log(INFO, "loading level table"); + state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + + log(INFO, "collecting quest metadata"); + state->quest_index.reset(new QuestIndex("system/quests")); + } + + if (!state->username.empty()) { + log(INFO, "switching to user %s", state->username.c_str()); + drop_privileges(state->username); } - game_server->start(); bool should_run_shell = (state->run_shell_behavior == ServerState::RunShellBehavior::Always); if (state->run_shell_behavior == ServerState::RunShellBehavior::Default) { should_run_shell = isatty(fileno(stdin)); } + shared_ptr shell; if (should_run_shell) { log(INFO, "starting interactive shell"); - run_shell(state); - - } else { - for (;;) { - sigset_t s; - sigemptyset(&s); - sigsuspend(&s); + if (proxy_port) { + shell.reset(new ProxyShell(base, state, proxy_server)); + } else { + shell.reset(new ServerShell(base, state)); } } - log(INFO, "waiting for servers to terminate"); - dns_server->schedule_stop(); - game_server->schedule_stop(); - dns_server->wait_for_stop(); - game_server->wait_for_stop(); + log(INFO, "ready"); + event_base_dispatch(base.get()); + log(INFO, "normal shutdown"); return 0; } diff --git a/Makefile b/Makefile index f6143283..488512d7 100644 --- a/Makefile +++ b/Makefile @@ -2,19 +2,18 @@ OBJECTS=FileContentsCache.o Menu.o PSOProtocol.o Client.o Lobby.o \ ServerState.o Server.o License.o PSOEncryption.o Player.o SendCommands.o \ ChatCommands.o ReceiveSubcommands.o ReceiveCommands.o Version.o Items.o \ LevelTable.o Compression.o Quest.o RareItemSet.o Map.o NetworkAddresses.o \ - Text.o DNSServer.o Shell.o Main.o + Text.o DNSServer.o ProxyServer.o Shell.o ServerShell.o ProxyShell.o Main.o CXX=g++ CXXFLAGS=-I/opt/local/include -I/usr/local/include -std=c++14 -g -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -Werror -LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -levent_pthreads -lphosg -lpthread -lreadline -EXECUTABLE=newserv +LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -levent_pthreads -lphosg -lpthread -all: $(EXECUTABLE) +all: newserv -$(EXECUTABLE): $(OBJECTS) - $(CXX) $(OBJECTS) $(LDFLAGS) -o $(EXECUTABLE) +newserv: $(OBJECTS) + $(CXX) $(OBJECTS) $(LDFLAGS) -o newserv clean: find . -name \*.o -delete - rm -rf *.dSYM $(EXECUTABLE) gmon.out + rm -rf *.dSYM newserv newserv-dns gmon.out .PHONY: clean test diff --git a/PSODolphinConfig.py b/PSODolphinConfig.py index 22dc664f..985fb952 100644 --- a/PSODolphinConfig.py +++ b/PSODolphinConfig.py @@ -54,7 +54,6 @@ def main(argv): try: username = os.environ['SUDO_USER'] - print(username) except KeyError: print('$SUDO_USER not set; use `sudo -E`') return 1 @@ -72,6 +71,7 @@ def main(argv): os.set_inheritable(tap_fd, True) subprocess.check_call(['ifconfig', tap_name, args.tap_ip], stderr=subprocess.DEVNULL) subprocess.check_call(['ifconfig', tap_name, 'up'], stderr=subprocess.DEVNULL) + subprocess.check_call(['ifconfig', tap_name, 'mtu', '9000'], stderr=subprocess.DEVNULL) # 2. fork a Dolphin process, dropping privileges first print("starting dolphin") diff --git a/PSOEncryption.hh b/PSOEncryption.hh index e64389c5..8e816cfd 100644 --- a/PSOEncryption.hh +++ b/PSOEncryption.hh @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/Player.cc b/Player.cc index 6a1618bb..06e97fb7 100644 --- a/Player.cc +++ b/Player.cc @@ -37,6 +37,7 @@ PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { bb.level = this->level; bb.experience = this->experience; bb.meseta = this->meseta; + memset(bb.guild_card, 0, sizeof(bb.guild_card)); strcpy(bb.guild_card, " 0"); bb.unknown3[0] = this->unknown3[0]; bb.unknown3[1] = this->unknown3[1]; @@ -59,6 +60,7 @@ PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { bb.hair_b = this->hair_b; bb.proportion_x = this->proportion_x; bb.proportion_y = this->proportion_y; + memset(bb.name, 0, sizeof(bb.name)); decode_sjis(bb.name, this->name, 0x10); add_language_marker_inplace(bb.name, 'J', 0x10); memcpy(&bb.config, &this->config, 0x48); @@ -103,6 +105,7 @@ PlayerDispDataPCGC PlayerDispDataBB::to_pcgc() const { pcgc.hair_b = this->hair_b; pcgc.proportion_x = this->proportion_x; pcgc.proportion_y = this->proportion_y; + memset(pcgc.name, 0, sizeof(pcgc.name)); encode_sjis(pcgc.name, this->name, 0x10); remove_language_marker_inplace(pcgc.name); memcpy(&pcgc.config, &this->config, 0x48); @@ -115,6 +118,7 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { PlayerDispDataBBPreview pre; pre.level = this->level; pre.experience = this->experience; + memset(pre.guild_card, 0, sizeof(pre.guild_card)); strcpy(pre.guild_card, this->guild_card); pre.unknown3[0] = this->unknown3[0]; pre.unknown3[1] = this->unknown3[1]; @@ -137,6 +141,7 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { pre.hair_b = this->hair_b; pre.proportion_x = this->proportion_x; pre.proportion_y = this->proportion_y; + memset(pre.name, 0, sizeof(pre.name)); char16cpy(pre.name, this->name, 16); pre.play_time = this->play_time; return pre; @@ -145,6 +150,7 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { this->level = pre.level; this->experience = pre.experience; + memset(this->guild_card, 0, sizeof(this->guild_card)); strcpy(this->guild_card, pre.guild_card); this->unknown3[0] = pre.unknown3[0]; this->unknown3[1] = pre.unknown3[1]; @@ -167,6 +173,7 @@ void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { this->hair_b = pre.hair_b; this->proportion_x = pre.proportion_x; this->proportion_y = pre.proportion_y; + memset(this->name, 0, sizeof(this->name)); char16cpy(this->name, pre.name, 16); this->play_time = 0; } @@ -190,8 +197,10 @@ void Player::import(const PSOPlayerDataPC& pc) { this->inventory = pc.inventory; this->disp = pc.disp.to_bb(); /* TODO: fix and re-enable this functionality + memset(this->info_board, 0, sizeof(this->info_board)); decode_sjis(this->info_board, pc->info_board); memcpy(&this->blocked, pc->blocked, sizeof(uint32_t) * 30); + memset(this->auto_reply, 0, sizeof(this->auto_reply)); if (pc->auto_reply_enabled) { decode_sjis(this->auto_reply, pc->auto_reply); } else {*/ @@ -202,8 +211,10 @@ void Player::import(const PSOPlayerDataPC& pc) { void Player::import(const PSOPlayerDataGC& gc) { this->inventory = gc.inventory; this->disp = gc.disp.to_bb(); + memset(this->info_board, 0, sizeof(this->info_board)); decode_sjis(this->info_board, gc.info_board, 0xAC); memcpy(&this->blocked, gc.blocked, sizeof(uint32_t) * 30); + memset(this->auto_reply, 0, sizeof(this->auto_reply)); if (gc.auto_reply_enabled) { decode_sjis(this->auto_reply, gc.auto_reply, 0xAC); } else { @@ -214,8 +225,10 @@ void Player::import(const PSOPlayerDataGC& gc) { void Player::import(const PSOPlayerDataBB& bb) { // note: we don't copy the inventory and disp here because we already have // it (we sent the player data to the client in the first place) + memset(this->info_board, 0, sizeof(this->info_board)); char16cpy(this->info_board, bb.info_board, 0xAC); memcpy(&this->blocked, bb.blocked, sizeof(uint32_t) * 30); + memset(this->auto_reply, 0, sizeof(this->auto_reply)); if (bb.auto_reply_enabled) { char16cpy(this->auto_reply, bb.auto_reply, 0xAC); } else { @@ -271,8 +284,11 @@ PlayerBB Player::export_bb_player_data() const { memcpy(bb.quest_data1, &this->quest_data1, 0x0208); bb.bank = this->bank; bb.serial_number = this->serial_number; + memset(bb.name, 0, sizeof(bb.name)); char16cpy(bb.name, this->disp.name, 24); + memset(bb.team_name, 0, sizeof(bb.team_name)); char16cpy(bb.team_name, this->team_name, 16); + memset(bb.guild_card_desc, 0, sizeof(bb.guild_card_desc)); char16cpy(bb.guild_card_desc, this->guild_card_desc, 0x58); bb.reserved1 = 0; bb.reserved2 = 0; @@ -281,7 +297,9 @@ PlayerBB Player::export_bb_player_data() const { bb.unknown3 = 0; memcpy(bb.symbol_chats, this->symbol_chats, 0x04E0); memcpy(bb.shortcuts, this->shortcuts, 0x0A40); + memset(bb.auto_reply, 0, sizeof(bb.auto_reply)); char16cpy(bb.auto_reply, this->auto_reply, 0xAC); + memset(bb.info_board, 0, sizeof(bb.info_board)); char16cpy(bb.info_board, this->info_board, 0xAC); memset(bb.unknown5, 0, 0x1C); memcpy(bb.challenge_data, this->challenge_data, 0x0140); @@ -325,6 +343,7 @@ void Player::load_account_data(const string& filename) { this->option_flags = account.option_flags; memcpy(&this->shortcuts, &account.shortcuts, 0x0A40); memcpy(&this->symbol_chats, &account.symbol_chats, 0x04E0); + memset(this->team_name, 0, sizeof(this->team_name)); char16cpy(this->team_name, account.team_name, 16); } @@ -338,6 +357,7 @@ void Player::save_account_data(const string& filename) const { account.option_flags = this->option_flags; memcpy(&account.shortcuts, &this->shortcuts, 0x0A40); memcpy(&account.symbol_chats, &this->symbol_chats, 0x04E0); + memset(account.team_name, 0, sizeof(account.team_name)); char16cpy(account.team_name, this->team_name, 16); save_file(filename, &account, sizeof(account)); @@ -350,16 +370,19 @@ void Player::load_player_data(const string& filename) { throw runtime_error("account data header is incorrect"); } + memset(this->auto_reply, 0, sizeof(this->auto_reply)); char16cpy(this->auto_reply, player.auto_reply, 0xAC); this->bank = player.bank; memcpy(&this->challenge_data, &player.challenge_data, 0x0140); this->disp = player.disp; - char16cpy(this->guild_card_desc,player.guild_card_desc, 0x58); - char16cpy(this->info_board,player.info_board, 0xAC); + memset(this->guild_card_desc, 0, sizeof(this->guild_card_desc)); + char16cpy(this->guild_card_desc, player.guild_card_desc, 0x58); + memset(this->info_board, 0, sizeof(this->info_board)); + char16cpy(this->info_board, player.info_board, 0xAC); this->inventory = player.inventory; - memcpy(&this->quest_data1,&player.quest_data1,0x0208); - memcpy(&this->quest_data2,&player.quest_data2,0x0058); - memcpy(&this->tech_menu_config,&player.tech_menu_config,0x0028); + memcpy(&this->quest_data1, &player.quest_data1, 0x0208); + memcpy(&this->quest_data2, &player.quest_data2, 0x0058); + memcpy(&this->tech_menu_config, &player.tech_menu_config, 0x0028); } void Player::save_player_data(const string& filename) const { @@ -367,16 +390,19 @@ void Player::save_player_data(const string& filename) const { strcpy(player.signature, PLAYER_FILE_SIGNATURE); player.preview = this->disp.to_preview(); + memset(player.auto_reply, 0, sizeof(player.auto_reply)); char16cpy(player.auto_reply, this->auto_reply, 0xAC); player.bank = this->bank; memcpy(&player.challenge_data, &this->challenge_data, 0x0140); player.disp = this->disp; + memset(player.guild_card_desc, 0, sizeof(player.guild_card_desc)); char16cpy(player.guild_card_desc,this->guild_card_desc, 0x58); - char16cpy(player.info_board,this->info_board, 0xAC); + memset(player.info_board, 0, sizeof(player.info_board)); + char16cpy(player.info_board, this->info_board, 0xAC); player.inventory = this->inventory; - memcpy(&player.quest_data1,&this->quest_data1,0x0208); - memcpy(&player.quest_data2,&this->quest_data2,0x0058); - memcpy(&player.tech_menu_config,&this->tech_menu_config,0x0028); + memcpy(&player.quest_data1, &this->quest_data1, 0x0208); + memcpy(&player.quest_data2, &this->quest_data2, 0x0058); + memcpy(&player.tech_menu_config, &this->tech_menu_config, 0x0028); save_file(filename, &player, sizeof(player)); } diff --git a/ProxyServer.cc b/ProxyServer.cc new file mode 100644 index 00000000..c8da84ae --- /dev/null +++ b/ProxyServer.cc @@ -0,0 +1,344 @@ +#include "ProxyServer.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "PSOProtocol.hh" +#include "ReceiveCommands.hh" + +using namespace std; + + + +ProxyServer::ProxyServer(shared_ptr base, + const struct sockaddr_storage& initial_destination, int listen_port) : + base(base), listener(evconnlistener_new(this->base.get(), + ProxyServer::dispatch_on_listen_accept, this, LEV_OPT_REUSEABLE, 0, + ::listen("", listen_port, SOMAXCONN)), evconnlistener_free), + client_bev(nullptr, bufferevent_free), + server_bev(nullptr, bufferevent_free), + next_destination(initial_destination), listen_port(listen_port) { + memset(&this->client_input_header, 0, sizeof(this->client_input_header)); + memset(&this->server_input_header, 0, sizeof(this->server_input_header)); +} + + + +void ProxyServer::send_to_client(const std::string& data) { + this->send_to_end(data, false); +} + +void ProxyServer::send_to_server(const std::string& data) { + this->send_to_end(data, true); +} + +void ProxyServer::send_to_end(const std::string& data, bool to_server) { + struct bufferevent* bev = to_server ? this->server_bev.get() : this->client_bev.get(); + if (!bev) { + throw runtime_error("connection not open"); + } + + struct evbuffer* buf = bufferevent_get_output(bev); + + PSOEncryption* crypt = to_server ? this->server_output_crypt.get() : this->client_output_crypt.get(); + if (crypt) { + string crypted_data = data; + crypt->encrypt(const_cast(crypted_data.data()), crypted_data.size()); + evbuffer_add(buf, crypted_data.data(), crypted_data.size()); + } else { + evbuffer_add(buf, data.data(), data.size()); + } +} + + + +void ProxyServer::dispatch_on_listen_accept( + struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr* address, int socklen, void* ctx) { + reinterpret_cast(ctx)->on_listen_accept(listener, fd, address, + socklen); +} + +void ProxyServer::dispatch_on_listen_error(struct evconnlistener* listener, + void* ctx) { + reinterpret_cast(ctx)->on_listen_error(listener); +} + +void ProxyServer::dispatch_on_client_input(struct bufferevent* bev, void* ctx) { + reinterpret_cast(ctx)->on_client_input(bev); +} + +void ProxyServer::dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx) { + reinterpret_cast(ctx)->on_client_error(bev, events); +} + +void ProxyServer::dispatch_on_server_input(struct bufferevent* bev, void* ctx) { + reinterpret_cast(ctx)->on_server_input(bev); +} + +void ProxyServer::dispatch_on_server_error(struct bufferevent* bev, short events, + void* ctx) { + reinterpret_cast(ctx)->on_server_error(bev, events); +} + + + +void ProxyServer::on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr* address, int socklen) { + + if (this->client_bev.get()) { + log(WARNING, "ignoring client connection because client already exists"); + close(fd); + return; + } else { + log(INFO, "client connected"); + } + + this->client_bev.reset(bufferevent_socket_new(this->base.get(), fd, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); + + bufferevent_setcb(this->client_bev.get(), + &ProxyServer::dispatch_on_client_input, NULL, + &ProxyServer::dispatch_on_client_error, this); + bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE); + + // connect to the server, disconnecting first if needed + this->server_bev.reset(bufferevent_socket_new(this->base.get(), -1, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); + + // TODO: figure out why this copy is necessary... shouldn't we just be able to + // use the sockaddr_storage directly? + const struct sockaddr_in* sin_ss = reinterpret_cast(&this->next_destination); + if (sin_ss->sin_family != AF_INET) { + throw logic_error("ss not AF_INET"); + } + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = sin_ss->sin_port; + sin.sin_addr.s_addr = sin_ss->sin_addr.s_addr; + + string netloc_str = render_sockaddr_storage(this->next_destination); + log(INFO, "connecting to %s", netloc_str.c_str()); + if (bufferevent_socket_connect(this->server_bev.get(), + reinterpret_cast(&sin), sizeof(sin)) != 0) { + throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); + } + bufferevent_setcb(this->server_bev.get(), + &ProxyServer::dispatch_on_server_input, NULL, + &ProxyServer::dispatch_on_server_error, this); + bufferevent_enable(this->server_bev.get(), EV_READ | EV_WRITE); +} + +void ProxyServer::on_listen_error(struct evconnlistener* listener) { + int err = EVUTIL_SOCKET_ERROR(); + log(ERROR, "failure on listening socket %d: %d (%s)", + evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err)); + event_base_loopexit(this->base.get(), NULL); +} + +void ProxyServer::on_client_input(struct bufferevent* bev) { + this->receive_and_process_commands(false); +} + +void ProxyServer::on_server_input(struct bufferevent* bev) { + this->receive_and_process_commands(true); +} + +void ProxyServer::on_client_error(struct bufferevent* bev, short events) { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "error %d (%s) in client stream", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + log(INFO, "client has disconnected"); + this->client_bev.reset(); + // "forward" the disconnection to the server + this->server_bev.reset(); + + // disable encryption + this->server_input_crypt.reset(); + this->server_output_crypt.reset(); + this->client_input_crypt.reset(); + this->client_output_crypt.reset(); + } +} + +void ProxyServer::on_server_error(struct bufferevent* bev, short events) { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "error %d (%s) in server stream", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + log(INFO, "server has disconnected"); + this->server_bev.reset(); + // "forward" the disconnection to the client + this->client_bev.reset(); + + // disable encryption + this->server_input_crypt.reset(); + this->server_output_crypt.reset(); + this->client_input_crypt.reset(); + this->client_output_crypt.reset(); + } +} + +void ProxyServer::receive_and_process_commands(bool from_server) { + struct bufferevent* source_bev = from_server ? this->server_bev.get() : this->client_bev.get(); + struct bufferevent* dest_bev = from_server ? this->client_bev.get() : this->server_bev.get(); + + struct evbuffer* source_buf = bufferevent_get_input(source_bev); + struct evbuffer* dest_buf = dest_bev ? bufferevent_get_output(dest_bev) : NULL; + + PSOEncryption* source_crypt = from_server ? this->server_input_crypt.get() : this->client_input_crypt.get(); + PSOEncryption* dest_crypt = from_server ? this->client_output_crypt.get() : this->server_output_crypt.get(); + + PSOCommandHeaderDCGC* input_header = from_server ? &this->server_input_header : &this->client_input_header; + + for (;;) { + if (input_header->size == 0) { + ssize_t bytes = evbuffer_copyout(source_buf, input_header, + sizeof(*input_header)); + //log(INFO, "[ProxyServer-debug] %zd bytes copied for header", bytes); + if (bytes < sizeof(*input_header)) { + break; + } + + //log(INFO, "[ProxyServer-debug] received encrypted header"); + //print_data(stderr, input_header, sizeof(*input_header)); + + if (source_crypt) { + source_crypt->decrypt(input_header, sizeof(*input_header)); + } + } + + if (evbuffer_get_length(source_buf) < input_header->size) { + //log(INFO, "[ProxyServer-debug] insufficient data for command (%zX/%hX bytes)", evbuffer_get_length(source_buf), input_header->size); + break; + } + + string command(input_header->size, '\0'); + ssize_t bytes = evbuffer_remove(source_buf, + const_cast(command.data()), input_header->size); + if (bytes < input_header->size) { + throw logic_error("enough bytes available, but could not remove them"); + } + //log(INFO, "[ProxyServer-debug] read command (%zX bytes)", bytes); + // overwrite the header with the already-decrypted header + memcpy(const_cast(command.data()), input_header, + sizeof(*input_header)); + + //log(INFO, "[ProxyServer-debug] received encrypted command with pre-decrypted header"); + //print_data(stderr, command); + + if (source_crypt) { + source_crypt->decrypt( + const_cast(command.data() + sizeof(*input_header)), + input_header->size - sizeof(*input_header)); + } + + log(INFO, "%s:", from_server ? "server" : "client"); + print_data(stderr, command); + + // preprocess the command if needed + if (from_server) { + switch (input_header->command) { + case 0x02: // init encryption + case 0x17: { // init encryption + struct InitEncryptionCommand { + PSOCommandHeaderDCGC header; + char copyright[0x40]; + uint32_t server_key; + uint32_t client_key; + }; + if (command.size() < sizeof(InitEncryptionCommand)) { + throw std::runtime_error("init encryption command is too small"); + } + + const InitEncryptionCommand* cmd = reinterpret_cast( + command.data()); + this->server_input_crypt.reset(new PSOGCEncryption(cmd->server_key)); + this->server_output_crypt.reset(new PSOGCEncryption(cmd->client_key)); + this->client_input_crypt.reset(new PSOGCEncryption(cmd->client_key)); + this->client_output_crypt.reset(new PSOGCEncryption(cmd->server_key)); + break; + } + + case 0x19: { // reconnect + struct ReconnectCommand { + PSOCommandHeaderDCGC header; + uint32_t address; + uint16_t port; + uint16_t unused; + }; + if (command.size() < sizeof(ReconnectCommand)) { + throw std::runtime_error("init encryption command is too small"); + } + + ReconnectCommand* cmd = reinterpret_cast( + const_cast(command.data())); + memset(&this->next_destination, 0, sizeof(this->next_destination)); + struct sockaddr_in* sin = reinterpret_cast( + &this->next_destination); + sin->sin_family = AF_INET; + sin->sin_port = htons(cmd->port); + sin->sin_addr.s_addr = cmd->address; // already network byte order + + if (!dest_bev) { + log(WARNING, "received reconnect command with no destination present"); + } else { + struct sockaddr_storage sockname_ss; + socklen_t len = sizeof(sockname_ss); + getsockname(bufferevent_getfd(dest_bev), + reinterpret_cast(&sockname_ss), &len); + if (sockname_ss.ss_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); + } + + struct sockaddr_in* sockname_sin = reinterpret_cast( + &sockname_ss); + cmd->address = sockname_sin->sin_addr.s_addr; // already network byte order + cmd->port = this->listen_port; + } + break; + } + } + } + + // reencrypt and forward the command + if (dest_buf) { + if (dest_crypt) { + dest_crypt->encrypt(const_cast(command.data()), command.size()); + } + //log(INFO, "[ProxyServer-debug] sending encrypted command"); + //print_data(stderr, command); + + evbuffer_add(dest_buf, command.data(), command.size()); + } else { + log(WARNING, "no destination present; dropping command"); + } + + // clear the input header so we can read the next command + memset(input_header, 0, sizeof(*input_header)); + } +} diff --git a/ProxyServer.hh b/ProxyServer.hh new file mode 100644 index 00000000..61cd9fc1 --- /dev/null +++ b/ProxyServer.hh @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "PSOEncryption.hh" +#include "PSOProtocol.hh" + + + +class ProxyServer { +public: + ProxyServer() = delete; + ProxyServer(const ProxyServer&) = delete; + ProxyServer(ProxyServer&&) = delete; + ProxyServer(std::shared_ptr base, + const struct sockaddr_storage& initial_destination, int listen_port); + virtual ~ProxyServer() = default; + + void send_to_client(const std::string& data); + void send_to_server(const std::string& data); + +private: + std::shared_ptr base; + std::unique_ptr listener; + std::unique_ptr client_bev; + std::unique_ptr server_bev; + struct sockaddr_storage next_destination; + int listen_port; + + PSOCommandHeaderDCGC client_input_header; + PSOCommandHeaderDCGC server_input_header; + std::shared_ptr client_input_crypt; + std::shared_ptr client_output_crypt; + std::shared_ptr server_input_crypt; + std::shared_ptr server_output_crypt; + + void send_to_end(const std::string& data, bool to_server); + + static void dispatch_on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); + static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + static void dispatch_on_server_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_server_error(struct bufferevent* bev, short events, + void* ctx); + + void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr *address, int socklen); + void on_listen_error(struct evconnlistener* listener); + void on_client_input(struct bufferevent* bev); + void on_client_error(struct bufferevent* bev, short events); + void on_server_input(struct bufferevent* bev); + void on_server_error(struct bufferevent* bev, short events); + + void receive_and_process_commands(bool from_server); +}; diff --git a/ProxyShell.cc b/ProxyShell.cc new file mode 100644 index 00000000..0f2c3fb5 --- /dev/null +++ b/ProxyShell.cc @@ -0,0 +1,81 @@ +#include "ProxyShell.hh" + +#include +#include + +#include + +using namespace std; + + + +ProxyShell::ProxyShell(std::shared_ptr base, + std::shared_ptr state, + std::shared_ptr proxy_server) : Shell(base, state), + proxy_server(proxy_server) { } + +void ProxyShell::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); + + if (command_name == "exit") { + throw exit_shell(); + + } else if (command_name == "help") { + fprintf(stderr, "\ +commands:\n\ + help\n\ + you\'re reading it now\n\ + exit (or ctrl+d)\n\ + shut down the proxy\n\ + sc \n\ + send a command to the client\n\ + ss \n\ + send a command to the server\n\ + chat \n\ + send a chat message to the server\n\ +"); + + } else if ((command_name == "sc") || (command_name == "ss")) { + bool to_client = (command_name[1] == 'c'); + string data = parse_data_string(command_args); + if (data.size() & 3) { + throw invalid_argument("data size is not a multiple of 4"); + } + if (data.size() == 0) { + throw invalid_argument("no data given"); + } + uint16_t* size_field = reinterpret_cast(const_cast(data.data() + 2)); + *size_field = data.size(); + + log(INFO, "%s (from proxy):", to_client ? "server" : "client"); + print_data(stderr, data); + + if (to_client) { + this->proxy_server->send_to_client(data); + } else { + this->proxy_server->send_to_server(data); + } + + } else if (command_name == "chat") { + string data(12, '\0'); + data[0] = 0x06; + data.push_back('\x09'); + data.push_back('E'); + data += command_args; + data.push_back('\0'); + data.resize((data.size() + 3) & (~3)); + uint16_t* size_field = reinterpret_cast(const_cast(data.data() + 2)); + *size_field = data.size(); + + log(INFO, "client (from proxy):"); + print_data(stderr, data); + this->proxy_server->send_to_server(data); + + } else { + throw invalid_argument("unknown command; try \'help\'"); + } +} diff --git a/ProxyShell.hh b/ProxyShell.hh new file mode 100644 index 00000000..97168baa --- /dev/null +++ b/ProxyShell.hh @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include + +#include "Shell.hh" +#include "ProxyServer.hh" + + + +class ProxyShell : public Shell { +public: + ProxyShell(std::shared_ptr base, + std::shared_ptr state, + std::shared_ptr proxy_server); + virtual ~ProxyShell() = default; + ProxyShell(const ProxyShell&) = delete; + ProxyShell(ProxyShell&&) = delete; + ProxyShell& operator=(const ProxyShell&) = delete; + ProxyShell& operator=(ProxyShell&&) = delete; + +protected: + std::shared_ptr proxy_server; + + virtual void execute_command(const std::string& command); +}; diff --git a/README.md b/README.md index f2de3c05..61ebe35c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Sometime in 2006 or 2007, I abandoned khyller and rebuilt the entire thing from A little-known fact is that no version of khyller or newserv was ever tested with the DreamCast versions of PSO. Both projects claimed to support them, but the DC server implementations were based only on chat conversations (likely now lost to time) with other people in the community who had done research on the DC version. -Last weekend (October 2018), I had some random cause to reminisce. I looked back in my old code archives and came across newserv. Somehow inspired, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and the concurrency primitives it uses are correct (thought I haven't audited where exactly they're used; there are likely some missing lock contexts still). +Sometime in October 2018, I had some random cause to reminisce. I looked back in my old code archives and came across newserv. Somehow inspired, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and it no longer has insidious concurrency bugs because it's no longer concurrent - the server is now entirely event-driven. ## Future @@ -27,7 +27,7 @@ This project is primarily for my own nostalgia. Feel free to peruse if you'd lik Currently this code should build on macOS and Ubuntu. It might build on other Linux flavors, but don't expect it to work on Windows at all. So, you've read all of the above and you want to try it out? Here's what you do: -- Make sure you have libreadline and libevent installed (use Homebrew in macOS, or install libreadline-dev and libevent-dev in Linux). +- Make sure you have libevent installed (use Homebrew in macOS, or install libevent-dev in Linux). - Build and install phosg (https://github.com/fuzziqersoftware/phosg). - Run `make`. - Edit system/config.json to your liking. diff --git a/ReceiveCommands.cc b/ReceiveCommands.cc index 9f118474..8bef0af4 100644 --- a/ReceiveCommands.cc +++ b/ReceiveCommands.cc @@ -51,26 +51,39 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) } case ServerBehavior::LoginServer: - case ServerBehavior::LobbyServer: + if (!s->welcome_message.empty() && !(c->flags & ClientFlag::NoMessageBoxCloseConfirmation)) { + c->flags |= ClientFlag::AtWelcomeMessage; + } send_server_init(c, true); break; + case ServerBehavior::LobbyServer: case ServerBehavior::DataServerBB: case ServerBehavior::PatchServer: send_server_init(c, false); break; + + default: + log(ERROR, "unimplemented behavior: %" PRId64, + static_cast(c->server_behavior)); } } void process_login_complete(shared_ptr s, shared_ptr c) { if (c->server_behavior == ServerBehavior::LoginServer) { - // on the login server, send the ep3 updates and the main menu + // on the login server, send the ep3 updates and the main menu or welcome + // message if (c->flags & ClientFlag::Episode3Games) { send_ep3_card_list_update(c); send_ep3_rank_update(c); } - send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + if (s->welcome_message.empty()) { + c->flags &= ~ClientFlag::AtWelcomeMessage; + send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + } else { + send_message_box(c, s->welcome_message.c_str()); + } } else if (c->server_behavior == ServerBehavior::LobbyServer) { @@ -166,7 +179,7 @@ void process_verify_license_gc(shared_ptr s, shared_ptr c, } c->flags |= flags_for_version(c->version, cmd->sub_version); - send_command(c, 0x9A, 0x01); + send_command(c, 0x9A, 0x02); } void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, @@ -247,7 +260,8 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, ClientConfig cfg; uint8_t unused4[0x64]; }; - check_size(size, sizeof(Cmd)); + // sometimes the unused bytes aren't sent? + check_size(size, sizeof(Cmd) - 0x64, sizeof(Cmd)); const auto* cmd = reinterpret_cast(data); c->flags |= flags_for_version(c->version, cmd->sub_version); @@ -481,11 +495,11 @@ void process_ep3_server_data_request(shared_ptr s, shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, uint16_t size, const void* data) { // D6 - if (c->in_information_menu) { - // add a reference to ensure it's not destroyed by another thread - auto info_menu = s->information_menu; - send_menu(c, u"Information", INFORMATION_MENU_ID, *info_menu, false); - return; + if (c->flags & ClientFlag::InInformationMenu) { + send_menu(c, u"Information", INFORMATION_MENU_ID, *s->information_menu, false); + } else if (c->flags & ClientFlag::AtWelcomeMessage) { + send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + c->flags &= ~ClientFlag::AtWelcomeMessage; } } @@ -521,10 +535,8 @@ void process_menu_item_info_request(shared_ptr s, shared_ptrinformation_menu; - send_ship_info(c, info_menu->at(cmd->item_id + 1).description.c_str()); + send_ship_info(c, s->information_menu->at(cmd->item_id + 1).description.c_str()); } catch (const out_of_range&) { send_ship_info(c, u"$C6No such information exists."); } @@ -579,12 +591,11 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case MAIN_MENU_INFORMATION: { - auto info_menu = s->information_menu; - send_menu(c, u"Information", INFORMATION_MENU_ID, *info_menu, false); - c->in_information_menu = true; + case MAIN_MENU_INFORMATION: + send_menu(c, u"Information", INFORMATION_MENU_ID, + *s->information_menu, false); + c->flags |= ClientFlag::InInformationMenu; break; - } case MAIN_MENU_DISCONNECT: c->should_disconnect = true; @@ -599,14 +610,12 @@ void process_menu_selection(shared_ptr s, shared_ptr c, case INFORMATION_MENU_ID: { if (cmd->item_id == INFORMATION_MENU_GO_BACK) { - c->in_information_menu = false; + c->flags &= ~ClientFlag::InInformationMenu; send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); } else { try { - // add a reference to ensure it's not destroyed by another thread - auto info_menu = s->information_contents; - send_message_box(c, info_menu->at(cmd->item_id).c_str()); + send_message_box(c, s->information_contents->at(cmd->item_id).c_str()); } catch (const out_of_range&) { send_message_box(c, u"$C6No such information exists."); } @@ -725,7 +734,6 @@ void process_menu_selection(shared_ptr s, shared_ptr c, auto bin_contents = q->bin_contents(); auto dat_contents = q->dat_contents(); - rw_guard g(l->lock, true); if (q->joinable) { l->flags |= LobbyFlag::JoinableQuestInProgress; } else { @@ -740,7 +748,6 @@ void process_menu_selection(shared_ptr s, shared_ptr c, send_quest_file(c, bin_basename, *bin_contents, false, false); send_quest_file(c, dat_basename, *dat_contents, false, false); - rw_guard g(l->clients[x]->lock, true); l->clients[x]->flags |= ClientFlag::Loading; } break; @@ -889,25 +896,18 @@ void process_quest_ready(shared_ptr s, shared_ptr c, return; } - { - rw_guard g(c->lock, true); - c->flags &= ~ClientFlag::Loading; - } + c->flags &= ~ClientFlag::Loading; // check if any client is still loading // TODO: we need to handle clients disconnecting while loading. probably // process_client_disconnect needs to check for this case or something size_t x; - { - rw_guard g(l->lock, true); - - for (x = 0; x < l->max_clients; x++) { - if (!l->clients[x]) { - continue; - } - if (l->clients[x]->flags & ClientFlag::Loading) { - break; - } + for (x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + if (l->clients[x]->flags & ClientFlag::Loading) { + break; } } @@ -923,26 +923,23 @@ void process_quest_ready(shared_ptr s, shared_ptr c, void process_player_data(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 61 98 - { - // note: we add extra buffer on the end when checking sizes because the - // autoreply text is a variable length - rw_guard g(c->lock, true); - switch (c->version) { - case GameVersion::PC: - check_size(size, sizeof(PSOPlayerDataPC), sizeof(PSOPlayerDataPC) + 2 * 0xAC); - c->player.import(*reinterpret_cast(data)); - break; - case GameVersion::GC: - check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC); - c->player.import(*reinterpret_cast(data)); - break; - case GameVersion::BB: - check_size(size, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + 2 * 0xAC); - c->player.import(*reinterpret_cast(data)); - break; - default: - throw logic_error("player data command not implemented for version"); - } + // note: we add extra buffer on the end when checking sizes because the + // autoreply text is a variable length + switch (c->version) { + case GameVersion::PC: + check_size(size, sizeof(PSOPlayerDataPC), sizeof(PSOPlayerDataPC) + 2 * 0xAC); + c->player.import(*reinterpret_cast(data)); + break; + case GameVersion::GC: + check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC); + c->player.import(*reinterpret_cast(data)); + break; + case GameVersion::BB: + check_size(size, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + 2 * 0xAC); + c->player.import(*reinterpret_cast(data)); + break; + default: + throw logic_error("player data command not implemented for version"); } if (command == 0x61 && !c->pending_bb_save_username.empty()) { @@ -1029,7 +1026,6 @@ void process_chat_generic(shared_ptr s, shared_ptr c, return; } - rw_guard g(l->lock, false); for (size_t x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { continue; @@ -1581,11 +1577,11 @@ void process_client_ready(shared_ptr s, shared_ptr c, c->flags &= (~ClientFlag::Loading); // tell the other players to stop waiting for the new player to load - send_resume_game(l); + send_resume_game(l, c); // tell the new player the time - send_server_time(c); + //send_server_time(c); // get character info - send_get_player_info(c); + //send_get_player_info(c); } //////////////////////////////////////////////////////////////////////////////// @@ -2104,9 +2100,39 @@ static process_command_t* handlers[6] = { void process_command(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, uint16_t size, const void* data) { - log(INFO, "received version=%d size=%04hX command=%04hX flag=%08X", - static_cast(c->version), size, command, flag); - print_data(stderr, data, size); + // TODO: this is slow; make it better somehow + { + log(INFO, "received version=%d size=%04hX command=%04hX flag=%08X", + static_cast(c->version), size, command, flag); + + string data_to_print; + if (c->version == GameVersion::BB) { + data_to_print.resize(size + 8); + PSOCommandHeaderBB* header = reinterpret_cast( + const_cast(data_to_print.data())); + header->command = command; + header->flag = flag; + header->size = size + 8; + memcpy(const_cast(data_to_print.data() + 8), data, size); + } else if (c->version == GameVersion::PC) { + data_to_print.resize(size + 4); + PSOCommandHeaderPC* header = reinterpret_cast( + const_cast(data_to_print.data())); + header->command = command; + header->flag = flag; + header->size = size + 4; + memcpy(const_cast(data_to_print.data() + 4), data, size); + } else { // DC/GC + data_to_print.resize(size + 4); + PSOCommandHeaderDCGC* header = reinterpret_cast( + const_cast(data_to_print.data())); + header->command = command; + header->flag = flag; + header->size = size + 4; + memcpy(const_cast(data_to_print.data() + 4), data, size); + } + print_data(stderr, data_to_print); + } auto fn = handlers[static_cast(c->version)][command & 0xFF]; if (fn) { diff --git a/ReceiveSubcommands.cc b/ReceiveSubcommands.cc index 88cec0d7..874bf662 100644 --- a/ReceiveSubcommands.cc +++ b/ReceiveSubcommands.cc @@ -60,16 +60,20 @@ void forward_subcommand(shared_ptr l, shared_ptr c, if (command_is_private(command)) { if (flag >= l->max_clients) { + log(INFO, "[subcommand-debug] skipping forwarding command; flag=%hhX and max_clients=%zu", + flag, l->max_clients); return; } auto target = l->clients[flag]; if (!target) { + log(INFO, "[subcommand-debug] skipping forwarding command; target is missing"); return; } + log(INFO, "[subcommand-debug] forwarding command"); send_command(target, command, flag, p, count * 4); } else { - // TODO: don't send the command back to the client it originated from - send_command(l, command, flag, p, count * 4); + log(INFO, "[subcommand-debug] not private (%02hhX)", command); + send_command_excluding_client(l, c, command, flag, p, count * 4); } } @@ -208,12 +212,7 @@ static void process_subcommand_drop_item(shared_ptr s, } PlayerInventoryItem item; - { - rw_guard g(c->lock, true); - c->player.remove_item(cmd->item_id, 0, &item); - } - - // note: this locks the lobby itself; we don't need to manually do it + c->player.remove_item(cmd->item_id, 0, &item); l->add_item(item); } forward_subcommand(l, c, command, flag, p, count); @@ -245,10 +244,7 @@ static void process_subcommand_drop_stacked_item(shared_ptr s, } PlayerInventoryItem item; - { - rw_guard g(c->lock, true); - c->player.remove_item(cmd->item_id, cmd->amount, &item); - } + c->player.remove_item(cmd->item_id, cmd->amount, &item); // if a stack was split, the original item still exists, so the dropped item // needs a new ID. remove_item signals this by returning an item with id=-1 @@ -256,7 +252,6 @@ static void process_subcommand_drop_stacked_item(shared_ptr s, item.data.item_id = l->generate_item_id(c->lobby_client_id); } - // note: this locks the lobby itself; we don't need to manually do it l->add_item(item); send_drop_stacked_item(l, c, item.data, cmd->area, cmd->x, cmd->y); @@ -290,11 +285,7 @@ static void process_subcommand_pick_up_item(shared_ptr s, PlayerInventoryItem item; l->remove_item(cmd->item_id, &item); - - { - rw_guard g(c->lock, true); - c->player.add_item(item); - } + c->player.add_item(item); send_pick_up_item(l, c, item.data.item_id, cmd->area); @@ -315,7 +306,6 @@ static void process_subcommand_equip_unequip_item(shared_ptr s, return; } - rw_guard g(c->lock, true); size_t index = c->player.inventory.find_item(cmd->item_id); if (cmd->command == 0x25) { c->player.inventory.items[index].game_flags |= 0x00000008; // equip @@ -339,7 +329,6 @@ static void process_subcommand_use_item(shared_ptr s, return; } - rw_guard g(c->lock, true); size_t index = c->player.inventory.find_item(cmd->item_id); if (cmd->command == 0x25) { c->player.inventory.items[index].game_flags |= 0x00000008; // equip @@ -347,7 +336,7 @@ static void process_subcommand_use_item(shared_ptr s, c->player.inventory.items[index].game_flags &= 0xFFFFFFF7; // unequip } - player_use_item_locked(l, c, index); + player_use_item(l, c, index); } forward_subcommand(l, c, command, flag, p, count); @@ -384,7 +373,6 @@ static void process_subcommand_bank_action(shared_ptr s, return; } - rw_guard g(c->lock, true); if (cmd->action == 0) { // deposit if (cmd->item_id == 0xFFFFFFFF) { // meseta if (cmd->meseta_amount > c->player.disp.meseta) { @@ -445,7 +433,6 @@ static void process_subcommand_sort_inventory(shared_ptr s, PlayerInventory sorted; memset(&sorted, 0, sizeof(PlayerInventory)); - rw_guard g(c->lock, true); for (size_t x = 0; x < 30; x++) { if (cmd->item_ids[x] == 0xFFFFFFFF) { sorted.items[x].data.item_id = 0xFFFFFFFF; @@ -525,6 +512,9 @@ static void process_subcommand_enemy_drop_item(shared_ptr s, l->add_item(item); send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, cmd->request_id); + + } else { + forward_subcommand(l, c, command, flag, p, count); } } @@ -594,6 +584,9 @@ static void process_subcommand_box_drop_item(shared_ptr s, l->add_item(item); send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, cmd->request_id); + + } else { + forward_subcommand(l, c, command, flag, p, count); } } @@ -622,7 +615,6 @@ static void process_subcommand_monster_hit(shared_ptr s, return; } - rw_guard g(l->lock, true); if (l->enemies[cmd->enemy_id].hit_flags & 0x80) { return; } @@ -664,7 +656,6 @@ static void process_subcommand_monster_killed(shared_ptr s, return; } - rw_guard g(l->lock, true); auto& enemy = l->enemies[cmd->enemy_id]; enemy.hit_flags |= 0x80; for (size_t x = 0; x < l->max_clients; x++) { @@ -688,7 +679,6 @@ static void process_subcommand_monster_killed(shared_ptr s, exp = ((enemy.experience * 77) / 100); } - rw_guard g(other_c->lock, true); other_c->player.disp.experience += exp; send_give_experience(l, other_c, exp); @@ -740,7 +730,6 @@ static void process_subcommand_identify_item(shared_ptr s, return; } - rw_guard g(c->lock, true); size_t x = c->player.inventory.find_item(cmd->item_id); if (c->player.inventory.items[x].data.item_data1[0] != 0) { return; // only weapons can be identified diff --git a/SendCommands.cc b/SendCommands.cc index 11d65107..55f45510 100644 --- a/SendCommands.cc +++ b/SendCommands.cc @@ -75,17 +75,28 @@ void send_command(shared_ptr c, uint16_t command, uint32_t flag, c->send(move(send_data)); } -void send_command(shared_ptr l, uint16_t command, uint32_t flag, - const void* data, size_t size) { - rw_guard g(l->lock, false); +void send_command_excluding_client(shared_ptr l, shared_ptr c, + uint16_t command, uint32_t flag, const void* data, size_t size) { for (auto& client : l->clients) { - if (!client) { + if (!client || (client == c)) { continue; } send_command(client, command, flag, data, size); } } +void send_command(shared_ptr l, uint16_t command, uint32_t flag, + const void* data, size_t size) { + send_command_excluding_client(l, NULL, command, flag, data, size); +} + +void send_command(shared_ptr s, uint16_t command, uint32_t flag, + const void* data, size_t size) { + for (auto& l : s->all_lobbies()) { + send_command(l, command, flag, data, size); + } +} + // specific command sending functions follow. in general, they're written in @@ -126,7 +137,6 @@ static void send_server_init_dc_pc_gc(shared_ptr c, const char* copyrigh strcpy(cmd.after_message, anti_copyright); send_command(c, command, 0x00, cmd); - rw_guard g(c->lock, true); switch (c->version) { case GameVersion::DC: case GameVersion::PC: @@ -169,7 +179,6 @@ static void send_server_init_bb(shared_ptr c, bool initial_connection) { strcpy(cmd.after_message, anti_copyright); send_command(c, 0x03, 0x00, cmd); - rw_guard(c->lock, true); c->crypt_out.reset(new PSOBBEncryption(cmd.server_key)); c->crypt_in.reset(new PSOBBEncryption(cmd.client_key)); } @@ -192,7 +201,6 @@ static void send_server_init_patch(shared_ptr c, bool initial_connection cmd.client_key = client_key; send_command(c, 0x02, 0x00, cmd); - rw_guard g(c->lock, true); c->crypt_out.reset(new PSOPCEncryption(server_key)); c->crypt_in.reset(new PSOPCEncryption(client_key)); } @@ -218,7 +226,7 @@ void send_update_client_config(shared_ptr c) { uint32_t serial_number; ClientConfig config; } cmd = { - 0x00000100, + 0x00010000, c->license->serial_number, c->config.cfg, }; @@ -275,7 +283,7 @@ void send_client_init_bb(shared_ptr c, uint32_t error) { uint32_t caps; // should be 0x00000102 } cmd = { error, - 0x00000100, + 0x00010000, c->license->serial_number, static_cast(random_object()), c->config, @@ -511,7 +519,6 @@ void send_text_message(shared_ptr c, const char16_t* text) { } void send_text_message(shared_ptr l, const char16_t* text) { - rw_guard g(l->lock, false); for (size_t x = 0; x < l->max_clients; x++) { if (l->clients[x]) { send_text_message(l->clients[x], text); @@ -519,6 +526,12 @@ void send_text_message(shared_ptr l, const char16_t* text) { } } +void send_text_message(shared_ptr s, const char16_t* text) { + for (auto& l : s->all_lobbies()) { + send_text_message(l, text); + } +} + void send_chat_message(shared_ptr c, uint32_t from_serial_number, const char16_t* from_name, const char16_t* text) { u16string data; @@ -543,20 +556,17 @@ static void send_info_board_pc_bb(shared_ptr c, shared_ptr l) { }; vector entries; - { - rw_guard g(l->lock, false); - for (const auto& c : l->clients) { - if (!c.get()) { - continue; - } - - entries.emplace_back(); - auto& e = entries.back(); - memset(&e, 0, sizeof(Entry)); - char16cpy(e.name, c->player.disp.name, 0x10); - char16cpy(e.message, c->player.info_board, 0xAC); - add_color_inplace(e.message); + for (const auto& c : l->clients) { + if (!c.get()) { + continue; } + + entries.emplace_back(); + auto& e = entries.back(); + memset(&e, 0, sizeof(Entry)); + char16cpy(e.name, c->player.disp.name, 0x10); + char16cpy(e.message, c->player.info_board, 0xAC); + add_color_inplace(e.message); } send_command(c, 0xD8, 0x00, entries); @@ -569,20 +579,17 @@ static void send_info_board_dc_gc(shared_ptr c, shared_ptr l) { }; vector entries; - { - rw_guard g(l->lock, false); - for (const auto& c : l->clients) { - if (!c.get()) { - continue; - } - - entries.emplace_back(); - auto& e = entries.back(); - memset(&e, 0, sizeof(Entry)); - encode_sjis(e.name, c->player.disp.name, 0x10); - encode_sjis(e.message, c->player.info_board, 0xAC); - add_color_inplace(e.message); + for (const auto& c : l->clients) { + if (!c.get()) { + continue; } + + entries.emplace_back(); + auto& e = entries.back(); + memset(&e, 0, sizeof(Entry)); + encode_sjis(e.name, c->player.disp.name, 0x10); + encode_sjis(e.message, c->player.info_board, 0xAC); + add_color_inplace(e.message); } send_command(c, 0xD8, 0x00, entries); @@ -634,7 +641,7 @@ static void send_card_search_result_dc_pc_gc(shared_ptr s, } cmd; memset(&cmd, 0, sizeof(cmd)); - cmd.player_tag = 0x00000100; + cmd.player_tag = 0x00010000; cmd.searcher_serial_number = c->license->serial_number; cmd.result_serial_number = result->license->serial_number; if (c->version == GameVersion::PC) { @@ -697,7 +704,7 @@ static void send_card_search_result_bb(shared_ptr s, } cmd; memset(&cmd, 0, sizeof(cmd)); - cmd.player_tag = 0x00000100; + cmd.player_tag = 0x00010000; cmd.searcher_serial_number = c->license->serial_number; cmd.result_serial_number = result->license->serial_number; cmd.destination_command.size = 0x0010; @@ -755,19 +762,16 @@ static void send_guild_card_gc(shared_ptr c, shared_ptr source) cmd.subcommand = 0x06; cmd.subsize = 0x25; cmd.unused = 0x0000; - cmd.player_tag = 0x00000100; + cmd.player_tag = 0x00010000; cmd.reserved1 = 1; cmd.reserved2 = 1; - { - rw_guard g(source->lock, false); - cmd.serial_number = source->license->serial_number; - encode_sjis(cmd.name, source->player.disp.name, 0x18); - remove_language_marker_inplace(cmd.name); - encode_sjis(cmd.desc, source->player.guild_card_desc, 0x6C); - cmd.section_id = source->player.disp.section_id; - cmd.char_class = source->player.disp.char_class; - } + cmd.serial_number = source->license->serial_number; + encode_sjis(cmd.name, source->player.disp.name, 0x18); + remove_language_marker_inplace(cmd.name); + encode_sjis(cmd.desc, source->player.guild_card_desc, 0x6C); + cmd.section_id = source->player.disp.section_id; + cmd.char_class = source->player.disp.char_class; send_command(c, 0x62, c->lobby_client_id, cmd); } @@ -793,17 +797,14 @@ static void send_guild_card_bb(shared_ptr c, shared_ptr source) cmd.reserved1 = 1; cmd.reserved2 = 1; - { - rw_guard g(source->lock, false); - cmd.serial_number = source->license->serial_number; - char16cpy(cmd.name, source->player.disp.name, 0x18); - remove_language_marker_inplace(cmd.name); - char16cpy(cmd.team_name, source->player.team_name, 0x10); - remove_language_marker_inplace(cmd.team_name); - char16cpy(cmd.desc, source->player.guild_card_desc, 0x58); - cmd.section_id = source->player.disp.section_id; - cmd.char_class = source->player.disp.char_class; - } + cmd.serial_number = source->license->serial_number; + char16cpy(cmd.name, source->player.disp.name, 0x18); + remove_language_marker_inplace(cmd.name); + char16cpy(cmd.team_name, source->player.team_name, 0x10); + remove_language_marker_inplace(cmd.team_name); + char16cpy(cmd.desc, source->player.guild_card_desc, 0x58); + cmd.section_id = source->player.disp.section_id; + cmd.char_class = source->player.disp.char_class; send_command(c, 0x62, c->lobby_client_id, cmd); } @@ -954,7 +955,6 @@ static void send_game_menu_pc(shared_ptr c, shared_ptr s) { auto& e = entries.back(); e.menu_id = GAME_MENU_ID; - rw_guard g(l->lock, false); e.game_id = l->lobby_id; e.difficulty_tag = l->difficulty + 0x22; e.num_players = l->count_clients(); @@ -998,7 +998,6 @@ static void send_game_menu_gc(shared_ptr c, shared_ptr s) { auto& e = entries.back(); e.menu_id = GAME_MENU_ID; - rw_guard g(l->lock, false); e.game_id = l->lobby_id; e.difficulty_tag = ((l->flags & LobbyFlag::Episode3) ? 0x0A : (l->difficulty + 0x22)); e.num_players = l->count_clients(); @@ -1046,7 +1045,6 @@ static void send_game_menu_bb(shared_ptr c, shared_ptr s) { auto& e = entries.back(); e.menu_id = GAME_MENU_ID; - rw_guard g(l->lock, false); e.game_id = l->lobby_id; e.difficulty_tag = l->difficulty + 0x22; e.num_players = l->count_clients(); @@ -1290,7 +1288,7 @@ static void send_join_game_pc(shared_ptr c, shared_ptr l) { uint8_t event; uint8_t section_id; uint8_t challenge_mode; - uint32_t game_id; // actually random number for rare monster selection; whatever + uint32_t rare_seed; uint8_t episode; uint8_t unused2; uint8_t solo_mode; @@ -1298,38 +1296,34 @@ static void send_join_game_pc(shared_ptr c, shared_ptr l) { } cmd; size_t player_count = 0; - { - rw_guard g(l->lock, false); - memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); - for (size_t x = 0; x < 4; x++) { - if (!l->clients[x]) { - memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataPC)); - } else { - rw_guard g(l->clients[x]->lock, false); - cmd.lobby_data[x].player_tag = 0x00000100; - cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - cmd.lobby_data[x].ip_address = 0xFFFFFFFF; - cmd.lobby_data[x].client_id = c->lobby_client_id; - char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); - player_count++; - } + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + if (!l->clients[x]) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataPC)); + } else { + cmd.lobby_data[x].player_tag = 0x00010000; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].ip_address = 0x00000000; + cmd.lobby_data[x].client_id = c->lobby_client_id; + char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + player_count++; } - - cmd.client_id = c->lobby_client_id; - cmd.leader_id = l->leader_id; - cmd.unused = 0x00; - cmd.difficulty = l->difficulty; - cmd.battle_mode = (l->mode == 1) ? 1 : 0; - cmd.event = l->event; - cmd.section_id = l->section_id; - cmd.challenge_mode = (l->mode == 2) ? 1 : 0; - cmd.game_id = l->lobby_id; - cmd.episode = 0x00; - cmd.unused2 = 0x01; - cmd.solo_mode = 0x00; - cmd.unused3 = 0x00; } + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.unused = 0x00; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.rare_seed = l->rare_seed; + cmd.episode = 0x00; + cmd.unused2 = 0x01; + cmd.solo_mode = 0x00; + cmd.unused3 = 0x00; + send_command(c, 0x64, player_count, cmd); } @@ -1339,13 +1333,13 @@ static void send_join_game_gc(shared_ptr c, shared_ptr l) { PlayerLobbyDataGC lobby_data[4]; uint8_t client_id; uint8_t leader_id; - uint8_t unused; + uint8_t disable_udp; // guess; putting 0 here causes no movement messages to be sent uint8_t difficulty; uint8_t battle_mode; uint8_t event; uint8_t section_id; uint8_t challenge_mode; - uint32_t game_id; // actually random number for rare monster selection; whatever + uint32_t rare_seed; uint32_t episode; // for PSOPC, this must be 0x00000100 struct { PlayerInventory inventory; @@ -1354,39 +1348,35 @@ static void send_join_game_gc(shared_ptr c, shared_ptr l) { } cmd; size_t player_count = 0; - { - rw_guard g(l->lock, false); - memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); - for (size_t x = 0; x < 4; x++) { - if (!l->clients[x]) { - memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataGC)); - } else { - rw_guard g(l->clients[x]->lock, false); - cmd.lobby_data[x].player_tag = 0x00000100; - cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - cmd.lobby_data[x].ip_address = 0xFFFFFFFF; - cmd.lobby_data[x].client_id = c->lobby_client_id; - encode_sjis(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); - if (l->flags & LobbyFlag::Episode3) { - cmd.player[x].inventory = l->clients[x]->player.inventory; - cmd.player[x].disp = l->clients[x]->player.disp.to_pcgc(); - } - player_count++; + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + if (!l->clients[x]) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataGC)); + } else { + cmd.lobby_data[x].player_tag = 0x00010000; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].ip_address = 0x00000000; + cmd.lobby_data[x].client_id = c->lobby_client_id; + encode_sjis(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + if (l->flags & LobbyFlag::Episode3) { + cmd.player[x].inventory = l->clients[x]->player.inventory; + cmd.player[x].disp = l->clients[x]->player.disp.to_pcgc(); } + player_count++; } - - cmd.client_id = c->lobby_client_id; - cmd.leader_id = l->leader_id; - cmd.unused = 0x00; - cmd.difficulty = l->difficulty; - cmd.battle_mode = (l->mode == 1) ? 1 : 0; - cmd.event = l->event; - cmd.section_id = l->section_id; - cmd.challenge_mode = (l->mode == 2) ? 1 : 0; - cmd.game_id = l->lobby_id; - cmd.episode = l->episode; } + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.disable_udp = 0x01; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.rare_seed = l->rare_seed; + cmd.episode = l->episode; + // player is only sent in ep3 games size_t data_size = (l->flags & LobbyFlag::Episode3) ? 0x1184 : 0x0110; send_command(c, 0x64, player_count, &cmd, data_size); @@ -1404,7 +1394,7 @@ static void send_join_game_bb(shared_ptr c, shared_ptr l) { uint8_t event; uint8_t section_id; uint8_t challenge_mode; - uint32_t game_id; // actually random number for rare monster selection; whatever + uint32_t rare_seed; uint8_t episode; uint8_t unused2; uint8_t solo_mode; @@ -1412,42 +1402,36 @@ static void send_join_game_bb(shared_ptr c, shared_ptr l) { } cmd; size_t player_count = 0; - { - rw_guard g(l->lock, false); - memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); - for (size_t x = 0; x < 4; x++) { - memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataBB)); - if (l->clients[x]) { - rw_guard g(l->clients[x]->lock, false); - cmd.lobby_data[x].player_tag = 0x00000100; - cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - cmd.lobby_data[x].client_id = c->lobby_client_id; - char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); - player_count++; - } + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataBB)); + if (l->clients[x]) { + cmd.lobby_data[x].player_tag = 0x00010000; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].client_id = c->lobby_client_id; + char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + player_count++; } - - cmd.client_id = c->lobby_client_id; - cmd.leader_id = l->leader_id; - cmd.unused = 0x00; - cmd.difficulty = l->difficulty; - cmd.battle_mode = (l->mode == 1) ? 1 : 0; - cmd.event = l->event; - cmd.section_id = l->section_id; - cmd.challenge_mode = (l->mode == 2) ? 1 : 0; - cmd.game_id = l->lobby_id; - cmd.episode = 0x00; - cmd.unused2 = 0x01; - cmd.solo_mode = 0x00; - cmd.unused3 = 0x00; } + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.unused = 0x00; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.rare_seed = l->rare_seed; + cmd.episode = 0x00; + cmd.unused2 = 0x01; + cmd.solo_mode = 0x00; + cmd.unused3 = 0x00; + send_command(c, 0x64, player_count, cmd); } static void send_join_lobby_pc(shared_ptr c, shared_ptr l) { - rw_guard g(l->lock, false); - uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; struct { uint8_t client_id; @@ -1481,10 +1465,9 @@ static void send_join_lobby_pc(shared_ptr c, shared_ptr l) { entries.emplace_back(); auto& e = entries.back(); - rw_guard g(l->clients[x]->lock, false); - e.lobby_data.player_tag = 0x00000100; + e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = l->clients[x]->license->serial_number; - e.lobby_data.ip_address = 0xFFFFFFFF; + e.lobby_data.ip_address = 0x00000000; e.lobby_data.client_id = l->clients[x]->lobby_client_id; char16cpy(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); e.data = l->clients[x]->player.export_lobby_data_pc(); @@ -1494,8 +1477,6 @@ static void send_join_lobby_pc(shared_ptr c, shared_ptr l) { } static void send_join_lobby_gc(shared_ptr c, shared_ptr l) { - rw_guard g(l->lock, false); - uint8_t lobby_type = l->type; if (c->flags & ClientFlag::Episode3Games) { if ((l->type > 0x14) && (l->type < 0xE9)) { @@ -1539,10 +1520,9 @@ static void send_join_lobby_gc(shared_ptr c, shared_ptr l) { entries.emplace_back(); auto& e = entries.back(); - rw_guard g(l->clients[x]->lock, false); - e.lobby_data.player_tag = 0x00000100; + e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = l->clients[x]->license->serial_number; - e.lobby_data.ip_address = 0xFFFFFFFF; + e.lobby_data.ip_address = 0x00000000; e.lobby_data.client_id = l->clients[x]->lobby_client_id; encode_sjis(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); e.data = l->clients[x]->player.export_lobby_data_gc(); @@ -1552,8 +1532,6 @@ static void send_join_lobby_gc(shared_ptr c, shared_ptr l) { } static void send_join_lobby_bb(shared_ptr c, shared_ptr l) { - rw_guard g(l->lock, false); - uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; struct { uint8_t client_id; @@ -1588,8 +1566,7 @@ static void send_join_lobby_bb(shared_ptr c, shared_ptr l) { auto& e = entries.back(); memset(&e.lobby_data, 0, sizeof(e.lobby_data)); - rw_guard g(l->clients[x]->lock, false); - e.lobby_data.player_tag = 0x00000100; + e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = l->clients[x]->license->serial_number; e.lobby_data.client_id = l->clients[x]->lobby_client_id; char16cpy(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); @@ -1651,7 +1628,7 @@ static void send_player_join_notification_pc(shared_ptr c, l->block, l->event, 0x00000000, - {0x00000100, joining_client->license->serial_number, 0xFFFFFFFF, joining_client->lobby_client_id, {0}}, + {0x00000100, joining_client->license->serial_number, 0x00000000, joining_client->lobby_client_id, {0}}, joining_client->player.export_lobby_data_pc(), }; char16cpy(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); @@ -1680,7 +1657,7 @@ static void send_player_join_notification_gc(shared_ptr c, l->block, l->event, 0x00000000, - {0x00000100, joining_client->license->serial_number, 0xFFFFFFFF, joining_client->lobby_client_id, {0}}, + {0x00000100, joining_client->license->serial_number, 0x00000000, joining_client->lobby_client_id, {0}}, joining_client->player.export_lobby_data_gc(), }; encode_sjis(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); @@ -1713,7 +1690,7 @@ static void send_player_join_notification_bb(shared_ptr c, joining_client->player.export_lobby_data_bb(), }; memset(&cmd.lobby_data, 0, sizeof(cmd.lobby_data)); - cmd.lobby_data.player_tag = 0x00000100; + cmd.lobby_data.player_tag = 0x00010000; cmd.lobby_data.guild_card = joining_client->license->serial_number; cmd.lobby_data.client_id = joining_client->lobby_client_id; char16cpy(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); @@ -1760,7 +1737,6 @@ void send_arrow_update(shared_ptr l) { }; vector entries; - rw_guard g(l->lock, false); for (size_t x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { continue; @@ -1768,7 +1744,7 @@ void send_arrow_update(shared_ptr l) { entries.emplace_back(); auto& e = entries.back(); - e.player_tag = 0x00000100; + e.player_tag = 0x00010000; e.serial_number = l->clients[x]->license->serial_number; e.arrow_color = l->clients[x]->lobby_arrow_color; } @@ -1777,9 +1753,9 @@ void send_arrow_update(shared_ptr l) { } // tells the player that the joining player is done joining, and the game can resume -void send_resume_game(shared_ptr l) { +void send_resume_game(shared_ptr l, shared_ptr ready_client) { uint32_t data = 0x081C0372; - send_command(l, 0x60, 0x00, &data, 4); + send_command_excluding_client(l, ready_client, 0x60, 0x00, &data, 4); } diff --git a/SendCommands.hh b/SendCommands.hh index 5b53222f..702c3191 100644 --- a/SendCommands.hh +++ b/SendCommands.hh @@ -30,12 +30,19 @@ -void send_command(std::shared_ptr c, uint16_t command, uint32_t flag = 0, +void send_command(std::shared_ptr c, uint16_t command, + uint32_t flag = 0, const void* data = NULL, size_t size = 0); + +void send_command_excluding_client(std::shared_ptr l, + std::shared_ptr c, uint16_t command, uint32_t flag = 0, const void* data = NULL, size_t size = 0); void send_command(std::shared_ptr l, uint16_t command, uint32_t flag = 0, const void* data = NULL, size_t size = 0); +void send_command(std::shared_ptr s, uint16_t command, + uint32_t flag = 0, const void* data = NULL, size_t size = 0); + template void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, const STRUCT& data) { @@ -94,6 +101,7 @@ void send_lobby_message_box(std::shared_ptr c, const char16_t* text); void send_ship_info(std::shared_ptr c, const char16_t* text); void send_text_message(std::shared_ptr c, const char16_t* text); void send_text_message(std::shared_ptr l, const char16_t* text); +void send_text_message(std::shared_ptr l, const char16_t* text); void send_chat_message(std::shared_ptr c, uint32_t from_serial_number, const char16_t* from_name, const char16_t* text); @@ -130,7 +138,8 @@ void send_player_leave_notification(std::shared_ptr l, void send_get_player_info(std::shared_ptr c); void send_arrow_update(std::shared_ptr l); -void send_resume_game(std::shared_ptr l); +void send_resume_game(std::shared_ptr l, + std::shared_ptr ready_client); enum PlayerStatsChange { SubtractHP = 0, diff --git a/Server.cc b/Server.cc index 2b05e4aa..ca64eb60 100644 --- a/Server.cc +++ b/Server.cc @@ -19,7 +19,6 @@ #include #include #include -#include #include "PSOProtocol.hh" #include "ReceiveCommands.hh" @@ -28,22 +27,14 @@ using namespace std; -Server::WorkerThread::WorkerThread(Server* server, int worker_num) : - server(server), worker_num(worker_num), - base(event_base_new(), event_base_free), t() { - this->thread_name = string_printf("Server::run_thread (worker_num=%d)", - worker_num); +void Server::disconnect_client(struct bufferevent* bev) { + this->disconnect_client(this->bev_to_client.at(bev)); } -void Server::WorkerThread::disconnect_client(struct bufferevent* bev) { - { - auto client = this->bev_to_client.at(bev); - this->bev_to_client.erase(bev); - this->server->client_count--; - - rw_guard g(client->lock, true); - client->bev = NULL; - } +void Server::disconnect_client(shared_ptr c) { + this->bev_to_client.erase(c->bev); + struct bufferevent* bev = c->bev; + c->bev = NULL; // if the output buffer is not empty, move the client into the draining pool // instead of disconnecting it, to make sure all the data gets sent @@ -54,71 +45,52 @@ void Server::WorkerThread::disconnect_client(struct bufferevent* bev) { // the callbacks will free it when all the data is sent or the client // disconnects bufferevent_setcb(bev, NULL, - Server::WorkerThread::dispatch_on_disconnecting_client_output, - Server::WorkerThread::dispatch_on_disconnecting_client_error, this); + Server::dispatch_on_disconnecting_client_output, + Server::dispatch_on_disconnecting_client_error, this); bufferevent_disable(bev, EV_READ); } + + process_disconnect(this->state, c); } -void Server::WorkerThread::dispatch_on_listen_accept( - struct evconnlistener *listener, evutil_socket_t fd, - struct sockaddr *address, int socklen, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_listen_accept(*wt, listener, fd, address, socklen); +void Server::dispatch_on_listen_accept( + struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr* address, int socklen, void* ctx) { + reinterpret_cast(ctx)->on_listen_accept(listener, fd, address, + socklen); } -void Server::WorkerThread::dispatch_on_listen_error( - struct evconnlistener *listener, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_listen_error(*wt, listener); +void Server::dispatch_on_listen_error(struct evconnlistener* listener, + void* ctx) { + reinterpret_cast(ctx)->on_listen_error(listener); } -void Server::WorkerThread::dispatch_on_client_input( - struct bufferevent *bev, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_client_input(*wt, bev); +void Server::dispatch_on_client_input(struct bufferevent* bev, void* ctx) { + reinterpret_cast(ctx)->on_client_input(bev); } -void Server::WorkerThread::dispatch_on_client_error( - struct bufferevent *bev, short events, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_client_error(*wt, bev, events); +void Server::dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx) { + reinterpret_cast(ctx)->on_client_error(bev, events); } -void Server::WorkerThread::dispatch_on_disconnecting_client_output( - struct bufferevent *bev, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_disconnecting_client_output(*wt, bev); +void Server::dispatch_on_disconnecting_client_output(struct bufferevent* bev, + void* ctx) { + reinterpret_cast(ctx)->on_disconnecting_client_output(bev); } -void Server::WorkerThread::dispatch_on_disconnecting_client_error( - struct bufferevent *bev, short events, void *ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->on_disconnecting_client_error(*wt, bev, events); +void Server::dispatch_on_disconnecting_client_error(struct bufferevent* bev, + short events, void* ctx) { + reinterpret_cast(ctx)->on_disconnecting_client_error(bev, events); } -void Server::WorkerThread::dispatch_check_for_thread_exit( - evutil_socket_t fd, short what, void* ctx) { - WorkerThread* wt = (WorkerThread*)ctx; - wt->server->check_for_thread_exit(*wt, fd, what); -} - -void Server::on_listen_accept(Server::WorkerThread& wt, - struct evconnlistener *listener, evutil_socket_t fd, - struct sockaddr *address, int socklen) { - - int fd_flags = fcntl(fd, F_GETFD, 0); - if (fd_flags >= 0) { - fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC); - } +void Server::on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr* address, int socklen) { int listen_fd = evconnlistener_get_fd(listener); - GameVersion version; - ServerBehavior initial_state; + ListeningSocket* listening_socket; try { - auto p = this->listen_fd_to_version_and_state.at(listen_fd); - version = p.first; - initial_state = p.second; + listening_socket = &this->listening_sockets.at(listen_fd); } catch (const out_of_range& e) { log(WARNING, "[Server] can\'t determine version for socket %d; disconnecting client", listen_fd); @@ -126,88 +98,76 @@ void Server::on_listen_accept(Server::WorkerThread& wt, return; } - struct bufferevent *bev = bufferevent_socket_new(wt.base.get(), fd, - BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_UNLOCK_CALLBACKS); - shared_ptr c(new Client(bev, version, initial_state)); - auto emplace_ret = wt.bev_to_client.emplace(make_pair(bev, c)); - this->client_count++; + log(INFO, "[Server] client connected via fd %d", listen_fd); - bufferevent_setcb(bev, &WorkerThread::dispatch_on_client_input, NULL, - &WorkerThread::dispatch_on_client_error, &wt); + struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + shared_ptr c(new Client(bev, listening_socket->version, + listening_socket->behavior)); + this->bev_to_client.emplace(make_pair(bev, c)); + + bufferevent_setcb(bev, &Server::dispatch_on_client_input, NULL, + &Server::dispatch_on_client_error, this); bufferevent_enable(bev, EV_READ | EV_WRITE); - this->process_client_connect(emplace_ret.first->second); + process_connect(this->state, c); } -void Server::on_listen_error(Server::WorkerThread& wt, - struct evconnlistener *listener) { +void Server::on_listen_error(struct evconnlistener* listener) { int err = EVUTIL_SOCKET_ERROR(); - log(ERROR, "[Server] failure on listening socket %d: %d (%s)\n", - evconnlistener_get_fd(listener), err, - evutil_socket_error_to_string(err)); - event_base_loopexit(wt.base.get(), NULL); + log(ERROR, "[Server] failure on listening socket %d: %d (%s)", + evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err)); + event_base_loopexit(this->base.get(), NULL); } -void Server::on_client_input(Server::WorkerThread& wt, - struct bufferevent *bev) { +void Server::on_client_input(struct bufferevent* bev) { shared_ptr c; try { - c = wt.bev_to_client.at(bev); + c = this->bev_to_client.at(bev); } catch (const out_of_range& e) { log(WARNING, "[Server] received message from client with no configuration"); // ignore all the data + // TODO: we probably should disconnect them or something struct evbuffer* in_buffer = bufferevent_get_input(bev); evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer)); return; } if (c->should_disconnect) { - wt.disconnect_client(bev); - this->process_client_disconnect(c); + this->disconnect_client(bev); return; } c->last_recv_time = now(); - this->receive_and_process_commands(c, bev); + this->receive_and_process_commands(c); if (c->should_disconnect) { - wt.disconnect_client(bev); - this->process_client_disconnect(c); + this->disconnect_client(bev); return; } } -void Server::on_disconnecting_client_output(Server::WorkerThread& wt, - struct bufferevent *bev) { +void Server::on_disconnecting_client_output(struct bufferevent* bev) { bufferevent_free(bev); } -void Server::on_client_error(Server::WorkerThread& wt, - struct bufferevent *bev, short events) { - shared_ptr c; - try { - c = wt.bev_to_client.at(bev); - } catch (const out_of_range& e) { } - +void Server::on_client_error(struct bufferevent* bev, short events) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "[Server] client caused %d (%s)\n", err, + log(WARNING, "[Server] client caused %d (%s)", err, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - wt.disconnect_client(bev); - if (c) { - this->process_client_disconnect(c); - } + this->disconnect_client(bev); } } -void Server::on_disconnecting_client_error(Server::WorkerThread& wt, - struct bufferevent *bev, short events) { +void Server::on_disconnecting_client_error(struct bufferevent* bev, + short events) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "[Server] disconnecting client caused %d (%s)\n", err, + log(WARNING, "[Server] disconnecting client caused %d (%s)", err, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { @@ -215,15 +175,8 @@ void Server::on_disconnecting_client_error(Server::WorkerThread& wt, } } -void Server::check_for_thread_exit(Server::WorkerThread& wt, - evutil_socket_t fd, short what) { - if (this->should_exit) { - event_base_loopexit(wt.base.get(), NULL); - } -} - -void Server::receive_and_process_commands(shared_ptr c, struct bufferevent* bev) { - struct evbuffer* buf = bufferevent_get_input(bev); +void Server::receive_and_process_commands(shared_ptr c) { + struct evbuffer* buf = bufferevent_get_input(c->bev); size_t header_size = (c->version == GameVersion::BB) ? 8 : 4; // read as much data into recv_buffer as we can and decrypt it @@ -274,92 +227,40 @@ void Server::receive_and_process_commands(shared_ptr c, struct buffereve c->recv_buffer = c->recv_buffer.substr(offset); } -void Server::process_client_connect(std::shared_ptr c) { - process_connect(this->state, c); -} +Server::Server(shared_ptr base, + shared_ptr state) : base(base), state(state) { } -void Server::process_client_disconnect(std::shared_ptr c) { - process_disconnect(this->state, c); -} - -void Server::run_thread(int worker_num) { - WorkerThread& wt = this->threads[worker_num]; - - struct timeval tv = usecs_to_timeval(2000000); - - struct event* ev = event_new(wt.base.get(), -1, EV_PERSIST, - &WorkerThread::dispatch_check_for_thread_exit, &wt); - event_add(ev, &tv); - - event_base_dispatch(wt.base.get()); - - event_del(ev); -} - -Server::Server(shared_ptr state) : - should_exit(false), client_count(0), state(state) { - for (size_t x = 0; x < this->state->num_threads; x++) { - this->threads.emplace_back(this, x); - } -} - -void Server::listen(const string& socket_path, GameVersion version, ServerBehavior initial_state) { +void Server::listen(const string& socket_path, GameVersion version, + ServerBehavior behavior) { int fd = ::listen(socket_path, 0, SOMAXCONN); log(INFO, "[Server] listening on unix socket %s (version %s) on fd %d", socket_path.c_str(), name_for_version(version), fd); - this->add_socket(fd, version, initial_state); + this->add_socket(fd, version, behavior); } -void Server::listen(const string& addr, int port, GameVersion version, ServerBehavior initial_state) { +void Server::listen(const string& addr, int port, GameVersion version, + ServerBehavior behavior) { int fd = ::listen(addr, port, SOMAXCONN); string netloc_str = render_netloc(addr, port); log(INFO, "[Server] listening on tcp interface %s (version %s) on fd %d", netloc_str.c_str(), name_for_version(version), fd); - this->add_socket(fd, version, initial_state); + this->add_socket(fd, version, behavior); } -void Server::listen(int port, GameVersion version, ServerBehavior initial_state) { - this->listen("", port, version, initial_state); +void Server::listen(int port, GameVersion version, ServerBehavior behavior) { + this->listen("", port, version, behavior); } -void Server::add_socket(int fd, GameVersion version, ServerBehavior initial_state) { - this->listen_fd_to_version_and_state.emplace(piecewise_construct, - forward_as_tuple(fd), forward_as_tuple(version, initial_state)); +Server::ListeningSocket::ListeningSocket(Server* s, int fd, + GameVersion version, ServerBehavior behavior) : + fd(fd), version(version), behavior(behavior), listener( + evconnlistener_new(s->base.get(), Server::dispatch_on_listen_accept, s, + LEV_OPT_REUSEABLE, 0, this->fd), evconnlistener_free) { + evconnlistener_set_error_cb(this->listener.get(), + Server::dispatch_on_listen_error); } -void Server::start() { - for (auto& wt : this->threads) { - for (const auto& it : this->listen_fd_to_version_and_state) { - struct evconnlistener* listener = evconnlistener_new(wt.base.get(), - WorkerThread::dispatch_on_listen_accept, &wt, LEV_OPT_REUSEABLE, 0, - it.first); - if (!listener) { - throw runtime_error("can\'t create evconnlistener"); - } - evconnlistener_set_error_cb(listener, WorkerThread::dispatch_on_listen_error); - wt.listeners.emplace(listener, evconnlistener_free); - } - wt.t = thread(&Server::run_thread, this, wt.worker_num); - } -} - -void Server::schedule_stop() { - log(INFO, "[Server] scheduling exit for all threads"); - this->should_exit = true; - - for (const auto& it : listen_fd_to_version_and_state) { - log(INFO, "[Server] closing listening fd %d", it.first); - close(it.first); - } -} - -void Server::wait_for_stop() { - for (auto& wt : this->threads) { - if (!wt.t.joinable()) { - continue; - } - log(INFO, "[Server] waiting for worker %d to terminate", wt.worker_num); - wt.t.join(); - } - log(INFO, "[Server] shutdown complete"); +void Server::add_socket(int fd, GameVersion version, ServerBehavior behavior) { + this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), + forward_as_tuple(this, fd, version, behavior)); } diff --git a/Server.hh b/Server.hh index b3c0a019..57e3e38b 100644 --- a/Server.hh +++ b/Server.hh @@ -6,7 +6,6 @@ #include #include #include -#include #include "Client.hh" #include "ServerState.hh" @@ -14,62 +13,12 @@ class Server { -private: - struct WorkerThread { - Server* server; - int worker_num; - std::unique_ptr base; - std::unordered_set> listeners; - std::unordered_map> bev_to_client; - std::thread t; - std::string thread_name; - - WorkerThread(Server* server, int worker_num); - - void disconnect_client(struct bufferevent* bev); - - static void dispatch_on_listen_accept(struct evconnlistener* listener, - evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); - static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); - static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); - static void dispatch_on_client_error(struct bufferevent* bev, short events, - void* ctx); - static void dispatch_on_disconnecting_client_output(struct bufferevent* bev, - void* ctx); - static void dispatch_on_disconnecting_client_error(struct bufferevent* bev, - short events, void* ctx); - static void dispatch_check_for_thread_exit(evutil_socket_t fd, short what, void* ctx); - }; - - std::atomic should_exit; - std::vector threads; - - std::atomic client_count; - std::unordered_map> listen_fd_to_version_and_state; - std::shared_ptr state; - - void on_listen_accept(WorkerThread& wt, struct evconnlistener *listener, - evutil_socket_t fd, struct sockaddr *address, int socklen); - void on_listen_error(WorkerThread& wt, struct evconnlistener *listener); - void on_client_input(WorkerThread& wt, struct bufferevent *bev); - void on_client_error(WorkerThread& wt, struct bufferevent *bev, short events); - void on_disconnecting_client_output(WorkerThread& wt, struct bufferevent *bev); - void on_disconnecting_client_error(WorkerThread& wt, struct bufferevent *bev, short events); - void check_for_thread_exit(WorkerThread& wt, evutil_socket_t fd, short what); - - void receive_and_process_commands(std::shared_ptr c, struct bufferevent* buf); - - void process_client_connect(std::shared_ptr c); - void process_client_disconnect(std::shared_ptr c); - void process_client_command(std::shared_ptr c, const std::string& command); - - void run_thread(int thread_id); - public: Server() = delete; Server(const Server&) = delete; Server(Server&&) = delete; - Server(std::shared_ptr state); + Server(std::shared_ptr base, + std::shared_ptr state); virtual ~Server() = default; void listen(const std::string& socket_path, GameVersion version, ServerBehavior initial_state); @@ -77,7 +26,44 @@ public: void listen(int port, GameVersion version, ServerBehavior initial_state); void add_socket(int fd, GameVersion version, ServerBehavior initial_state); - virtual void start(); - virtual void schedule_stop(); - virtual void wait_for_stop(); +private: + std::shared_ptr base; + + struct ListeningSocket { + int fd; + GameVersion version; + ServerBehavior behavior; + std::unique_ptr listener; + + ListeningSocket(Server* s, int fd, GameVersion version, + ServerBehavior behavior); + }; + std::unordered_map listening_sockets; + std::unordered_map> bev_to_client; + + std::shared_ptr state; + + static void dispatch_on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); + static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + static void dispatch_on_disconnecting_client_output(struct bufferevent* bev, + void* ctx); + static void dispatch_on_disconnecting_client_error(struct bufferevent* bev, + short events, void* ctx); + + void disconnect_client(struct bufferevent* bev); + void disconnect_client(std::shared_ptr c); + + void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr *address, int socklen); + void on_listen_error(struct evconnlistener* listener); + void on_client_input(struct bufferevent* bev); + void on_client_error(struct bufferevent* bev, short events); + void on_disconnecting_client_output(struct bufferevent* bev); + void on_disconnecting_client_error(struct bufferevent* bev, short events); + + void receive_and_process_commands(std::shared_ptr c); }; diff --git a/ServerShell.cc b/ServerShell.cc new file mode 100644 index 00000000..5e9ea9a1 --- /dev/null +++ b/ServerShell.cc @@ -0,0 +1,148 @@ +#include "ServerShell.hh" + +#include +#include + +#include + +using namespace std; + + + +ServerShell::ServerShell(std::shared_ptr base, + std::shared_ptr state) : Shell(base, state) { } + +void ServerShell::print_prompt() { + fwrite("newserv> ", 9, 1, stdout); + fflush(stdout); +} + +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); + + if (command_name == "exit") { + throw exit_shell(); + + } else if (command_name == "help") { + fprintf(stderr, "\ +commands:\n\ + help\n\ + you\'re reading it now\n\ + exit (or ctrl+d)\n\ + shut down the server\n\ + reload ...\n\ + reload data. can be licenses, battle-params, level-table, or quests.\n\ + add-license \n\ + add a license to the server. is some subset of the following:\n\ + username= (bb username)\n\ + bb-password= (bb password)\n\ + gc-password= (gc password)\n\ + access-key= (gc/pc access key)\n\ + serial= (gc/pc serial number; required for all licenses)\n\ + privileges= (can be normal, mod, admin, root, or numeric)\n\ + delete-license \n\ + delete a license from the server\n\ + list-licenses\n\ + list all licenses registered on the server\n\ +"); + + } else if (command_name == "reload") { + auto types = split(command_args, ' '); + if (types.empty()) { + throw invalid_argument("no data type given"); + } + for (const string& type : types) { + if (type == "licenses") { + shared_ptr lm(new LicenseManager("system/licenses.nsi")); + state->license_manager = lm; + } else if (type == "battle-params") { + shared_ptr bpt(new BattleParamTable("system/blueburst/BattleParamEntry")); + state->battle_params = bpt; + } else if (type == "level-table") { + shared_ptr lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + state->level_table = lt; + } else if (type == "quests") { + shared_ptr qi(new QuestIndex("system/quests")); + state->quest_index = qi; + } else { + throw invalid_argument("incorrect data type"); + } + } + + } else if (command_name == "add-license") { + shared_ptr l(new License()); + memset(l.get(), 0, sizeof(License)); + + for (const string& token : split(command_args, ' ')) { + if (starts_with(token, "username=")) { + if (token.size() >= 29) { + throw invalid_argument("username too long"); + } + strcpy(l->username, token.c_str() + 9); + + } else if (starts_with(token, "bb-password=")) { + if (token.size() >= 32) { + throw invalid_argument("bb-password too long"); + } + strcpy(l->bb_password, token.c_str() + 12); + + } else if (starts_with(token, "gc-password=")) { + if (token.size() > 20) { + throw invalid_argument("gc-password too long"); + } + strcpy(l->gc_password, token.c_str() + 12); + + } else if (starts_with(token, "access-key=")) { + if (token.size() > 23) { + throw invalid_argument("access-key is too long"); + } + strcpy(l->access_key, token.c_str() + 11); + + } else if (starts_with(token, "serial=")) { + l->serial_number = stoul(token.substr(7)); + + } else if (starts_with(token, "privileges=")) { + string mask = token.substr(11); + if (mask == "normal") { + l->privileges = 0; + } else if (mask == "mod") { + l->privileges = Privilege::Moderator; + } else if (mask == "admin") { + l->privileges = Privilege::Administrator; + } else if (mask == "root") { + l->privileges = Privilege::Root; + } else { + l->privileges = stoul(mask); + } + + } else { + throw invalid_argument("incorrect field"); + } + } + + if (!l->serial_number) { + throw invalid_argument("license does not contain serial number"); + } + + state->license_manager->add(l); + fprintf(stderr, "license added\n"); + + } else if (command_name == "delete-license") { + uint32_t serial_number = stoul(command_args); + state->license_manager->remove(serial_number); + fprintf(stderr, "license deleted\n"); + + } else if (command_name == "list-licenses") { + for (const auto& l : state->license_manager->snapshot()) { + string s = l.str(); + fprintf(stderr, "%s\n", s.c_str()); + } + + } else { + throw invalid_argument("unknown command; try \'help\'"); + } +} diff --git a/ServerShell.hh b/ServerShell.hh new file mode 100644 index 00000000..c88b41c3 --- /dev/null +++ b/ServerShell.hh @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +#include "Shell.hh" + + + +class ServerShell : public Shell { +public: + ServerShell(std::shared_ptr base, + std::shared_ptr state); + virtual ~ServerShell() = default; + ServerShell(const ServerShell&) = delete; + ServerShell(ServerShell&&) = delete; + ServerShell& operator=(const ServerShell&) = delete; + ServerShell& operator=(ServerShell&&) = delete; + +protected: + virtual void print_prompt(); + virtual void execute_command(const std::string& command); +}; diff --git a/ServerState.cc b/ServerState.cc index e3836fa7..524fd9b5 100644 --- a/ServerState.cc +++ b/ServerState.cc @@ -33,9 +33,6 @@ ServerState::ServerState() : run_dns_server(true), } void ServerState::add_client_to_available_lobby(shared_ptr c) { - rw_guard g(this->lobbies_lock, false); - - // nonnegative lobby IDs are public, so start at 0 auto it = this->id_to_lobby.lower_bound(0); for (; it != this->id_to_lobby.end(); it++) { if (!(it->second->flags & LobbyFlag::Public)) { @@ -56,8 +53,6 @@ void ServerState::add_client_to_available_lobby(shared_ptr c) { } void ServerState::remove_client_from_lobby(shared_ptr c) { - rw_guard g(this->lobbies_lock, false); - auto l = this->id_to_lobby.at(c->lobby_id); l->remove_client(c); send_player_leave_notification(l, c->lobby_client_id); @@ -86,7 +81,6 @@ void ServerState::change_client_lobby(shared_ptr c, shared_ptr ne void ServerState::send_lobby_join_notifications(shared_ptr l, shared_ptr joining_client) { - rw_guard g2(l->lock, false); for (auto& other_client : l->clients) { if (!other_client) { continue; @@ -99,12 +93,10 @@ void ServerState::send_lobby_join_notifications(shared_ptr l, } shared_ptr ServerState::find_lobby(uint32_t lobby_id) { - rw_guard g(this->lobbies_lock, false); return this->id_to_lobby.at(lobby_id); } vector> ServerState::all_lobbies() { - rw_guard g(this->lobbies_lock, false); vector> ret; for (auto& it : this->id_to_lobby) { ret.emplace_back(it.second); @@ -114,23 +106,14 @@ vector> ServerState::all_lobbies() { void ServerState::add_lobby(shared_ptr l) { l->lobby_id = this->next_lobby_id++; - - rw_guard g(this->lobbies_lock, true); if (this->id_to_lobby.count(l->lobby_id)) { throw logic_error("lobby already exists with the given id"); } - - log(INFO, "creating lobby %" PRId64, l->lobby_id); this->id_to_lobby.emplace(l->lobby_id, l); } void ServerState::remove_lobby(uint32_t lobby_id) { - rw_guard g(this->lobbies_lock, true); - auto it = this->id_to_lobby.find(lobby_id); - if (it == this->id_to_lobby.end()) { - return; - } - this->id_to_lobby.erase(it); + this->id_to_lobby.erase(lobby_id); } shared_ptr ServerState::find_client(const char16_t* identifier, diff --git a/ServerState.hh b/ServerState.hh index ea097c30..11838f40 100644 --- a/ServerState.hh +++ b/ServerState.hh @@ -34,6 +34,7 @@ struct ServerState { std::u16string name; std::unordered_map port_configuration; + std::string username; bool run_dns_server; RunShellBehavior run_shell_behavior; std::shared_ptr quest_index; @@ -46,10 +47,8 @@ struct ServerState { std::vector main_menu; std::shared_ptr> information_menu; std::shared_ptr> information_contents; + std::u16string welcome_message; - size_t num_threads; - - rw_lock lobbies_lock; std::map> id_to_lobby; std::atomic next_lobby_id; diff --git a/Shell.cc b/Shell.cc index a72dc7a2..246925e5 100644 --- a/Shell.cc +++ b/Shell.cc @@ -1,7 +1,6 @@ #include "Shell.hh" -#include -#include +#include #include #include @@ -10,173 +9,84 @@ using namespace std; -class exit_shell : public runtime_error { -public: - exit_shell() : runtime_error("shell exited") { } - ~exit_shell() = default; -}; +Shell::exit_shell::exit_shell() : runtime_error("shell exited") { } -void execute_command(shared_ptr state, 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); +Shell::Shell(std::shared_ptr base, + std::shared_ptr state) : base(base), state(state), + 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(), NULL); - if (command_name == "exit") { - throw exit_shell(); + // 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); - } else if (command_name == "help") { - fprintf(stderr, "\ -commands:\n\ - help\n\ - you\'re reading it now\n\ - exit (or ctrl+d)\n\ - shut down the server\n\ - reload ...\n\ - reload data. can be licenses, battle-params, level-table, or quests.\n\ - add-license \n\ - add a license to the server. is some subset of the following:\n\ - username= (bb username)\n\ - bb-password= (bb password)\n\ - gc-password= (gc password)\n\ - access-key= (gc/pc access key)\n\ - serial= (gc/pc serial number; required for all licenses)\n\ - privileges= (can be normal, mod, admin, root, or numeric)\n\ - delete-license \n\ - delete a license from the server\n\ - list-licenses\n\ - list all licenses registered on the server\n\ -"); - - } else if (command_name == "reload") { - auto types = split(command_args, ' '); - for (const string& type : types) { - if (type == "licenses") { - shared_ptr lm(new LicenseManager("system/licenses.nsi")); - state->license_manager = lm; - } else if (type == "battle-params") { - shared_ptr bpt(new BattleParamTable("system/blueburst/BattleParamEntry")); - state->battle_params = bpt; - } else if (type == "level-table") { - shared_ptr lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); - state->level_table = lt; - } else if (type == "quests") { - shared_ptr qi(new QuestIndex("system/quests")); - state->quest_index = qi; - } else { - throw invalid_argument("incorrect data type"); - } - } - - } else if (command_name == "add-license") { - shared_ptr l(new License()); - memset(l.get(), 0, sizeof(License)); - - for (const string& token : split(command_args, ' ')) { - if (starts_with(token, "username=")) { - if (token.size() >= 29) { - throw invalid_argument("username too long"); - } - strcpy(l->username, token.c_str() + 9); - - } else if (starts_with(token, "bb-password=")) { - if (token.size() >= 32) { - throw invalid_argument("bb-password too long"); - } - strcpy(l->bb_password, token.c_str() + 12); - - } else if (starts_with(token, "gc-password=")) { - if (token.size() > 20) { - throw invalid_argument("gc-password too long"); - } - strcpy(l->gc_password, token.c_str() + 12); - - } else if (starts_with(token, "access-key=")) { - if (token.size() > 23) { - throw invalid_argument("access-key is too long"); - } - strcpy(l->access_key, token.c_str() + 11); - - } else if (starts_with(token, "serial=")) { - l->serial_number = stoul(token.substr(7)); - - } else if (starts_with(token, "privileges=")) { - string mask = token.substr(11); - if (mask == "normal") { - l->privileges = 0; - } else if (mask == "mod") { - l->privileges = Privilege::Moderator; - } else if (mask == "admin") { - l->privileges = Privilege::Administrator; - } else if (mask == "root") { - l->privileges = Privilege::Root; - } else { - l->privileges = stoul(mask); - } - - } else { - throw invalid_argument("incorrect field"); - } - } - - if (!l->serial_number) { - throw invalid_argument("license does not contain serial number"); - } - - state->license_manager->add(l); - fprintf(stderr, "license added\n"); - - } else if (command_name == "delete-license") { - uint32_t serial_number = stoul(command_args); - state->license_manager->remove(serial_number); - fprintf(stderr, "license deleted\n"); - - } else if (command_name == "list-licenses") { - for (const auto& l : state->license_manager->snapshot()) { - string s = l.str(); - fprintf(stderr, "%s\n", s.c_str()); - } - - } else { - throw invalid_argument("unknown command; try \'help\'"); - } + this->poll.add(0, POLLIN); } -void run_shell(shared_ptr state) { - // initialize history - using_history(); - // string history_filename = get_user_home_directory() + "/.newserv_history"; - // read_history(history_filename.c_str()); - // stifle_history(HISTORY_FILE_LENGTH); +void Shell::dispatch_print_prompt(evutil_socket_t fd, short events, void* ctx) { + reinterpret_cast(ctx)->print_prompt(); +} - // read and execute commands - bool should_continue = true; - while (should_continue) { +void Shell::print_prompt() { + // default behavior: no prompt +} - // read the command - char* command = readline("newserv> "); - if (!command) { - fprintf(stderr, " -- exit\n"); +void Shell::dispatch_read_stdin(evutil_socket_t fd, short events, 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; } - // if there's a command, add it to the history - const char* command_to_execute = command + skip_whitespace(command, 0); - if (command_to_execute && *command_to_execute) { - add_history(command); + string command(2048, '\0'); + if (!fgets(const_cast(command.data()), command.size(), stdin)) { + if (!any_command_read) { + // ctrl+d probably; we should exit + fputc('\n', stderr); + event_base_loopexit(this->base.get(), NULL); + return; + } else { + break; // probably not EOF; just no more commands for now + } } - // dispatch the command + // 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(state, command_to_execute); + execute_command(command); } catch (const exit_shell&) { - should_continue = false; + event_base_loopexit(this->base.get(), NULL); + return; } catch (const exception& e) { fprintf(stderr, "FAILED: %s\n", e.what()); } - free(command); } + + this->print_prompt(); } diff --git a/Shell.hh b/Shell.hh index 884c94eb..30150fe6 100644 --- a/Shell.hh +++ b/Shell.hh @@ -1,7 +1,42 @@ #pragma once +#include + +#include #include +#include +#include #include "ServerState.hh" -void run_shell(std::shared_ptr state); + + +class Shell { +public: + Shell(std::shared_ptr base, + std::shared_ptr state); + virtual ~Shell() = default; + Shell(const Shell&) = delete; + Shell(Shell&&) = delete; + Shell& operator=(const Shell&) = delete; + Shell& operator=(Shell&&) = delete; + +protected: + std::shared_ptr base; + std::shared_ptr state; + 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/Text.cc b/Text.cc index 1acfabaa..370bfd34 100644 --- a/Text.cc +++ b/Text.cc @@ -14,15 +14,36 @@ using namespace std; int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) { - return char_traits::compare(s1, s2, count); + size_t x; + for (x = 0; x < count && s1[x] != 0 && s2[x] != 0; x++) { + if (s1[x] < s2[x]) { + return -1; + } else if (s1[x] > s2[x]) { + return 1; + } + } + if (s1[x] < s2[x]) { + return -1; + } else if (s1[x] > s2[x]) { + return 1; + } + return 0; } -char16_t* char16cpy(char16_t* dest, const char16_t* src, size_t count) { - return char_traits::copy(dest, src, count); +void char16cpy(char16_t* dest, const char16_t* src, size_t count) { + size_t x; + for (x = 0; x < count && src[x] != 0; x++) { + dest[x] = src[x]; + } + if (x < count) { + dest[x] = 0; + } } size_t char16len(const char16_t* s) { - return char_traits::length(s); + size_t x; + for (x = 0; s[x] != 0; x++); + return x; } diff --git a/Text.hh b/Text.hh index d949a499..47848a9e 100644 --- a/Text.hh +++ b/Text.hh @@ -7,7 +7,7 @@ int char16cmp(const char16_t* s1, const char16_t* s2, size_t count); -char16_t* char16cpy(char16_t* dest, const char16_t* src, size_t count); +void char16cpy(char16_t* dest, const char16_t* src, size_t count); size_t char16len(const char16_t* s); diff --git a/Version.hh b/Version.hh index 207e1619..43d8f92f 100644 --- a/Version.hh +++ b/Version.hh @@ -26,6 +26,11 @@ enum ClientFlag { // client is loading into a game Loading = 0x0080, + // client is in the information menu (login server only) + InInformationMenu = 0x0100, + // client is at the welcome message (login server only) + AtWelcomeMessage = 0x0200, + DefaultV1 = IsDCv1, DefaultV2DC = 0x0000, DefaultV2PC = 0x0000, diff --git a/system/config.json b/system/config.json index df2bc27b..1c0a7627 100755 --- a/system/config.json +++ b/system/config.json @@ -7,15 +7,18 @@ "LocalAddress": "192.168.0.5", // Address to connect external clients to "ExternalAddress": "10.0.1.6", - // Number of worker threads to run. If zero, use as many threads as there are - // CPU cores. - "Threads": 0, // Set to false to disable the DNS server "RunDNSServer": true, // By default, the interactive shell runs if stdin is a terminal, and doesn't // run if it's not. This option, if present, overrides that behavior. // "RunInteractiveShell": false, + // User to run the server as. If present, newserv will attempt to switch to + // this user's permissions after loading its configuration and opening + // listening sockets. The special value $SUDO_USER causes newserv to look up + // the desired username in the $SUDO_USER variable instead. + "User": "$SUDO_USER", + // Information menu contents. Each entry is a 3-list of // [title, short description, full contents]. In the short description and // full contents, you can use PSO escape codes with the $ character (for @@ -38,6 +41,9 @@ ["Ep3 lobby types", "$C7Display lobby type\nlist for Episode\nIII", "These values can be used with the %sln command.\n$C6*$C7 indicates lobbies where players can't move.\n$C8Pink$C7 indicates Episode 3 only lobbies.\n\nnormal - standard lobby\n$C8planet$C7 - Blank Ragol Lobby\n$C8clouds$C7 - Blank Sky Lobby\n$C8cave$C7 - Unguis Lapis\n$C8jungle$C7 - Episode 2 Jungle\n$C8forest2-1$C7 - Episode 1 Forest 2 (ground)\n$C8forest2-2$C7 - Episode 1 Forest 2 (near Dome)\n$C8windpower$C7\n$C8overview$C7\n$C8seaside$C7 - Episode 2 Seaside\n$C8some?$C7\n$C8dmorgue$C7 - Destroyed Morgue\n$C8caelum$C7 - Caelum\n$C8digital$C7\n$C8boss1$C7\n$C8boss2$C7\n$C8boss3$C7\n$C8knight$C7 - Leukon Knight stage\n$C8sky$C7 - Via Tubus\n$C8morgue$C7 - Morgue"], ["Area list", "$C7Display stage code\nlist", "These values can be used with the $C6%swarp$C7 command.\n\n$C2Green$C7 areas will be empty unless you are in a quest.\n$C6Yellow$C7 areas will not allow you to move.\n\n $C8Episode 1 / Episode 2 / Episode 4$C7\n0: Pioneer 2 / Pioneer 2 / Pioneer 2\n1: Forest 1 / Temple Alpha / Crater East\n2: Forest 2 / Temple Beta / Crater West\n3: Caves 1 / Spaceship Alpha / Crater South\n4: Caves 2 / Spaceship Beta / Crater North\n5: Caves 3 / CCA / Crater Interior\n6: Mines 1 / Jungle North / Desert 1\n7: Mines 2 / Jungle South / Desert 2\n8: Ruins 1 / Mountain / Desert 3\n9: Ruins 2 / Seaside / Saint Million\n10: Ruins 3 / Seabed Upper / $C6Purgatory$C7\n11: Dragon / Seabed Lower\n12: De Rol Le / Gal Gryphon\n13: Vol Opt / Olga Flow\n14: Dark Falz / Barba Ray\n15: $C2Lobby$C7 / Gol Dragon\n16: $C6Battle 1$C7 / $C6Seaside Night$C7\n17: $C6Battle 2$C7 / $C2Tower$C7"], ], + // Welcome message. If not blank, this message will be shown to console users + // upon first connecting. + "WelcomeMessage": "Welcome to $C6Alexandria$C7, a private PSO server\npowered by newserv.", // Item drop rates for non-rare items. For each type (boxes or enemies), all // the categories must add up to a number less than 0x100000000. Each number