// A stand-in for VintagestoryServer, used by tests. It copies the behavior measured // on 1.22.7 (see the design doc) and can be told to misbehave through environment // variables: // FAKE_MODE Normal | NoReady | FailOnStart | CrashAfterReady | IgnoreStop | IgnoreAll // FAKE_STARTUP_MS delay before the ready line (default 500) // FAKE_STOP_MS time the shutdown sequence takes (default 200) // FAKE_CRASH_AFTER_MS delay between ready and the crash in CrashAfterReady (default 300) // FAKE_CRASH_EXIT_CODE exit code used when crashing (default 0, like the real game) // FAKE_STDIN_EOF "stop" makes the server stop when stdin closes (default: ignore it) // // Deliberate differences from the real game: // - --dataPath is required, so a test can never touch a default location. // - Only strict JSON is accepted by --setconfig, not the relaxed syntax. // - If a value-taking flag swallows --dataPath, this exits with 2 where the real // game would fall back to its default path. using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; using FakeGameServer; using static FakeGameServer.FakeOutput; const int SetConfigSuccessExitCode = 200; var cli = CommandLine.Parse(args); if (cli.DataPath is null) { await Console.Error.WriteLineAsync("fake: --dataPath is required, refusing to use a default location"); return 2; } var fake = FakeSettings.FromEnvironment(); var dataPath = Path.GetFullPath(cli.DataPath); Directory.CreateDirectory(dataPath); Log("Server Notification", "Server logger started."); Log("Server Notification", "Game Version: v0.0.0 (Fake)"); var configPath = Path.Combine(dataPath, "serverconfig.json"); JsonObject config; if (File.Exists(configPath)) { config = JsonNode.Parse(File.ReadAllText(configPath))?.AsObject() ?? new JsonObject(); } else { Log("Server Notification", "serverconfig.json not found, creating new one"); config = new JsonObject { ["ServerName"] = "Vintage Story Server", ["Port"] = 42420, ["MaxClients"] = 16, }; } if (cli.SetConfig is not null) { JsonObject patch; try { patch = JsonNode.Parse(cli.SetConfig)?.AsObject() ?? throw new JsonException("null value"); } catch (Exception ex) when (ex is JsonException or InvalidOperationException) { // Unverified: whether the real game leaves a config file behind after this failure. // The fake does not. var first = cli.SetConfig.TrimStart('-').FirstOrDefault(); return Crash( dataPath, fake.CrashExitCode, "Newtonsoft.Json.JsonReaderException: Unexpected character encountered while " + $"parsing value: {first}. Path '', line 0, position 0."); } foreach (var (key, value) in patch.ToList()) { config[key] = value?.DeepClone(); Log("Server Notification", $"Ok, value {value} set for {key}."); } File.WriteAllText(configPath, config.ToJsonString(JsonOptions.Indented)); return SetConfigSuccessExitCode; } File.WriteAllText(configPath, config.ToJsonString(JsonOptions.Indented)); var port = cli.Port ?? config["Port"]?.GetValue() ?? 42420; using var shutdown = new CancellationTokenSource(); var gate = new object(); string? stopReason = null; var stopViaConsole = false; void RequestStop(string reason, bool viaConsole) { lock (gate) { if (stopReason is not null) { return; } stopReason = reason; stopViaConsole = viaConsole; } shutdown.Cancel(); } void OnSignal(PosixSignalContext context) { context.Cancel = true; if (fake.Mode != FakeMode.IgnoreAll) { RequestStop("Shutdown via signal", false); } } using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnSignal); using var sigint = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnSignal); _ = Task.Run(async () => { string? line; while ((line = await Console.In.ReadLineAsync()) is not null) { line = line.Trim(); if (line.Length == 0) { continue; } Log("Server Notification", $"Handling Console Command {line}"); var ignoresStop = fake.Mode is FakeMode.IgnoreStop or FakeMode.IgnoreAll; if (line == "/stop" && !ignoresStop) { RequestStop("Shutdown via server command", true); } } if (fake.StdinEofStopsServer) { RequestStop("stdin closed", false); } }); Log("Server Event", "Launching server..."); Log("Server Notification", "Entering runphase Configuration"); Log("Server Notification", "Loading savegame"); try { await Task.Delay(fake.StartupMs, shutdown.Token); } catch (OperationCanceledException) { // A stop request during startup is handled below. } if (!shutdown.IsCancellationRequested) { if (fake.Mode == FakeMode.FailOnStart) { return Crash(dataPath, fake.CrashExitCode, $"Fake failure: could not bind port {port}"); } if (fake.Mode != FakeMode.NoReady) { Log("Server Event", $"Dedicated Server now running on Port {port} and all ips!"); } var wait = fake.Mode == FakeMode.CrashAfterReady ? fake.CrashAfterMs : Timeout.Infinite; try { await Task.Delay(wait, shutdown.Token); } catch (OperationCanceledException) { // Stop requested. } if (!shutdown.IsCancellationRequested) { return Crash(dataPath, fake.CrashExitCode, "Fake failure: simulated crash while running"); } } string reason; bool viaConsole; lock (gate) { reason = stopReason ?? "unknown"; viaConsole = stopViaConsole; } Log("Server Notification", $"Server stop requested, begin shutdown sequence. Stop reason: {reason}"); Log("Server Notification", "Entering runphase Shutdown"); await Task.Delay(fake.StopMs); Log("Server Event", "World saved!"); Log("Server Event", "Stopped the server!"); if (viaConsole) { Log("Server Notification", "Shut down command executed"); } return 0;