- Updater checks git.mbm.wtf every 6 h (and on demand) for a newer <Version> on main, shows the new commits and the required mod version - Install: download the source, build it with dotnet publish into data/app/current, warn players and restart (exit code 75 for Docker/systemd, self-relaunch on Windows). At startup a newer installed update is loaded in its own AssemblyLoadContext; after 3 failed starts the built-in version runs - Panel: Update page, sidebar badge, full-screen progress overlay (download, build, install, restart) that survives the restart and reloads the panel - Panel sessions survive restarts (hashed tokens in data/panel_sessions.json) - Docker image is based on the .NET SDK so it can build updates - Version 2.9.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
7.4 KiB
C#
175 lines
7.4 KiB
C#
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>": 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 <command> (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 <befehl>" 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 <befehl>" (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();
|
|
}
|