Files
ReSkate/Server/main.cpp
T
AyeZeeBB 55a7342dc6 Multiplayer: dedicated servers require plugins, and players get them and the map on one card
A server lists Thunderstore packages in plugins.required ("Owner-Name", or "Owner-Name-1.2.3"
for at least that version, at most 16). It checks each on Thunderstore as it starts and leaves
out one not listed as a plugin, and sends the rest with its map offer (protocol 55).

A joining player's game checks them before the server's map loads. Those missing, switched off
or too old go on one card with the map when that is missing too, so the player is asked once.
GET ALL downloads and installs each, switches on any that were off, runs one live merge when
any brings game data, and joins; CANCEL leaves. The game fetches a package only when
Thunderstore lists it as a plugin and it installs as one (a reskate-plugin.json naming Luau
code), whatever a server asks; anything else is taken out again, so a cosmetic can never be
required.

map_download handles a list of packages instead of one map, and the Thunderstore chooser takes
the category it needs. A map alone keeps its card as it was.
2026-10-24 12:00:09 -05:00

775 lines
37 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 "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 <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?");
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<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 {
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
// "<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)
RequiredPluginCheck plugin_check; // "plugins.required" (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());
}
if (auto plugins = plugin_check.tick(config.required_plugins); plugins.finished) {
for (const auto &line : plugins.lines) write_log(line);
host.refuse_plugins(std::move(plugins.refused));
}
// (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) {
// 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