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=<IP_OR_HOSTNAME> 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
This commit is contained in:
Martin Michelsen
2020-02-16 15:03:47 -08:00
parent 76c810c1e6
commit 0d4b0b2279
39 changed files with 1487 additions and 975 deletions
+12 -28
View File
@@ -262,7 +262,7 @@ static void command_ax(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
static void command_announce(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const char16_t* args) {
check_privileges(c, Privilege::Announce);
// TODO: implement this
send_text_message(s, args);
}
static void command_arrow(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
@@ -289,7 +289,6 @@ static void command_cheat(shared_ptr<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby>
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<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby> 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<ServerState> s, shared_ptr<Lobby> 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.");
}
+3 -5
View File
@@ -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;
}
+1 -4
View File
@@ -34,9 +34,7 @@ struct ClientConfigBB {
};
struct Client {
rw_lock lock;
// license & account
// license & account
std::shared_ptr<const License> 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?
+49 -77
View File
@@ -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<struct event_base> 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<struct event, void(*)(struct event*)> 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<DNSServer*>(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<pollfd> 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<char*>(input.data()), input.size(),
0, reinterpret_cast<sockaddr*>(&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<char*>(input.data()),
input.size(), 0, reinterpret_cast<sockaddr*>(&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<const char*>(&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<sockaddr*>(&remote), remote_size);
}
sendto(fd, response.data(), response.size(), 0,
reinterpret_cast<const sockaddr*>(&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<const char*>(&connect_address_be), 4);
return ret;
}
+11 -15
View File
@@ -1,14 +1,17 @@
#pragma once
#include <atomic>
#include <thread>
#include <event2/event.h>
#include <memory>
#include <unordered_map>
#include <string>
#include <set>
class DNSServer {
public:
DNSServer(uint32_t local_connect_address, uint32_t external_connect_address);
DNSServer(std::shared_ptr<struct event_base> 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<bool> should_exit;
std::thread t;
std::set<int> fds;
std::shared_ptr<struct event_base> base;
std::unordered_map<int, std::unique_ptr<struct event, void(*)(struct event*)>> 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);
};
+5 -11
View File
@@ -15,24 +15,18 @@ shared_ptr<const string> FileContentsCache::get(const std::string& name) {
uint64_t t = now();
try {
lock_guard<mutex> 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<const string> contents(new string(load_file(name)));
{
lock_guard<mutex> 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<const string> 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<const string> FileContentsCache::get(const char* name) {
-2
View File
@@ -1,7 +1,6 @@
#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
@@ -38,5 +37,4 @@ public:
private:
std::unordered_map<std::string, File> name_to_file;
mutable std::mutex lock;
};
+1 -1
View File
@@ -141,7 +141,7 @@ using namespace std;
////////////////////////////////////////////////////////////////////////////////
void player_use_item_locked(shared_ptr<Lobby> l, shared_ptr<Client> c,
void player_use_item(shared_ptr<Lobby> l, shared_ptr<Client> c,
size_t item_index) {
ssize_t equipped_weapon = -1;
+1 -1
View File
@@ -7,7 +7,7 @@
#include "Lobby.hh"
#include "Client.hh"
void player_use_item_locked(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
void player_use_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
size_t item_index);
struct CommonItemCreator {
+12 -30
View File
@@ -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<const License> 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<const License> LicenseManager::verify_pc(uint32_t serial_number,
shared_ptr<const License> 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<const License> LicenseManager::verify_gc(uint32_t serial_number,
shared_ptr<const License> 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<const License> 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<License> 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<License> LicenseManager::snapshot() const {
vector<License> ret;
rw_guard g(this->lock, false);
for (auto it : this->serial_number_to_license) {
ret.emplace_back(*it.second);
}
+1 -3
View File
@@ -58,9 +58,7 @@ public:
std::vector<License> snapshot() const;
protected:
void save_locked() const;
mutable rw_lock lock;
void save() const;
std::string filename;
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
+16 -44
View File
@@ -2,6 +2,8 @@
#include <string.h>
#include <phosg/Random.hh>
#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<uint32_t>()), 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<Client> c) {
rw_guard g(this->lock, true);
this->add_client_locked(c);
}
void Lobby::add_client_locked(shared_ptr<Client> 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<Client> c) {
rw_guard g(this->lock, true);
this->remove_client_locked(c);
}
void Lobby::remove_client_locked(shared_ptr<Client> 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<Client> 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<Lobby> dest_lobby,
@@ -131,31 +118,18 @@ void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
return;
}
// deadlock prevention: lock the lobbies in increasing order of memory address
vector<rw_guard> guards;
uint8_t* this_ptr = reinterpret_cast<uint8_t*>(this);
uint8_t* dest_ptr = reinterpret_cast<uint8_t*>(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<Client> 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");
+4 -8
View File
@@ -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<Client> c);
void add_client_locked(std::shared_ptr<Client> c);
void remove_client(std::shared_ptr<Client> c);
void remove_client_locked(std::shared_ptr<Client> c);
void move_client_to_lobby(std::shared_ptr<Lobby> dest_lobby,
std::shared_ptr<Client> c);
+106 -43
View File
@@ -1,22 +1,24 @@
#include <signal.h>
#include <pwd.h>
#include <event2/event.h>
#include <event2/thread.h>
#include <unordered_map>
#include <phosg/JSON.hh>
#include <phosg/Network.hh>
#include <phosg/Strings.hh>
#include <phosg/Filesystem.hh>
#include <set>
#include <thread>
#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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> state(new ServerState());
log(INFO, "starting network subsystem");
shared_ptr<struct event_base> 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<DNSServer> 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<Server> 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<ProxyServer> proxy_server;
shared_ptr<Server> 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> 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;
}
+6 -7
View File
@@ -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
+1 -1
View File
@@ -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")
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
+35 -9
View File
@@ -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));
}
+344
View File
@@ -0,0 +1,344 @@
#include "ProxyServer.hh"
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <phosg/Encoding.hh>
#include <phosg/Network.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#include "PSOProtocol.hh"
#include "ReceiveCommands.hh"
using namespace std;
ProxyServer::ProxyServer(shared_ptr<struct event_base> 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<char*>(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<ProxyServer*>(ctx)->on_listen_accept(listener, fd, address,
socklen);
}
void ProxyServer::dispatch_on_listen_error(struct evconnlistener* listener,
void* ctx) {
reinterpret_cast<ProxyServer*>(ctx)->on_listen_error(listener);
}
void ProxyServer::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
reinterpret_cast<ProxyServer*>(ctx)->on_client_input(bev);
}
void ProxyServer::dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx) {
reinterpret_cast<ProxyServer*>(ctx)->on_client_error(bev, events);
}
void ProxyServer::dispatch_on_server_input(struct bufferevent* bev, void* ctx) {
reinterpret_cast<ProxyServer*>(ctx)->on_server_input(bev);
}
void ProxyServer::dispatch_on_server_error(struct bufferevent* bev, short events,
void* ctx) {
reinterpret_cast<ProxyServer*>(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<const sockaddr_in*>(&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<const sockaddr*>(&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<char*>(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<char*>(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<char*>(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<const InitEncryptionCommand*>(
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<ReconnectCommand*>(
const_cast<char*>(command.data()));
memset(&this->next_destination, 0, sizeof(this->next_destination));
struct sockaddr_in* sin = reinterpret_cast<struct sockaddr_in*>(
&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<struct sockaddr*>(&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<struct sockaddr_in*>(
&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<char*>(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));
}
}
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <event2/event.h>
#include <unordered_set>
#include <vector>
#include <string>
#include <memory>
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
class ProxyServer {
public:
ProxyServer() = delete;
ProxyServer(const ProxyServer&) = delete;
ProxyServer(ProxyServer&&) = delete;
ProxyServer(std::shared_ptr<struct event_base> 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<struct event_base> base;
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
struct sockaddr_storage next_destination;
int listen_port;
PSOCommandHeaderDCGC client_input_header;
PSOCommandHeaderDCGC server_input_header;
std::shared_ptr<PSOEncryption> client_input_crypt;
std::shared_ptr<PSOEncryption> client_output_crypt;
std::shared_ptr<PSOEncryption> server_input_crypt;
std::shared_ptr<PSOEncryption> 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);
};
+81
View File
@@ -0,0 +1,81 @@
#include "ProxyShell.hh"
#include <event2/event.h>
#include <stdio.h>
#include <phosg/Strings.hh>
using namespace std;
ProxyShell::ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> 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 <data>\n\
send a command to the client\n\
ss <data>\n\
send a command to the server\n\
chat <text>\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<uint16_t*>(const_cast<char*>(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<uint16_t*>(const_cast<char*>(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\'");
}
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <memory>
#include <string>
#include <event2/event.h>
#include "Shell.hh"
#include "ProxyServer.hh"
class ProxyShell : public Shell {
public:
ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> 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<ProxyServer> proxy_server;
virtual void execute_command(const std::string& command);
};
+2 -2
View File
@@ -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.
+91 -65
View File
@@ -51,26 +51,39 @@ void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> 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<int64_t>(c->server_behavior));
}
}
void process_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> c,
@@ -247,7 +260,8 @@ void process_login_d_e_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> 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<const Cmd*>(data);
c->flags |= flags_for_version(c->version, cmd->sub_version);
@@ -481,11 +495,11 @@ void process_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Clien
void process_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
// add a reference to ensure it's not destroyed by another thread
// we use item_id + 1 here because "go back" is the first item
auto info_menu = s->information_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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> c,
void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> 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<const PSOPlayerDataPC*>(data));
break;
case GameVersion::GC:
check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC);
c->player.import(*reinterpret_cast<const PSOPlayerDataGC*>(data));
break;
case GameVersion::BB:
check_size(size, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + 2 * 0xAC);
c->player.import(*reinterpret_cast<const PSOPlayerDataBB*>(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<const PSOPlayerDataPC*>(data));
break;
case GameVersion::GC:
check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC);
c->player.import(*reinterpret_cast<const PSOPlayerDataGC*>(data));
break;
case GameVersion::BB:
check_size(size, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + 2 * 0xAC);
c->player.import(*reinterpret_cast<const PSOPlayerDataBB*>(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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<int>(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<int>(c->version), size, command, flag);
string data_to_print;
if (c->version == GameVersion::BB) {
data_to_print.resize(size + 8);
PSOCommandHeaderBB* header = reinterpret_cast<PSOCommandHeaderBB*>(
const_cast<char*>(data_to_print.data()));
header->command = command;
header->flag = flag;
header->size = size + 8;
memcpy(const_cast<char*>(data_to_print.data() + 8), data, size);
} else if (c->version == GameVersion::PC) {
data_to_print.resize(size + 4);
PSOCommandHeaderPC* header = reinterpret_cast<PSOCommandHeaderPC*>(
const_cast<char*>(data_to_print.data()));
header->command = command;
header->flag = flag;
header->size = size + 4;
memcpy(const_cast<char*>(data_to_print.data() + 4), data, size);
} else { // DC/GC
data_to_print.resize(size + 4);
PSOCommandHeaderDCGC* header = reinterpret_cast<PSOCommandHeaderDCGC*>(
const_cast<char*>(data_to_print.data()));
header->command = command;
header->flag = flag;
header->size = size + 4;
memcpy(const_cast<char*>(data_to_print.data() + 4), data, size);
}
print_data(stderr, data_to_print);
}
auto fn = handlers[static_cast<size_t>(c->version)][command & 0xFF];
if (fn) {
+16 -27
View File
@@ -60,16 +60,20 @@ void forward_subcommand(shared_ptr<Lobby> l, shared_ptr<Client> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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<ServerState> 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
+148 -172
View File
@@ -75,17 +75,28 @@ void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag,
c->send(move(send_data));
}
void send_command(shared_ptr<Lobby> 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<Lobby> l, shared_ptr<Client> 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<Lobby> 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<ServerState> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> c, uint32_t error) {
uint32_t caps; // should be 0x00000102
} cmd = {
error,
0x00000100,
0x00010000,
c->license->serial_number,
static_cast<uint32_t>(random_object<uint32_t>()),
c->config,
@@ -511,7 +519,6 @@ void send_text_message(shared_ptr<Client> c, const char16_t* text) {
}
void send_text_message(shared_ptr<Lobby> 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<Lobby> l, const char16_t* text) {
}
}
void send_text_message(shared_ptr<ServerState> s, const char16_t* text) {
for (auto& l : s->all_lobbies()) {
send_text_message(l, text);
}
}
void send_chat_message(shared_ptr<Client> 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<Client> c, shared_ptr<Lobby> l) {
};
vector<Entry> 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<Client> c, shared_ptr<Lobby> l) {
};
vector<Entry> 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<ServerState> 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<ServerState> 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<Client> c, shared_ptr<Client> 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<Client> c, shared_ptr<Client> 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<Client> c, shared_ptr<ServerState> 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<Client> c, shared_ptr<ServerState> 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<Client> c, shared_ptr<ServerState> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> l) {
}
static void send_join_lobby_gc(shared_ptr<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> l) {
}
static void send_join_lobby_bb(shared_ptr<Client> c, shared_ptr<Lobby> 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<Client> c, shared_ptr<Lobby> 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<Client> 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<Client> 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<Client> 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<Lobby> l) {
};
vector<Entry> 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<Lobby> 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<Lobby> l) {
}
// tells the player that the joining player is done joining, and the game can resume
void send_resume_game(shared_ptr<Lobby> l) {
void send_resume_game(shared_ptr<Lobby> l, shared_ptr<Client> ready_client) {
uint32_t data = 0x081C0372;
send_command(l, 0x60, 0x00, &data, 4);
send_command_excluding_client(l, ready_client, 0x60, 0x00, &data, 4);
}
+11 -2
View File
@@ -30,12 +30,19 @@
void send_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag = 0,
void send_command(std::shared_ptr<Client> c, uint16_t command,
uint32_t flag = 0, const void* data = NULL, size_t size = 0);
void send_command_excluding_client(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, uint16_t command, uint32_t flag = 0,
const void* data = NULL, size_t size = 0);
void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag = 0,
const void* data = NULL, size_t size = 0);
void send_command(std::shared_ptr<ServerState> s, uint16_t command,
uint32_t flag = 0, const void* data = NULL, size_t size = 0);
template <typename TARGET, typename STRUCT>
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
const STRUCT& data) {
@@ -94,6 +101,7 @@ void send_lobby_message_box(std::shared_ptr<Client> c, const char16_t* text);
void send_ship_info(std::shared_ptr<Client> c, const char16_t* text);
void send_text_message(std::shared_ptr<Client> c, const char16_t* text);
void send_text_message(std::shared_ptr<Lobby> l, const char16_t* text);
void send_text_message(std::shared_ptr<ServerState> l, const char16_t* text);
void send_chat_message(std::shared_ptr<Client> 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<Lobby> l,
void send_get_player_info(std::shared_ptr<Client> c);
void send_arrow_update(std::shared_ptr<Lobby> l);
void send_resume_game(std::shared_ptr<Lobby> l);
void send_resume_game(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> ready_client);
enum PlayerStatsChange {
SubtractHP = 0,
+82 -181
View File
@@ -19,7 +19,6 @@
#include <phosg/Network.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#include <thread>
#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<Client> 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<Server*>(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<Server*>(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<Server*>(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<Server*>(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<Server*>(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<Server*>(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<Client> 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<Client> 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<Client> 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<Client> 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<Client> c, struct bufferevent* bev) {
struct evbuffer* buf = bufferevent_get_input(bev);
void Server::receive_and_process_commands(shared_ptr<Client> 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<Client> c, struct buffereve
c->recv_buffer = c->recv_buffer.substr(offset);
}
void Server::process_client_connect(std::shared_ptr<Client> c) {
process_connect(this->state, c);
}
Server::Server(shared_ptr<struct event_base> base,
shared_ptr<ServerState> state) : base(base), state(state) { }
void Server::process_client_disconnect(std::shared_ptr<Client> 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<ServerState> 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));
}
+42 -56
View File
@@ -6,7 +6,6 @@
#include <vector>
#include <string>
#include <memory>
#include <thread>
#include "Client.hh"
#include "ServerState.hh"
@@ -14,62 +13,12 @@
class Server {
private:
struct WorkerThread {
Server* server;
int worker_num;
std::unique_ptr<struct event_base, void(*)(struct event_base*)> base;
std::unordered_set<std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>> listeners;
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> 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<bool> should_exit;
std::vector<WorkerThread> threads;
std::atomic<size_t> client_count;
std::unordered_map<int, std::pair<GameVersion, ServerBehavior>> listen_fd_to_version_and_state;
std::shared_ptr<ServerState> 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<Client> c, struct bufferevent* buf);
void process_client_connect(std::shared_ptr<Client> c);
void process_client_disconnect(std::shared_ptr<Client> c);
void process_client_command(std::shared_ptr<Client> 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<ServerState> state);
Server(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> 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<struct event_base> base;
struct ListeningSocket {
int fd;
GameVersion version;
ServerBehavior behavior;
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
ListeningSocket(Server* s, int fd, GameVersion version,
ServerBehavior behavior);
};
std::unordered_map<int, ListeningSocket> listening_sockets;
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> bev_to_client;
std::shared_ptr<ServerState> 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<Client> 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<Client> c);
};
+148
View File
@@ -0,0 +1,148 @@
#include "ServerShell.hh"
#include <event2/event.h>
#include <stdio.h>
#include <phosg/Strings.hh>
using namespace std;
ServerShell::ServerShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> 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 <item> ...\n\
reload data. <item> can be licenses, battle-params, level-table, or quests.\n\
add-license <parameters>\n\
add a license to the server. <parameters> is some subset of the following:\n\
username=<username> (bb username)\n\
bb-password=<password> (bb password)\n\
gc-password=<password> (gc password)\n\
access-key=<access-key> (gc/pc access key)\n\
serial=<serial-number> (gc/pc serial number; required for all licenses)\n\
privileges=<privilege-mask> (can be normal, mod, admin, root, or numeric)\n\
delete-license <serial-number>\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<LicenseManager> lm(new LicenseManager("system/licenses.nsi"));
state->license_manager = lm;
} else if (type == "battle-params") {
shared_ptr<BattleParamTable> bpt(new BattleParamTable("system/blueburst/BattleParamEntry"));
state->battle_params = bpt;
} else if (type == "level-table") {
shared_ptr<LevelTable> lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
state->level_table = lt;
} else if (type == "quests") {
shared_ptr<QuestIndex> qi(new QuestIndex("system/quests"));
state->quest_index = qi;
} else {
throw invalid_argument("incorrect data type");
}
}
} else if (command_name == "add-license") {
shared_ptr<License> 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\'");
}
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <memory>
#include <string>
#include <event2/event.h>
#include "Shell.hh"
class ServerShell : public Shell {
public:
ServerShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> 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);
};
+1 -18
View File
@@ -33,9 +33,6 @@ ServerState::ServerState() : run_dns_server(true),
}
void ServerState::add_client_to_available_lobby(shared_ptr<Client> 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<Client> c) {
}
void ServerState::remove_client_from_lobby(shared_ptr<Client> 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<Client> c, shared_ptr<Lobby> ne
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l,
shared_ptr<Client> 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<Lobby> l,
}
shared_ptr<Lobby> ServerState::find_lobby(uint32_t lobby_id) {
rw_guard g(this->lobbies_lock, false);
return this->id_to_lobby.at(lobby_id);
}
vector<shared_ptr<Lobby>> ServerState::all_lobbies() {
rw_guard g(this->lobbies_lock, false);
vector<shared_ptr<Lobby>> ret;
for (auto& it : this->id_to_lobby) {
ret.emplace_back(it.second);
@@ -114,23 +106,14 @@ vector<shared_ptr<Lobby>> ServerState::all_lobbies() {
void ServerState::add_lobby(shared_ptr<Lobby> 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<Client> ServerState::find_client(const char16_t* identifier,
+2 -3
View File
@@ -34,6 +34,7 @@ struct ServerState {
std::u16string name;
std::unordered_map<std::string, PortConfiguration> port_configuration;
std::string username;
bool run_dns_server;
RunShellBehavior run_shell_behavior;
std::shared_ptr<const QuestIndex> quest_index;
@@ -46,10 +47,8 @@ struct ServerState {
std::vector<MenuItem> main_menu;
std::shared_ptr<std::vector<MenuItem>> information_menu;
std::shared_ptr<std::vector<std::u16string>> information_contents;
std::u16string welcome_message;
size_t num_threads;
rw_lock lobbies_lock;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::atomic<int32_t> next_lobby_id;
+61 -151
View File
@@ -1,7 +1,6 @@
#include "Shell.hh"
#include <readline/history.h>
#include <readline/readline.h>
#include <event2/event.h>
#include <stdio.h>
#include <phosg/Strings.hh>
@@ -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<ServerState> 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<struct event_base> base,
std::shared_ptr<ServerState> 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 <item> ...\n\
reload data. <item> can be licenses, battle-params, level-table, or quests.\n\
add-license <parameters>\n\
add a license to the server. <parameters> is some subset of the following:\n\
username=<username> (bb username)\n\
bb-password=<password> (bb password)\n\
gc-password=<password> (gc password)\n\
access-key=<access-key> (gc/pc access key)\n\
serial=<serial-number> (gc/pc serial number; required for all licenses)\n\
privileges=<privilege-mask> (can be normal, mod, admin, root, or numeric)\n\
delete-license <serial-number>\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<LicenseManager> lm(new LicenseManager("system/licenses.nsi"));
state->license_manager = lm;
} else if (type == "battle-params") {
shared_ptr<BattleParamTable> bpt(new BattleParamTable("system/blueburst/BattleParamEntry"));
state->battle_params = bpt;
} else if (type == "level-table") {
shared_ptr<LevelTable> lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
state->level_table = lt;
} else if (type == "quests") {
shared_ptr<QuestIndex> qi(new QuestIndex("system/quests"));
state->quest_index = qi;
} else {
throw invalid_argument("incorrect data type");
}
}
} else if (command_name == "add-license") {
shared_ptr<License> 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<ServerState> 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<Shell*>(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<Shell*>(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<char*>(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();
}
+36 -1
View File
@@ -1,7 +1,42 @@
#pragma once
#include <event2/event.h>
#include <stdexcept>
#include <memory>
#include <string>
#include <phosg/Filesystem.hh>
#include "ServerState.hh"
void run_shell(std::shared_ptr<ServerState> state);
class Shell {
public:
Shell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
virtual ~Shell() = default;
Shell(const Shell&) = delete;
Shell(Shell&&) = delete;
Shell& operator=(const Shell&) = delete;
Shell& operator=(Shell&&) = delete;
protected:
std::shared_ptr<struct event_base> base;
std::shared_ptr<ServerState> state;
std::unique_ptr<struct event, void (*)(struct event*)> read_event;
std::unique_ptr<struct event, void (*)(struct event*)> 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;
};
+25 -4
View File
@@ -14,15 +14,36 @@ using namespace std;
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) {
return char_traits<char16_t>::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<char16_t>::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<char16_t>::length(s);
size_t x;
for (x = 0; s[x] != 0; x++);
return x;
}
+1 -1
View File
@@ -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);
+5
View File
@@ -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,
+9 -3
View File
@@ -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