Files
ReSkate/Server/main.cpp
T
AyeZeeBBandClaude Opus 5.5 d2b8e18097 2.0.7 hotfix: map updates, vote bar, ReSkate Admin tag, servers on the newest map version
Map updates
- A map the game already has, being replaced by the host's version,
  could load as soon as the new files were in Mods, while the mods
  were still being applied: it ended on "This map is not loading". The
  session now holds the load while a map is downloading, installing or
  applying, as it waits for one that is missing.
- A player whose version of a map is later than the host's keeps it and
  loads. Only an older one is replaced by the host's. Versions are
  compared number by number (1.10.0 is later than 1.9.2).

Dedicated servers
- maps.latest_versions (off by default): the server asks Thunderstore
  for the newest version of each of its custom maps, at start and every
  maps.latest_check_minutes (5-1440, default 60), and writes it into
  the mod's manifest.json. Nothing is downloaded: the server reads only
  a map's level list and version, and players, who are held to the
  server's version, fetch it as they join. The map being played waits
  until nobody is on it. A folder that is not a Thunderstore package is
  not touched, and the level list is left as it is.
- Console: maps update, maps latest on|off, maps latest every <minutes>.

Overlay
- Vote card: the pass mark is always in the middle of the bar. Green
  grows over the left half up to the yes votes needed and over the
  right half past them, whatever share the vote needs.
- The "Staff" tag is "ReSkate Admin", shimmering from #2500FF to
  #B2F9FF.

No protocol change. Tests cover the version comparison and the
manifest write; the Thunderstore lookup was checked against the site.
The map update flow, the vote bar and the tag were not seen in game,
and the server setting has not run on a live server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:19:49 -05:00

750 lines
35 KiB
C++

// 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 "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 <charconv>
#include <cstdlib>
#include <Windows.h>
#include <timeapi.h>
#else
#include <cerrno>
#include <csignal>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <ctime>
#include <deque>
#include <fstream>
#include <future>
#include <iostream>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
using namespace dingosdk;
using namespace dingosdk::server;
namespace {
std::atomic<bool> 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<std::string> 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<bool> 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<DWORD>(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<std::size_t>(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<std::string> 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<std::string> 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 "<Skate folder>" "<file>" 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?");
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?");
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<std::string> 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\<mod>\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 {
install_world_layer_catalog(world_layer_scan::read(catalog));
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
// "<its SteamID64>,<its address a.b.c.d>,<its port>". 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 <SteamID64>,<a.b.c.d>,<port>.");
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<std::uint16_t>(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 <SteamID64> 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<bool> 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<UpdateCheck> 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<BanListCheck> 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 <new 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 <shellapi.h>
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) {
if (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