using System.Runtime.InteropServices; using MSCO.Server; // Datenordner (server.json, save/, world.json): Standard = Programmordner, // unter Linux z.B. "--data /var/lib/msco" oder per Umgebungsvariable MSCO_DATA string dataDir = ArgValue("--data") ?? Environment.GetEnvironmentVariable("MSCO_DATA") ?? AppContext.BaseDirectory; dataDir = Path.GetFullPath(dataDir); Directory.CreateDirectory(dataDir); // Im Panel installiertes, neueres Update (DATA/app/current)? Dann läuft stattdessen das. if (Array.IndexOf(args, "--cmd") < 0 && Updater.RunInstalled(args, dataDir) is int updatedExit) return updatedExit; string configPath = Path.Combine(dataDir, "server.json"); string commandFile = Path.Combine(dataDir, "commands.in"); // --cmd "": Befehl an den laufenden Server geben (z.B. docker exec msco-server msco op 1). // Der Server liest die Datei jede halbe Sekunde; die Antwort steht im Server-Log (docker logs). if (Array.IndexOf(args, "--cmd") >= 0) { int at = Array.IndexOf(args, "--cmd"); string command = string.Join(" ", args.Skip(at + 1)).Trim(); if (command.Length == 0) { Console.WriteLine("Usage: msco (e.g. msco players, msco op 1, msco say hello)"); return 1; } File.AppendAllText(commandFile, command + Environment.NewLine); Console.WriteLine("Sent: " + command + " (output: docker logs --tail 20 msco-server)"); return 0; } try { Console.Title = "MSCO Server"; } catch { } // nicht jedes Terminal unterstützt einen Titel GameServer.Log("MSCO dedicated server " + Updater.CurrentVersion + " for MSCO mod " + GameServer.ModVersion + " (" + RuntimeInformation.OSDescription + ")"); GameServer.Log("Data folder: " + dataDir); GameServer.Log("Master server: " + MasterServer.Url); // Datenordner beschreibbar? (Linux/Docker: gehört er einem anderen Benutzer, gleich klar melden statt abzustürzen) try { string probe = Path.Combine(dataDir, ".write-test"); File.WriteAllText(probe, "ok"); File.Delete(probe); } catch (Exception e) when (e is UnauthorizedAccessException or IOException) { GameServer.Log("No write access to the data folder " + dataDir + ": " + e.Message); GameServer.Log(GameServer.PermissionHint()); Pause(); return 1; } ServerConfig config; bool created; try { config = ServerConfig.LoadOrCreate(configPath, out created); } catch (Exception e) when (e is UnauthorizedAccessException or IOException) { GameServer.Log("Cannot read " + configPath + ": " + e.Message); GameServer.Log(GameServer.PermissionHint()); Pause(); return 1; } var fromEnvironment = config.ApplyEnvironment(); if (fromEnvironment.Count > 0) GameServer.Log("Settings overridden by environment (.env): " + string.Join(", ", fromEnvironment)); if (created && string.IsNullOrWhiteSpace(config.LicenseKey)) { GameServer.Log("Created " + configPath); GameServer.Log("Get a license key at " + MasterServer.Url + "/keys" + (config.PanelEnabled ? " and enter it in the web panel (Settings) - or put it into server.json (\"LicenseKey\")." : " and put it into server.json (\"LicenseKey\").")); if (!config.PanelEnabled) { Pause(); return 1; } } // --import-save [Pfad]: Spielstand aus My Summer Car übernehmen if (Array.IndexOf(args, "--import-save") >= 0) { string source = ArgValue("--import-save") ?? DefaultSaveFolder(); if (source == null || !Directory.Exists(source)) { GameServer.Log("Save folder not found" + (source != null ? ": " + source : "") + ". Use --import-save \"/path/to/My Summer Car\" (the folder containing defaultES2File.txt)."); Pause(); return 1; } string target = Path.Combine(dataDir, "save"); Directory.CreateDirectory(target); int copied = 0; foreach (string name in new[] { "defaultES2File.txt", "items.txt", "meshsave.txt", "graveyard.txt", "notepad.txt" }) { string file = Path.Combine(source, name); if (!File.Exists(file)) continue; File.Copy(file, Path.Combine(target, name), true); copied++; } GameServer.Log($"Imported {copied} save files from {source}"); // Neuer Spielstand = alte Änderungen passen nicht mehr dazu, und er ist der neue Reset-Punkt string world = Path.Combine(dataDir, "world.json"); if (File.Exists(world)) File.Delete(world); string original = Path.Combine(dataDir, "save_original"); if (Directory.Exists(original)) Directory.Delete(original, true); } // Der Host läuft die ganze Zeit (Port + Web-Panel) und startet/stoppt den Gameserver - auch wenn der nicht starten // kann (Lizenz, Spielstand ...), bleibt das Panel erreichbar. Konsole, "msco " und Signale gehen an den Host. var host = new ServerHost(config, dataDir, configPath); // Konsolenbefehle in eigenem Thread lesen, ausgeführt werden sie in der Server-Schleife. // Als Dienst (systemd/Docker ohne Terminal) gibt es keine Eingabe - dann einfach ohne Konsole laufen. var input = new Thread(() => { try { string line; while ((line = Console.ReadLine()) != null) host.Command(line); } catch { } }) { IsBackground = true }; input.Start(); // Befehle von "msco " (docker exec) aus der Datei commands.in übernehmen var commandWatcher = new Thread(() => { string processing = commandFile + ".processing"; while (true) { Thread.Sleep(500); try { if (!File.Exists(commandFile)) continue; File.Move(commandFile, processing, true); foreach (string line in File.ReadAllLines(processing)) if (line.Trim().Length > 0) { GameServer.Log("> " + line.Trim()); host.Command(line.Trim()); } File.Delete(processing); } catch { } } }) { IsBackground = true }; commandWatcher.Start(); // Sauber beenden (Weltzustand speichern) bei Strg+C, systemctl stop / docker stop (SIGTERM) und SIGQUIT Console.CancelKeyPress += (_, e) => { e.Cancel = true; host.RequestExit(); }; using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => { ctx.Cancel = true; host.RequestExit(); }); using var sigquit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => { ctx.Cancel = true; host.RequestExit(); }); int exitCode = host.Run(); if (exitCode == Updater.RestartExitCode) Updater.Relaunch(args); else if (exitCode != 0) Pause(); return exitCode; string ArgValue(string name) { int i = Array.IndexOf(args, name); return i >= 0 && i + 1 < args.Length && !args[i + 1].StartsWith("--") ? args[i + 1] : null; } // Spielstand dieses Rechners: Windows direkt, Linux über Steam Proton static string DefaultSaveFolder() { string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); const string relative = "AppData/LocalLow/Amistech/My Summer Car"; if (OperatingSystem.IsWindows()) return Path.Combine(home, relative); foreach (string steamRoot in new[] { ".steam/steam", ".local/share/Steam", ".var/app/com.valvesoftware.Steam/.local/share/Steam" }) { string proton = Path.Combine(home, steamRoot, "steamapps/compatdata/516750/pfx/drive_c/users/steamuser", relative); if (Directory.Exists(proton)) return proton; } return null; } static void Pause() { // In Docker nicht auf Enter warten: sonst sieht der Container wie "läuft" aus, obwohl der Server beendet ist if (Console.IsInputRedirected || Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") return; Console.WriteLine("Press Enter to exit."); Console.ReadLine(); }