// ReSkate dedicated server: a headless session host that players find in the // in-game server browser. Runs from its own folder, next to steam_api64.dll and // the Steam client files (steamclient64.dll, tier0_s64.dll, vstdlib_s64.dll). // On Linux the Steam files are libsteam_api.so and steamclient.so. #include "Engine/Game/Build/supported_build.h" #include "global_bans.h" #include "server_map_update.h" #include "server_config.h" #include "server_host.h" #include "server_update.h" #include "Extension/Multiplayer/developer_identity.h" #include "steam_server.h" #include "Extension/Multiplayer/Session/monotonic_clock.h" #include "Engine/Core/Platform/path_text.h" #include "Engine/Core/Text/word_filter.h" #include "Engine/Game/World/world_layer_catalog.h" #include "Engine/Game/World/world_names.h" #include "Engine/Vfs/world_layer_scan.h" #ifdef _WIN32 #include #include #include #include #else #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace dingosdk; using namespace dingosdk::server; namespace { std::atomic stopping{}; // run() returns this when a newer server release should be installed. constexpr int restart_for_update = -2; std::string update_version; std::mutex log_mutex; std::ofstream log_file; // The console is written by a thread of its own. Windows holds a console's output while text // in its window is selected, and a pipe nobody reads fills up: whoever writes then waits. When // that was the server loop, the server read nothing from its players until the console let go, // and they were gone by then. The log file is still written where the line is made. struct Console { std::mutex mutex; std::condition_variable more, drained; std::deque lines; std::size_t skipped{}; bool writing{}, started{}; void write(std::string line) { std::lock_guard lock(mutex); if (!started) { started = true; std::thread([this] { run(); }).detach(); } // A console that stays held must not grow this without end: the oldest lines go, and // the file has them all. if (lines.size() >= 4096) { lines.pop_front(); ++skipped; } lines.push_back(std::move(line)); more.notify_one(); } void run() { for (;;) { std::unique_lock lock(mutex); more.wait(lock, [&] { return !lines.empty(); }); const auto line = std::move(lines.front()); lines.pop_front(); const auto missed = std::exchange(skipped, 0); writing = true; lock.unlock(); if (missed) std::printf("(%zu earlier lines are only in ReSkateServer.log: the console was not taking output)\n", missed); std::fputs(line.c_str(), stdout); lock.lock(); writing = false; if (lines.empty()) drained.notify_all(); } } // Waits until everything written so far is on screen, but not for a console that is held. void flush(std::chrono::milliseconds limit) { std::unique_lock lock(mutex); drained.wait_for(lock, limit, [&] { return lines.empty() && !writing; }); } }; Console &console() { static auto *value = new Console; // outlives every thread that may still be writing at exit return *value; } void write_log(const std::string &text) { std::lock_guard lock(log_mutex); char stamp[64]{}; char file_stamp[64]{}; #ifdef _WIN32 SYSTEMTIME st; GetLocalTime(&st); if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, &st, nullptr, stamp, sizeof(stamp)) == 0) { const bool pm = st.wHour >= 12; const int hour12 = (st.wHour % 12 == 0) ? 12 : (st.wHour % 12); std::snprintf(stamp, sizeof(stamp), "%d:%02d:%02d %s", hour12, st.wMinute, st.wSecond, pm ? "PM" : "AM"); } std::snprintf(file_stamp, sizeof(file_stamp), "%04d-%02d-%02d %s", st.wYear, st.wMonth, st.wDay, stamp); #else const auto now_tp = std::chrono::system_clock::now(); const auto now_t = std::chrono::system_clock::to_time_t(now_tp); std::tm local{}; localtime_r(&now_t, &local); std::strftime(stamp, sizeof(stamp), "%X", &local); std::strftime(file_stamp, sizeof(file_stamp), "%Y-%m-%d %H:%M:%S", &local); #endif if (log_file) log_file << '[' << file_stamp << "] " << text << std::endl; console().write(std::string("[") + stamp + "] " + text + "\n"); } std::atomic finished{}; #ifdef _WIN32 BOOL WINAPI on_console(DWORD event) { stopping = true; // Closing the window ends the process as soon as this returns. Wait (Windows // allows about 5 s) for the server to sign out of Steam, so its entry leaves // the server list instead of lingering there. if (event == CTRL_CLOSE_EVENT || event == CTRL_LOGOFF_EVENT || event == CTRL_SHUTDOWN_EVENT) for (int i = 0; i < 90 && !finished; ++i) Sleep(50); return TRUE; } #else void on_signal(int) { stopping = true; } #endif std::filesystem::path folder() { #ifdef _WIN32 std::wstring path(32768, L'\0'); const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); if (!length || length >= path.size()) throw std::runtime_error("Cannot find ReSkateServer.exe"); path.resize(length); return std::filesystem::path(path).parent_path(); #else char path[4096]{}; const auto length = readlink("/proc/self/exe", path, sizeof(path) - 1); if (length <= 0) throw std::runtime_error("Cannot find ReSkateServer"); return std::filesystem::path(std::string(path, static_cast(length))).parent_path(); #endif } // Console lines, read on their own thread so the network loop never waits for typing. // One reader for the whole process: run() can start again after a failed update. struct Input { std::mutex mutex; std::deque lines; #ifdef _WIN32 void run() { std::string line; while (!stopping && std::getline(std::cin, line)) { std::lock_guard lock(mutex); lines.push_back(line); } } #else std::thread worker; // Raw poll()+read() on stdin instead of iostream: the thread holds no C++ // stream locks, so process exit can never deadlock against it, and poll() // wakes regularly so stopping is noticed without closing stdin. void run() { // Control pipes (fleet tools, `echo cmd > fifo`) open a writer per // command: read() returning 0 then means "no writers right now", not // end of input, so keep waiting instead of ending console input. const bool fifo = [] { struct stat info{}; return fstat(STDIN_FILENO, &info) == 0 && S_ISFIFO(info.st_mode); }(); std::string pending; pending.reserve(256); for (;;) { if (stopping) return; pollfd waiting{STDIN_FILENO, POLLIN, 0}; const int ready = poll(&waiting, 1, 200); if (ready < 0) { if (errno == EINTR) continue; return; } if (!ready) continue; // timeout: re-check stopping if (!(waiting.revents & (POLLIN | POLLHUP))) return; char buffer[4096]; const ssize_t count = read(STDIN_FILENO, buffer, sizeof(buffer)); if (count < 0) { if (errno == EINTR || errno == EAGAIN) continue; return; } if (count == 0) { if (!fifo) return; // EOF on files, /dev/null, terminals: input is over // No writers at the moment; sleep so POLLHUP doesn't spin. std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } for (ssize_t i = 0; i < count; ++i) { if (buffer[i] == '\n') { // CRLF input (a script saved on Windows, a panel or telnet): without // this, "quit\r" is an unknown command and the server keeps running. if (!pending.empty() && pending.back() == '\r') pending.pop_back(); std::lock_guard lock(mutex); lines.push_back(pending); pending.clear(); } else { pending.push_back(buffer[i]); } } } } void shutdown() { if (worker.joinable()) worker.join(); } void ensure_running() { std::lock_guard lock(mutex); if (!worker.joinable()) worker = std::thread([this] { run(); }); } #endif std::deque take() { std::lock_guard lock(mutex); return std::exchange(lines, {}); } }; Input &console_input() { static Input input; #ifdef _WIN32 static std::once_flag started; std::call_once(started, [] { std::thread([] { input.run(); }).detach(); }); #else input.ensure_running(); #endif return input; } constexpr auto update_interval = std::chrono::minutes(30); #ifdef _WIN32 // Double-clicked, the server is alone on a visible console that closes with it. // From a terminal, a script, a hosting panel or a hidden scheduled task it is not. bool own_window() { DWORD processes[2]{}; const auto window = GetConsoleWindow(); return GetConsoleProcessList(processes, 2) == 1 && window && IsWindowVisible(window) && GetFileType(GetStdHandle(STD_INPUT_HANDLE)) == FILE_TYPE_CHAR; } #endif } // namespace #ifdef _WIN32 int run(int argc, wchar_t **argv, bool skip_update) { // Unbuffered, so a hosting panel reading the pipe sees each line at once. setvbuf(stdout, nullptr, _IONBF, 0); SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hOut != INVALID_HANDLE_VALUE) { DWORD dwMode = 0; if (GetConsoleMode(hOut, &dwMode)) { dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(hOut, dwMode); } } SetConsoleCtrlHandler(on_console, TRUE); const auto here = folder(); // Release builds: ReSkateServer --export-world-layers "" "" writes the // players' world-layer catalog from the game's level data (release\build_release.bat). if (argc == 4 && std::wstring(argv[1]) == L"--export-world-layers") { try { const auto catalog = world_layer_scan::scan(argv[2]); if (catalog.layers.empty()) throw std::runtime_error("no world layers found; is that the Skate folder?"); if (catalog.layers.size() != supported_build::world_layer_count) throw std::runtime_error("that game has " + std::to_string(catalog.layers.size()) + " world layers, and the build ReSkate supports has " + std::to_string(supported_build::world_layer_count) + ": it is another version of skate. (one Steam has updated?). Give the folder ReSkate's own game is in"); std::ofstream out(argv[3], std::ios::binary | std::ios::trunc); out << world_layer_scan::to_json(catalog, "server"); if (!out) throw std::runtime_error("cannot write the file"); std::printf("Wrote %zu world layers.\n", catalog.layers.size()); return 0; } catch (const std::exception &e) { std::printf("World layer export failed: %s\n", e.what()); return 1; } } auto config_file = here / L"ReSkateServer.json"; for (int i = 1; i + 1 < argc; ++i) if (std::wstring(argv[i]) == L"--config") config_file = argv[++i]; if (!log_file.is_open()) log_file.open(here / L"ReSkateServer.log", std::ios::app); #else int run(int argc, char **argv, bool skip_update) { setvbuf(stdout, nullptr, _IONBF, 0); std::signal(SIGINT, on_signal); std::signal(SIGTERM, on_signal); // A hosting panel that dies leaves stdout a pipe with no reader. Without this // the next printf kills the server before it logs, saves or signs out of Steam. std::signal(SIGPIPE, SIG_IGN); const auto here = folder(); if (argc == 4 && std::string(argv[1]) == "--export-world-layers") { try { const auto catalog = world_layer_scan::scan(std::filesystem::path(argv[2])); if (catalog.layers.empty()) throw std::runtime_error("no world layers found; is that the Skate folder?"); if (catalog.layers.size() != supported_build::world_layer_count) throw std::runtime_error("that game has " + std::to_string(catalog.layers.size()) + " world layers, and the build ReSkate supports has " + std::to_string(supported_build::world_layer_count) + ": it is another version of skate. (one Steam has updated?). Give the folder ReSkate's own game is in"); std::ofstream out(std::filesystem::path(argv[3]), std::ios::binary | std::ios::trunc); out << world_layer_scan::to_json(catalog, "server"); if (!out) throw std::runtime_error("cannot write the file"); std::printf("Wrote %zu world layers.\n", catalog.layers.size()); return 0; } catch (const std::exception &e) { std::printf("World layer export failed: %s\n", e.what()); return 1; } } auto config_file = here / "ReSkateServer.json"; for (int i = 1; i + 1 < argc; ++i) if (std::string(argv[i]) == "--config") config_file = argv[++i]; if (!log_file.is_open()) log_file.open(here / "ReSkateServer.log", std::ios::app); #endif ServerConfig config; try { const bool fresh = !std::filesystem::exists(config_file); std::vector added; config = load_config(config_file, &added); if (fresh) write_log("Wrote a default " + path_utf8(config_file.filename()) + ". Edit it to name the server and add admins."); if (!added.empty()) { std::string names; for (const auto &name : added) names += (names.empty() ? "" : ", ") + name; write_log("Added new settings to " + path_utf8(config_file.filename()) + " with their defaults: " + names + "."); } } catch (const std::exception &e) { write_log("Cannot read " + path_utf8(config_file) + ": " + e.what()); return 1; } // Maps: the retail ones and custom maps from Mods\\reskate-levels.json. for (const auto &problem : load_levels(here / "Mods")) write_log("Mods: skipped " + problem); if (levels().size() > 6) write_log("Mods: " + std::to_string(levels().size() - 6) + " custom map(s)."); // Older configs name the map by its full destination; keep the plain name instead. bool renamed{}; if (const auto setting = map_setting(config.map); setting != config.map && !setting.empty()) { config.map = setting; renamed = true; } for (auto &map : config.map_pool) // short pool names ("isle") are saved in full if (const auto *level = find_level(map); level && level->name != map) { map = level->name; renamed = true; } for (auto &park : config.park_mods) // and so are the maps the park mods belong to if (const auto *level = find_level(park.map); level && level->name != park.map) { park.map = level->name; renamed = true; } config.mods = here / "Mods"; if (renamed) try { save_config(config); } catch (...) {} if (const auto error = config_error(config); !error.empty()) { write_log("Config problem: " + error); return 1; } // Updates: at startup, then every half hour while running (installed once // nobody is on). --no-update or "auto_update": false turns them off. remove_previous_update(here); bool auto_update = config.auto_update && updates_enabled() && !skip_update; for (int i = 1; i < argc; ++i) #ifdef _WIN32 if (std::wstring(argv[i]) == L"--no-update") auto_update = false; #else if (std::string(argv[i]) == "--no-update") auto_update = false; #endif if (auto_update) { write_log("Checking for updates..."); const auto check = check_for_update(); if (check.available) { update_version = check.version; write_log("Server update " + update_version + " found."); return restart_for_update; } write_log(check.problem.empty() ? "The server is up to date (" + check.version + ")." : "Update check skipped: " + check.problem + "."); } // World layers are optional: without the players' catalog every player keeps their own. if (const auto catalog = here / "world-layers.json"; std::filesystem::exists(catalog)) { try { auto layers = world_layer_scan::read(catalog); // (Players would turn down every message that carries the layers, and could not join.) if (layers.layers.size() != supported_build::world_layer_count) throw std::runtime_error("it has " + std::to_string(layers.layers.size()) + " layers and players' games have " + std::to_string(supported_build::world_layer_count) + ": it is of another version of skate. Take the one from this server's release"); install_world_layer_catalog(std::move(layers)); write_log("World layers: " + std::to_string(world_layers().size()) + " from world-layers.json."); } catch (const std::exception &e) { write_log(std::string("world-layers.json is unreadable; world layer sync is off: ") + e.what()); } } SteamServer steam; std::string error; if (!steam.start(here, config.port, config.query_port, config.steam_token, error)) { write_log(error); return 1; } write_log(config.steam_token.empty() ? "Signing in to Steam..." : "Signing in to Steam with steam_token..."); const auto login_started = std::chrono::steady_clock::now(); while (!steam.logged_on() && !stopping) { steam.run_callbacks(); if (std::chrono::steady_clock::now() - login_started > std::chrono::seconds(60)) { write_log(config.steam_token.empty() ? "Steam sign-in timed out after 60 s. Check the internet connection and try again." : "Steam sign-in timed out after 60 s. Steam may not have accepted steam_token: it must be a token for " "app 3354750 that no other running server is using. Or check the internet connection."); return 1; } std::this_thread::sleep_for(std::chrono::milliseconds(20)); } if (stopping) return 0; multiplayer::SteamTransport transport; if (!transport.open_game_server(steam.module())) { write_log("Steam networking failed: " + transport.status().detail); return 1; } // A check of another server's direct connections, from here: RESKATE_PROBE_DIRECT set to // ",,". Signs in as this server would, connects // as a game does (the address first, Steam's relays after five seconds), says which it got, // and exits. Run it from a machine outside the other server's network. std::string probe; #ifdef _WIN32 { char *value{}; std::size_t length{}; if (!_dupenv_s(&value, &length, "RESKATE_PROBE_DIRECT") && value) probe = value; std::free(value); } #else if (const char *value = std::getenv("RESKATE_PROBE_DIRECT")) probe = value; #endif if (!probe.empty()) { unsigned long long id{}; unsigned port{}; const auto first = probe.find(','), second = probe.find(',', first == std::string::npos ? 0 : first + 1); const auto address = first != std::string::npos && second != std::string::npos ? direct_ipv4(std::string_view(probe).substr(first + 1, second - first - 1)) : 0; const bool parsed = address && std::from_chars(probe.data(), probe.data() + first, id).ec == std::errc{} && std::from_chars(probe.data() + second + 1, probe.data() + probe.size(), port).ec == std::errc{}; if (!parsed || !id || !port || port > 65535) { write_log("RESKATE_PROBE_DIRECT must be ,,."); return 1; } const unsigned a = address >> 24, b = (address >> 16) & 255, c = (address >> 8) & 255, d = address & 255; write_log("Probe: connecting straight to " + std::to_string(a) + "." + std::to_string(b) + "." + std::to_string(c) + "." + std::to_string(d) + ":" + std::to_string(port) + " as a game would..."); if (!transport.join(id, address, static_cast(port))) { write_log("Probe: Steam would not start the connection: " + transport.status().detail); return 1; } std::string said; const auto started = std::chrono::steady_clock::now(); while (!stopping && std::chrono::steady_clock::now() - started < std::chrono::seconds(30)) { steam.run_callbacks(); transport.poll(); if (const auto &detail = transport.status().detail; detail != said) { said = detail; write_log("Probe: " + said); } const auto links = transport.links(); if (links.empty()) { write_log("Probe: no connection either way. " + transport.take_closed(id)); return 1; } if (links.front().connected && links.front().measured) { write_log(links.front().direct ? "Probe: CONNECTED DIRECTLY, ping " + std::to_string(links.front().ping_ms) + " ms." : "Probe: the address did not answer; connected through Steam's relays (" + links.front().relay + "-" + links.front().remote_relay + "), ping " + std::to_string(links.front().ping_ms) + " ms."); return 0; } std::this_thread::sleep_for(std::chrono::milliseconds(20)); } write_log("Probe: nothing after 30 s."); return 1; } Host host(config, transport, write_log); if (!host.start(error)) { write_log("Could not open the server: " + error); return 1; } write_log(config.name + " is up on " + host.map_name() + " for " + std::to_string(config.max_players) + " players."); write_log("Steam ID " + std::to_string(steam.steam_id()) + (config.steam_token.empty() ? " (anonymous: new every start; set steam_token to keep one)" : " (from steam_token: the same every start)") + ", public IP " + steam.public_ip() + "."); write_log("Join code: " + host.invite() + (config.password.empty() ? "" : " (password required)")); if (config.steam_token.empty() && config.listed) write_log("No steam_token: the server browser can be set to show only servers that have one, and then " "this server is not in it (players can still join with the code). It takes a minute to make " "one: see steam_token in README."); { std::size_t admins{}; for (const auto &[id, name] : config.roles.players) if (const auto *role = config.roles.find(name); role && role->granted_all) ++admins; write_log(!admins ? "No admins yet: type \"role admin\" to add one. Roles are in data/roles.json." : std::to_string(admins) + " admin(s), " + std::to_string(config.roles.players.size()) + " player(s) with a role. Type help for commands."); } auto &input = console_input(); #ifdef _WIN32 timeBeginPeriod(1); #endif auto next_advertise = std::chrono::steady_clock::now(); std::optional name_allowed; // last seen: whether the name may be listed bool tokens_required{}; // last seen: whether the browser wants a steam_token auto next_update_check = next_advertise + update_interval; std::future update_check; bool update_now{}, update_waiting{}, restart{}; // The backend's ban list (global_bans.h): read now and every ten minutes, a minute after a // failure. "global_bans": false leaves it unread and lets those players in. std::future ban_check; auto next_ban_check = next_advertise; bool bans_unread{}; MapUpdater map_updater; // "maps.latest_versions" and "maps update" (server_map_update.h) if (!config.global_bans) write_log("Global bans are off (\"use_global_bans\": false): only this server's own bans apply."); write_log(host.command("words")); // reads this server's own chat words // Signed in to Steam, as last logged, and when it is looked at again. Players already on stay // connected through a lost sign-in, but nobody new can join: Steam carries the first messages // of a connection, and theirs time out ("negotiate rendezvous"). bool signed_in = true; auto next_sign_in_check = std::chrono::steady_clock::now(); while (!stopping && !restart) { steam.run_callbacks(); if (const auto now = std::chrono::steady_clock::now(); now >= next_sign_in_check) { next_sign_in_check = now + std::chrono::seconds(5); if (const bool on = steam.logged_on(); on != signed_in) { signed_in = on; write_log(on ? "[steam] Signed in to Steam again: players can join." : "[steam] No longer signed in to Steam: players already on stay, but nobody can join until it is back. " "Steam signs a server out when another one signs in with the same steam_token, and for its own " "maintenance or a lost connection."); } } try { host.tick(multiplayer::now_us()); } catch (const std::exception &e) { write_log(std::string("Server error: ") + e.what()); } for (const auto &line : input.take()) { if (line == "quit" || line == "exit" || line == "stop") { stopping = true; break; } if (line.empty()) continue; // "update": check now and install straight away, even with players on. if (line == "update") { if (!updates_enabled()) { write_log("This is a local build; it doesn't update itself."); } else if (update_waiting) { write_log("Restarting to install server update " + update_version + "."); restart = true; } else { update_now = true; if (!update_check.valid()) update_check = std::async(std::launch::async, check_for_update); write_log("Checking for updates..."); } continue; } try { write_log(host.command(line)); } catch (const std::exception &e) { write_log(std::string("Command failed: ") + e.what()); } } const auto now_time = std::chrono::steady_clock::now(); if (auto_update && !update_waiting && !update_check.valid() && now_time >= next_update_check) update_check = std::async(std::launch::async, check_for_update); if (update_check.valid() && update_check.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { const auto check = update_check.get(); next_update_check = now_time + update_interval; if (check.available) { update_version = check.version; update_waiting = true; if (!update_now) write_log("Server update " + update_version + " is ready; the server restarts to install it when nobody is on."); } else if (update_now) { write_log(check.problem.empty() ? "The server is up to date (" + check.version + ")." : "Update check failed: " + check.problem + "."); } if (update_waiting && update_now) { write_log("Restarting to install server update " + update_version + "."); restart = true; } update_now = false; } if (const auto maps = map_updater.tick(config.latest_versions, config.latest_check_minutes, config.mods, host.map_folder_in_play(), host.take_map_update_request()); !maps.lines.empty() || maps.changed) { for (const auto &line : maps.lines) write_log(line); if (maps.changed) write_log(host.reload_maps()); } // (The chat word lists come in the same answer, and hold on every server: it is read // for them whether or not the bans are used.) if (!ban_check.valid() && now_time >= next_ban_check) ban_check = std::async(std::launch::async, read_global_bans); if (ban_check.valid() && ban_check.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { const auto check = ban_check.get(); next_ban_check = now_time + (check.ok ? std::chrono::minutes(10) : std::chrono::minutes(1)); // Said when it changes, not every ten minutes. if (config.global_bans && check.ok && (check.changed || bans_unread)) write_log("Global bans: " + std::to_string(check.banned) + " player(s) banned from ReSkate multiplayer cannot join."); if (check.ok && check.words_changed) write_log("Word lists: " + std::to_string(check.filtered_words) + " filtered and " + std::to_string(check.forbidden_words) + " not allowed at all, from the ReSkate team's lists." + (config.word_warnings ? "" : " (\"word_warnings\" is 0: a message with one is not passed on, and nobody is kicked).")); else if (!check.ok && !bans_unread) write_log("The ReSkate team's lists (global bans and chat words) could not be read (" + check.problem + "). Trying again every minute; until then the ones already read hold."); bans_unread = !check.ok; } if (update_waiting && !restart && host.players() == 0) { write_log("Nobody is on; restarting to install server update " + update_version + "."); restart = true; } if (const auto now = std::chrono::steady_clock::now(); now >= next_advertise) { next_advertise = now + std::chrono::seconds(2); // The ReSkate team's rule, read with its ban list: say when it starts or stops hiding this server. if (const bool required = multiplayer::server_tokens_required(); required != tokens_required) { tokens_required = required; if (config.steam_token.empty() && config.listed) write_log(required ? "The server browser now shows only servers with a steam_token, so this server is " "hidden from it. Add a steam_token (see README) to be listed again." : "The server browser shows servers without a steam_token again."); } // A name with a bad word in it is never listed (clients hide one too); the // server still runs and players can join with its code. const bool allowed = !text::contains_bad_words(config.name); if (name_allowed != allowed) { if (!allowed) write_log("The server name \"" + config.name + "\" contains blocked words, so the server is not listed " "in the server browser. Rename it with: name "); else if (name_allowed) write_log(config.listed ? "The server name is allowed again; the server is listed." : "The server name is allowed again (the server is still set to unlisted)."); name_allowed = allowed; } steam.advertise({config.name, host.map_name(), host.players(), config.max_players, !config.password.empty(), config.listed && allowed, host.secret(), host.direct_port()}); } std::this_thread::sleep_for(std::chrono::milliseconds(2)); } #ifdef _WIN32 timeEndPeriod(1); #else // Join the console reader before touching Steam shutdown and process exit: // it holds no stream locks, and poll() notices stopping within ~200 ms. input.shutdown(); #endif if (update_check.valid()) update_check.wait(); if (ban_check.valid()) ban_check.wait(); write_log(restart ? "Restarting for an update." : "Shutting down."); host.stop(restart ? "The server is restarting for an update. Rejoin in a minute." : "The server is shutting down."); // Leaving scope closes the networking before Steam itself shuts down. return restart ? restart_for_update : 0; } namespace { // Both entry points: run the server, and install an update and start it when run() asks. int serve(int argc, auto **argv) { #ifdef _WIN32 constexpr const char *exe = "ReSkateServer.exe"; #else constexpr const char *exe = "ReSkateServer"; #endif bool skip_update{}; for (;;) { const int code = run(argc, argv, skip_update); if (code != restart_for_update) return code; // Steam has shut down, so every server file can be replaced now. try { write_log("Installing server update " + update_version + "..."); install_update(folder()); write_log("Server update " + update_version + " installed; starting it."); console().flush(std::chrono::seconds(2)); if (relaunch()) return 0; write_log(std::string("Could not start the updated server; start ") + exe + " again."); return 1; } catch (const std::exception &e) { write_log(std::string("Server update failed (") + e.what() + "); carrying on with this version."); skip_update = true; } } } } // namespace #ifdef _WIN32 #include "GUI/server_gui.h" #include int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int cmd_show) { int argc = 0; LPWSTR *argvW = CommandLineToArgvW(GetCommandLineW(), &argc); bool headless = false; for (int i = 1; i < argc; ++i) { if (!argvW[i]) continue; std::wstring arg = argvW[i]; // The release build's world layer export (serve) is a console job too: the window would // open instead and nothing would be written. if (arg == L"--headless" || arg == L"--cli" || arg == L"--no-gui" || arg == L"-nogui" || arg == L"-headless" || arg == L"-cli" || arg == L"--export-world-layers") { headless = true; break; } } if (headless) { // A server started with its output piped or in a file (by a manager or a service) keeps // what it was given: reopening CONOUT$ closes it, and then nothing the server writes, or // is sent, gets through. Started by hand, it takes its parent's console or one of its own. const HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE); const DWORD out_type = out && out != INVALID_HANDLE_VALUE ? GetFileType(out) : FILE_TYPE_UNKNOWN; const bool redirected = out_type == FILE_TYPE_PIPE || out_type == FILE_TYPE_DISK; if (!redirected && (AttachConsole(ATTACH_PARENT_PROCESS) || AllocConsole())) { FILE *fp = nullptr; freopen_s(&fp, "CONOUT$", "w", stdout); freopen_s(&fp, "CONOUT$", "w", stderr); freopen_s(&fp, "CONIN$", "r", stdin); std::ios::sync_with_stdio(true); } const int code = serve(argc, argvW); LocalFree(argvW); finished = true; if (code != 0 && !stopping && own_window()) { auto &input = console_input(); input.take(); console().write("Press Enter to close.\n"); while (!stopping && input.take().empty()) Sleep(50); } console().flush(std::chrono::seconds(2)); return code; } LocalFree(argvW); try { return dingosdk::server_gui::run_gui(instance, cmd_show); } catch (const std::exception &e) { std::ofstream f("gui_exception.txt"); f << "EXCEPTION: " << e.what() << "\n"; return 1; } catch (...) { std::ofstream f("gui_exception.txt"); f << "UNKNOWN EXCEPTION\n"; return 1; } } #else int main(int argc, char **argv) { const int code = serve(argc, argv); finished = true; console().flush(std::chrono::seconds(2)); return code; } #endif