using System.Text.Json; namespace MSCO.Server; /// server.json - wird beim ersten Start mit Standardwerten angelegt. public sealed class ServerConfig { public string LicenseKey { get; set; } = ""; public string Name { get; set; } = "My MSCO Server"; public string Description { get; set; } = ""; /// /// Eigene Tags für die Serverliste (z.B. "deutsch", "roleplay"), höchstens 6. "official" und "featured" vergeben /// nur die MSCO-Admins im Keymaster. /// public List Tags { get; set; } = new(); public int Port { get; set; } = 7777; /// /// Adresse, unter der Spieler den Server erreichen (IP oder Domain). Leer = die IP, von der /// sich der Server beim Master meldet. Nötig z.B. wenn Master und Server im selben Docker-Netz laufen. /// public string PublicAddress { get; set; } = ""; public int MaxPlayers { get; set; } = 8; /// In der Serverliste anzeigen. public bool Public { get; set; } = true; /// Nur Spieler mit Steam zulassen (false = auch ohne Steam, "Non-Steam"-Server). public bool RequireSteam { get; set; } = true; /// Weltzustand so oft auf die Festplatte schreiben. public int AutosaveSeconds { get; set; } = 60; /// /// So oft (Minuten) lässt sich der Server von einem Spieler im Spiel den aktuellen Spielstand hochladen /// (Schrauben, Teile, alles wie im Spiel gespeichert). Speichern lässt das Spiel dieses Spielers einige Sekunden /// hängen - deshalb nicht zu oft. 0 = nur beim Beitreten. /// public int SaveSyncMinutes { get; set; } = 30; /// /// Beim Beitreten nur dann frisch speichern lassen, wenn der letzte Spielstand älter ist (Minuten) - sonst bekommt der /// Neue den gespeicherten Stand und die seitdem passierten Änderungen. 0 = bei jedem Beitritt frisch speichern. /// public int JoinSaveMaxAgeMinutes { get; set; } = 15; /// Version der Einstellungen (für einmalige Anpassungen alter server.json). public int ConfigVersion { get; set; } = 0; public const int CurrentConfigVersion = 2; /// Optional: Steam Web API Key, um Steam-Tickets der Spieler bei Steam zu prüfen. public string SteamWebApiKey { get; set; } = ""; /// Gesperrte SteamID64s. public List BannedSteamIds { get; set; } = new(); /// Admins (SteamID64): dürfen im Spiel mit /admin Spieler kicken und bannen. public List Admins { get; set; } = new(); // ---------------------------------------------------------------- Web-Panel /// Web-Panel einschalten. public bool PanelEnabled { get; set; } = true; /// /// Verwalten aus dem MSCO Launcher: wer den Server dort mit dem Lizenzschlüssel verknüpft hat, kann über den Keymaster /// Befehle schicken (say, kick, ban, save, backup, restart, mods reload, status, players). false = aus. /// public bool RemoteManagement { get; set; } = true; /// 0 = derselbe Port wie der Gameserver (Browser und Spiel werden automatisch unterschieden), sonst eigener Port. public int PanelPort { get; set; } = 0; /// Nur bei eigenem PanelPort: Adresse, auf der gelauscht wird ("0.0.0.0" = alle, "127.0.0.1" = nur lokal, z.B. hinter einem Proxy). public string PanelBindAddress { get; set; } = "0.0.0.0"; /// Hinter einem Reverse-Proxy: X-Forwarded-For/-Proto/-Prefix übernehmen (echte IP, HTTPS, Unterpfad). public bool PanelTrustProxy { get; set; } = false; /// Unterpfad, unter dem das Panel läuft, z.B. "/panel" (leer = direkt unter /). public string PanelBasePath { get; set; } = ""; /// /// HTTPS im Panel. Auf dem Spiel-Port immer verfügbar (https://<server>:<port>/); true = Browser, die von /// außen per HTTP kommen, werden auf HTTPS umgeleitet (Passwörter nie unverschlüsselt). Bei eigenem PanelPort spricht /// dieser nur mit eigenem Zertifikat (PanelCertificatePath) HTTPS. Hinter einem Reverse-Proxy (PanelTrustProxy) macht /// der Proxy HTTPS. /// public bool PanelHttps { get; set; } = true; /// Eigenes Zertifikat (PFX, Pfad absolut oder relativ zu data/) - leer = selbst signiertes in data/panel_cert.pfx. public string PanelCertificatePath { get; set; } = ""; public string PanelCertificatePassword { get; set; } = ""; static readonly JsonSerializerOptions Options = new() { WriteIndented = true }; public static ServerConfig LoadOrCreate(string path, out bool created) { created = !File.Exists(path); if (created) { var fresh = new ServerConfig { ConfigVersion = CurrentConfigVersion }; fresh.Save(path); return fresh; } var config = JsonSerializer.Deserialize(File.ReadAllText(path), Options) ?? new ServerConfig(); if (config.ConfigVersion < 2) { // Früherer Standard: alle 10 Minuten speichern lassen - das ließ einen Spieler alle 10 Minuten einige Sekunden // hängen. Wer den Standard nie geändert hat, bekommt den neuen (30). if (config.SaveSyncMinutes == 10) config.SaveSyncMinutes = 30; config.ConfigVersion = CurrentConfigVersion; } // Neue Einstellungen (z.B. Admins) auch in ältere server.json schreiben, damit man sie sieht try { config.Save(path); } catch (Exception) { } // z.B. schreibgeschützt - dann eben nicht return config; } public void Save(string path) { File.WriteAllText(path, JsonSerializer.Serialize(this, Options)); PanelTls.Restrict(path); // enthält den Lizenzschlüssel und Passwörter (Linux: nur der Server-Benutzer) } /// /// Umgebungsvariablen überschreiben server.json (praktisch für Docker/systemd): /// MSCO_LICENSE_KEY, MSCO_NAME, MSCO_DESCRIPTION, MSCO_PORT, MSCO_MAX_PLAYERS, MSCO_PUBLIC, /// MSCO_PUBLIC_ADDRESS, MSCO_STEAM_WEB_API_KEY (die Master-Adresse ist fest, siehe MasterServer) /// /// Namen der gesetzten Umgebungsvariablen (fürs Log, damit klar ist, woher ein Wert kommt). /// Per Umgebungsvariable gesetzte Einstellungen (nicht in server.json gespeichert). [System.Text.Json.Serialization.JsonIgnore] public List EnvironmentOverrides { get; private set; } = new(); public List ApplyEnvironment() { var used = new List(); EnvironmentOverrides = used; string Take(string name, string current) { string value = Env(name); if (value == null) return current; used.Add(name); return value; } LicenseKey = Take("MSCO_LICENSE_KEY", LicenseKey); Name = Take("MSCO_NAME", Name); Description = Take("MSCO_DESCRIPTION", Description); PublicAddress = Take("MSCO_PUBLIC_ADDRESS", PublicAddress); if (Env("MSCO_TAGS") is string tags) { Tags = NormalizeTags(tags.Split(','), out _); used.Add("MSCO_TAGS"); } SteamWebApiKey = Take("MSCO_STEAM_WEB_API_KEY", SteamWebApiKey); if (int.TryParse(Env("MSCO_PORT"), out int port)) { Port = port; used.Add("MSCO_PORT"); } if (int.TryParse(Env("MSCO_MAX_PLAYERS"), out int max)) { MaxPlayers = max; used.Add("MSCO_MAX_PLAYERS"); } if (bool.TryParse(Env("MSCO_PUBLIC"), out bool isPublic)) { Public = isPublic; used.Add("MSCO_PUBLIC"); } if (bool.TryParse(Env("MSCO_REQUIRE_STEAM"), out bool requireSteam)) { RequireSteam = requireSteam; used.Add("MSCO_REQUIRE_STEAM"); } if (bool.TryParse(Env("MSCO_PANEL_ENABLED"), out bool panel)) { PanelEnabled = panel; used.Add("MSCO_PANEL_ENABLED"); } if (int.TryParse(Env("MSCO_PANEL_PORT"), out int panelPort)) { PanelPort = panelPort; used.Add("MSCO_PANEL_PORT"); } if (bool.TryParse(Env("MSCO_PANEL_TRUST_PROXY"), out bool trustProxy)) { PanelTrustProxy = trustProxy; used.Add("MSCO_PANEL_TRUST_PROXY"); } PanelBindAddress = Take("MSCO_PANEL_BIND", PanelBindAddress); PanelBasePath = Take("MSCO_PANEL_BASE_PATH", PanelBasePath); if (bool.TryParse(Env("MSCO_PANEL_HTTPS"), out bool panelHttps)) { PanelHttps = panelHttps; used.Add("MSCO_PANEL_HTTPS"); } PanelCertificatePath = Take("MSCO_PANEL_CERT", PanelCertificatePath); PanelCertificatePassword = Take("MSCO_PANEL_CERT_PASSWORD", PanelCertificatePassword); return used; } public const int MaxTags = 6; /// Diese Tags vergeben nur die MSCO-Admins (Keymaster), nicht der Server selbst. public static readonly string[] ReservedTags = { "official", "featured" }; /// Wie der Keymaster: "Deutsch, Role Play!" -> ["deutsch", "role-play"]; reservierte fallen weg. public static List NormalizeTags(IEnumerable input, out List reserved) { reserved = new List(); var tags = new List(); foreach (string raw in input ?? Array.Empty()) { string t = (raw ?? "").Trim().ToLowerInvariant().Replace("ä", "ae").Replace("ö", "oe").Replace("ü", "ue").Replace("ß", "ss"); t = System.Text.RegularExpressions.Regex.Replace(t, @"\s+", "-"); t = System.Text.RegularExpressions.Regex.Replace(t, "[^a-z0-9-]", "").Trim('-'); if (t.Length > 20) t = t[..20].Trim('-'); if (t.Length == 0 || tags.Contains(t)) continue; if (ReservedTags.Contains(t)) { if (!reserved.Contains(t)) reserved.Add(t); continue; } if (tags.Count < MaxTags) tags.Add(t); } return tags; } static string Env(string name) { string value = Environment.GetEnvironmentVariable(name); return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } }