diff --git a/README.md b/README.md index 53557cbb..84bccb83 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO). To get started using newserv as a PSO server or as a proxy, see the [Server setup](#server-setup) section. +**To quickly get started using newserv, just read the [server setup](#server-setup) and [How to connect](#how-to-connect) sections.** + This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself, which was originally created by Sega. Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner, because this is a personal project undertaken primarily for the fun of reverse-engineering. If you want to contribute to newserv yourself, pull requests are welcome as well. diff --git a/src/Main.cc b/src/Main.cc index 1aa04cad..66e82c65 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,12 @@ using namespace std; +#ifdef PHOSG_WINDOWS +static constexpr bool IS_WINDOWS = true; +#else +static constexpr bool IS_WINDOWS = false; +#endif + bool use_terminal_colors = false; void print_version_info(); @@ -3167,9 +3174,21 @@ Action a_run_server_replay_log( state->ip_stack_simulator->listen( spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_RAW); if (netloc.second) { - if (state->local_address == state->external_address) { + if (state->local_address == 0 && state->external_address == 0) { config_log.info( - "Note: The Devolution phone number for %s is %" PRIu64, + "Cannot generate Devolution phone numbers for %s because LocalAddress and ExternalAddress are not specified in the configuration", + spec.c_str()); + } else if (state->local_address == 0) { + config_log.info( + "Note: The Devolution phone number for %s is %" PRIu64 " (external)", + spec.c_str(), devolution_phone_number_for_netloc(state->external_address, netloc.second)); + } else if (state->external_address == 0) { + config_log.info( + "Note: The Devolution phone number for %s is %" PRIu64 " (local)", + spec.c_str(), devolution_phone_number_for_netloc(state->local_address, netloc.second)); + } else if (state->local_address == state->external_address) { + config_log.info( + "Note: The Devolution phone number for %s is %" PRIu64 " (local+external)", spec.c_str(), devolution_phone_number_for_netloc(state->local_address, netloc.second)); } else { config_log.info( @@ -3328,30 +3347,30 @@ int main(int argc, char** argv) { phosg::log_error("Unknown or invalid action; try --help"); return 1; } -#ifdef PHOSG_WINDOWS - // Cygwin just gives a stackdump when an exception falls out of main(), so - // unlike Linux and macOS, we have to manually catch exceptions here just to - // see what the exception message was. - try { + if (IS_WINDOWS) { + // Cygwin just gives a stackdump when an exception falls out of main(), so + // unlike Linux and macOS, we have to manually catch exceptions here just to + // see what the exception message was. + try { + a->run(args); + } catch (const phosg::cannot_open_file& e) { + phosg::log_error("Top-level exception (cannot_open_file): %s", e.what()); + throw; + } catch (const invalid_argument& e) { + phosg::log_error("Top-level exception (invalid_argument): %s", e.what()); + throw; + } catch (const out_of_range& e) { + phosg::log_error("Top-level exception (out_of_range): %s", e.what()); + throw; + } catch (const runtime_error& e) { + phosg::log_error("Top-level exception (runtime_error): %s", e.what()); + throw; + } catch (const exception& e) { + phosg::log_error("Top-level exception: %s", e.what()); + throw; + } + } else { a->run(args); - } catch (const phosg::cannot_open_file& e) { - phosg::log_error("Top-level exception (cannot_open_file): %s", e.what()); - throw; - } catch (const invalid_argument& e) { - phosg::log_error("Top-level exception (invalid_argument): %s", e.what()); - throw; - } catch (const out_of_range& e) { - phosg::log_error("Top-level exception (out_of_range): %s", e.what()); - throw; - } catch (const runtime_error& e) { - phosg::log_error("Top-level exception (runtime_error): %s", e.what()); - throw; - } catch (const exception& e) { - phosg::log_error("Top-level exception: %s", e.what()); - throw; } -#else - a->run(args); -#endif return 0; } diff --git a/src/NetworkAddresses.cc b/src/NetworkAddresses.cc index 9920e00e..bf584a25 100644 --- a/src/NetworkAddresses.cc +++ b/src/NetworkAddresses.cc @@ -66,9 +66,16 @@ map get_local_addresses() { return ret; } +bool is_loopback_address(uint32_t addr) { + return ((addr & 0xFF000000) == 0x7F000000); // 127.0.0.0/8 +} + bool is_local_address(uint32_t addr) { - uint8_t net = (addr >> 24) & 0xFF; - return ((net == 127) || (net == 172) || (net == 10) || (net == 192)); + return is_loopback_address(addr) || // 127.0.0.0/8 + ((addr & 0xFF000000) == 0x0A000000) || // 10.0.0.0/8 + ((addr & 0xFFF00000) == 0xAC100000) || // 172.16.0.0/12 + ((addr & 0xFFFF0000) == 0xC0A80000) || // 192.168.0.0/16 + ((addr & 0xFFFF0000) == 0xA9FE0000); // 169.254.0.0/16 } bool is_local_address(const sockaddr_storage& daddr) { diff --git a/src/NetworkAddresses.hh b/src/NetworkAddresses.hh index 36f93cf3..635d61b9 100644 --- a/src/NetworkAddresses.hh +++ b/src/NetworkAddresses.hh @@ -12,6 +12,7 @@ uint32_t resolve_address(const char* address); std::map get_local_addresses(); uint32_t get_connected_address(int fd); +bool is_loopback_address(uint32_t addr); bool is_local_address(uint32_t daddr); bool is_local_address(const sockaddr_storage& daddr); diff --git a/src/ServerState.cc b/src/ServerState.cc index daa615a8..6fea306a 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include "Compression.hh" #include "EventUtils.hh" @@ -19,6 +20,12 @@ using namespace std; +#ifdef PHOSG_WINDOWS +static constexpr bool IS_WINDOWS = true; +#else +static constexpr bool IS_WINDOWS = false; +#endif + CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() { unordered_set enabled_keys; for (const auto& it : json.as_list()) { @@ -673,8 +680,10 @@ void ServerState::load_config_early() { for (const auto& item : this->config_json->at("IPStackListen").as_list()) { if (item->is_int()) { this->ip_stack_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int())); - } else { + } else if (!IS_WINDOWS) { this->ip_stack_addresses.emplace_back(item->as_string()); + } else { + config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str()); } } } catch (const out_of_range&) { @@ -683,8 +692,10 @@ void ServerState::load_config_early() { for (const auto& item : this->config_json->at("PPPStackListen").as_list()) { if (item->is_int()) { this->ppp_stack_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int())); - } else { + } else if (!IS_WINDOWS) { this->ppp_stack_addresses.emplace_back(item->as_string()); + } else { + config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str()); } } } catch (const out_of_range&) { @@ -693,8 +704,10 @@ void ServerState::load_config_early() { for (const auto& item : this->config_json->at("PPPRawListen").as_list()) { if (item->is_int()) { this->ppp_raw_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int())); - } else { + } else if (!IS_WINDOWS) { this->ppp_raw_addresses.emplace_back(item->as_string()); + } else { + config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str()); } } } catch (const out_of_range&) { @@ -703,8 +716,10 @@ void ServerState::load_config_early() { for (const auto& item : this->config_json->at("HTTPListen").as_list()) { if (item->is_int()) { this->http_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int())); - } else { + } else if (!IS_WINDOWS) { this->http_addresses.emplace_back(item->as_string()); + } else { + config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str()); } } } catch (const out_of_range&) { @@ -727,7 +742,18 @@ void ServerState::load_config_early() { this->all_addresses.erase(""); this->all_addresses.emplace("", this->local_address); } catch (const out_of_range&) { - config_log.warning("Local address not specified; interface defaults will be used"); + for (const auto& it : this->all_addresses) { + // Choose any local interface except the loopback interface + if (!is_loopback_address(it.second) && is_local_address(it.second)) { + this->local_address = it.second; + } + } + if (this->local_address) { + string addr_str = string_for_address(this->local_address); + config_log.warning("Local address not specified; using %s as default", addr_str.c_str()); + } else { + config_log.warning("Local address not specified and no default is available"); + } } try { @@ -744,7 +770,19 @@ void ServerState::load_config_early() { this->all_addresses.erase(""); this->all_addresses.emplace("", this->external_address); } catch (const out_of_range&) { - config_log.warning("External address not specified; only local clients will be able to connect"); + for (const auto& it : this->all_addresses) { + // Choose any non-local address, if any exist + if (!is_local_address(it.second)) { + this->external_address = it.second; + break; + } + } + if (this->external_address) { + string addr_str = string_for_address(this->external_address); + config_log.warning("External address not specified; using %s as default", addr_str.c_str()); + } else { + config_log.warning("External address not specified and no default is available; only local clients will be able to connect"); + } } try { diff --git a/system/config.example.json b/system/config.example.json index 399e7b0b..08f2c2a5 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -159,14 +159,15 @@ // set this to ["127.0.0.1:5059"] (which listens on port 5059 but only accepts // connections from the local machine), and configure Dolphin's tapserver BBA // to connect to 127.0.0.1:5059. - "IPStackListen": [], - "PPPStackListen": [], + "IPStackListen": ["/tmp/dolphin-tap", 5059], + "PPPStackListen": ["/tmp/dolphin-modem-tap", 5058], // Where to listen for PPP clients. This exists to interface with PSO GC // clients running on a Wii with Devolution, which emulates the modem adapter. // The PPP stream can be forwarded directly to newserv on any port specified - // here without using pppd or another PPP server. - "PPPRawListen": [], + // here without using pppd or another PPP server. When you start newserv, it + // will tell you the Devolution phone number you can use to connect. + "PPPRawListen": [5057], // Where to listen for HTTP connections. The HTTP server is intended as a // private interface to interact with newserv from e.g. an in-house Web portal @@ -193,10 +194,26 @@ // Note that PSO GameCube Episodes 1&2 Trial Edition uses the DC's // ProxyDestinations dictionary here. This is because other servers that // support that version treat it as PSO DC v2. - "ProxyDestinations-DC": {}, - "ProxyDestinations-PC": {}, - "ProxyDestinations-GC": {}, - "ProxyDestinations-XB": {}, + "ProxyDestinations-DC": { + "Schtserv": "psobb.dyndns.org:9200", + "Sylverant": "sylverant.net:9200", + "EU/Ragol": "ragol.org:9200", + }, + "ProxyDestinations-PC": { + "Schtserv": "psobb.dyndns.org:9100", + "Sylverant": "sylverant.net:9100", + "EU/Ragol": "ragol.org:9100", + }, + "ProxyDestinations-GC": { + "Schtserv": "psobb.dyndns.org:9103", + "Sylverant": "sylverant.net:9103", + "EU/Ragol": "ragol.org:9103", + }, + "ProxyDestinations-XB": { + "Schtserv": "psobb.dyndns.org:9500", + "Sylverant": "sylverant.net:9500", + "EU/Ragol": "ragol.org:9500", + }, // Proxy destination for patch server clients. If this is given, the internal // patch server (for PC and BB) is bypassed, and any client that connects to // the patch server is instead proxied to this destination. @@ -414,9 +431,7 @@ "none", // Lobby C4 (Episode 3 only) "none", // Lobby C5 (Episode 3 only) ], - - // Menu event. This is the holiday event during the lobby overview while at - // the main menu. + // Menu event. This is the holiday event the player sees at the main menu. "MenuEvent": "none", // Episode 3 menu song. If set, Episode 3 clients will hear this song when @@ -494,7 +509,8 @@ // any Meseta when a song is played. The check for 100 or more meseta happens // client-side, however, so even if this option is enabled, the client must // either have 100 or more Meseta or use the "Jukebox is free" Action Replay - // code to be able to play songs. + // code to be able to play songs. (In the Git repository, the code is in + // notes/ar-codes.txt.) "Episode3JukeboxIsFree": false, // Episode 3 battle behavior flags. When set to zero, battles behave as they @@ -514,7 +530,7 @@ // 0x0100 => Disable interference (COMs randomly coming to each other's // rescue) // 0x0200 => Allow interference even when neither player is a COM - "Episode3BehaviorFlags": 0x0002, + "Episode3BehaviorFlags": 0x0042, // Trap assist cards for each trap type in Episode 3 battles. These are the // default values used offline, but you can change the trap types online here.