From 8bac7d194ab4e857221f14fa5b260b56c70dd5fb Mon Sep 17 00:00:00 2001 From: MrMaho Date: Fri, 23 Oct 2026 20:26:52 +0200 Subject: [PATCH] 2.10.11: security levels (no joining servers/hosts below the required level, OUTDATED in the browser), announcements and maintenance notice from the keymaster in the MSCO menu Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++ LobbyUI.cs | 57 +++++++++++++++++- MSCO.cs | 15 ++++- MultiplayerManager.cs | 35 +++++++++++ ServerBrowser.cs | 17 ++++++ SiteStatus.cs | 134 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 SiteStatus.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index eea9fe9..bfbcd5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.10.11 + +- **Sicherheitsstufe:** Server und P2P-Hosts, denen MSCO-Sicherheitsupdates fehlen (Server älter als 2.10.6), kann man + nicht mehr betreten – im Server-Browser stehen sie als **OUTDATED**, statt JOIN steht dort „UPDATE REQUIRED“, und + wer trotzdem verbindet, bekommt den Grund angezeigt. Server-Betreiber: im Web-Panel auf *Update* klicken +- **Ankündigungen & Wartung:** Oben im MSCO-Menü stehen Ankündigungen des MSCO-Teams und ein Hinweis, wenn MSCO gerade + gewartet wird (mit × ausblendbar). Neue Ankündigungen während des Spielens erscheinen kurz am Bildschirmrand und + in der Konsole (F7) + ## 2.10.10 - **Mods im Singleplayer:** Startet der Launcher mit SINGLEPLAYER, lädt MSCO nur noch die eigenen Mods diff --git a/LobbyUI.cs b/LobbyUI.cs index b53e0a6..9e8c882 100644 --- a/LobbyUI.cs +++ b/LobbyUI.cs @@ -72,7 +72,12 @@ namespace MSCO string layoutError; GUIStyle dangerStyle; GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle; - GUIStyle featuredRowBox, pillFeaturedStyle; + GUIStyle featuredRowBox, pillFeaturedStyle, pillDangerStyle, newsTitleStyle; + // Ankündigungen/Wartung vom Keymaster - nur im Layout-Durchgang übernehmen (Liste kann sich im Hintergrund ändern) + List shownNews; + bool shownMaintenance; + string shownMaintenanceText; + readonly HashSet dismissedNews = new HashSet(); GUIStyle betaPillStyle, betaNoteStyle; GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle, fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle, @@ -141,6 +146,7 @@ namespace MSCO GUI.skin.verticalScrollbar = scrollbarStyle; GUI.skin.verticalScrollbarThumb = scrollThumbStyle; GUILayout.BeginArea(content); + if (!versionWarning && !pendingJoin && !mp.IsInSession) DrawAnnouncements(); if (versionWarning) DrawVersionWarning(mp); else if (pendingJoin) DrawJoinConfirm(mp); else if (mp.IsInSession) DrawSession(mp); @@ -358,6 +364,7 @@ namespace MSCO // Server auf diesem PC (localhost) nameX += Pill(nameX, row, "LOCAL", pillStyle); } + if (s.Outdated) nameX += Pill(nameX, row, "OUTDATED", pillDangerStyle); // fehlende Sicherheitsupdates if (!string.IsNullOrEmpty(s.Version) && !MSCO.Compatible(s.Version)) { nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version @@ -408,7 +415,43 @@ namespace MSCO static bool CanJoin(MultiplayerManager mp, ServerListEntry s) { - return s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable); + return !s.Outdated && s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable); + } + + /// + /// Oben im MSCO-Menü: Wartungshinweis und Ankündigungen der MSCO-Admins (Keymaster → Wartung & News). + /// Ankündigungen lassen sich mit × für diese Sitzung ausblenden. + /// + void DrawAnnouncements() + { + if (shownNews == null || Event.current.type == EventType.Layout) + { + shownNews = SiteStatus.Announcements.FindAll(a => !dismissedNews.Contains(a.Id)); + if (shownNews.Count > 3) shownNews.RemoveRange(3, shownNews.Count - 3); + shownMaintenance = SiteStatus.Maintenance; + shownMaintenanceText = SiteStatus.MaintenanceMessage; + } + if (shownMaintenance) + NewsBox("MAINTENANCE", pillFeaturedStyle, "MSCO is in maintenance right now", + string.IsNullOrEmpty(shownMaintenanceText) ? "Some things may not work for a while." : shownMaintenanceText, null); + foreach (Announcement a in shownNews) + NewsBox(a.Level == "critical" ? "IMPORTANT" : a.Level == "warning" ? "NOTICE" : "INFO", + a.Level == "critical" ? pillDangerStyle : a.Level == "warning" ? pillFeaturedStyle : pillMutedStyle, a.Title, a.Text, a.Id); + if (shownMaintenance || shownNews.Count > 0) GUILayout.Space(S(10)); + } + + void NewsBox(string label, GUIStyle pill, string title, string text, string id) + { + GUILayout.BeginVertical(rowBox); + GUILayout.BeginHorizontal(); + GUILayout.Label(label, pill, GUILayout.Height(S(20)), GUILayout.ExpandWidth(false)); + GUILayout.Space(S(8)); + GUILayout.Label(title, newsTitleStyle, GUILayout.ExpandWidth(true)); + if (id != null && GUILayout.Button("×", tabStyle, GUILayout.Width(S(28)), GUILayout.Height(S(22)))) dismissedNews.Add(id); + GUILayout.EndHorizontal(); + if (!string.IsNullOrEmpty(text)) GUILayout.Label(text, textStyle); + GUILayout.EndVertical(); + GUILayout.Space(S(6)); } void DrawNameField(MultiplayerManager mp, float width) @@ -613,6 +656,7 @@ namespace MSCO if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle); bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle); if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle); + if (s.Outdated) bx += Pill(bx, badgeRow, "OUTDATED", pillDangerStyle); if (!MSCO.Compatible(s.Version)) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle); if (s.Tags != null) foreach (string tag in s.Tags) @@ -625,10 +669,13 @@ namespace MSCO Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s); GUILayout.Space(S(8)); GUI.enabled = CanJoin(mp, s); - if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(S(44)), GUILayout.Width(S(180)))) + if (GUILayout.Button(s.Outdated ? "UPDATE REQUIRED" : s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(S(44)), GUILayout.Width(S(180)))) mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers); GUI.enabled = true; GUILayout.EndHorizontal(); + if (s.Outdated) + GUILayout.Label("This server is missing MSCO security updates" + (string.IsNullOrEmpty(s.ServerVersion) ? "" : " (server " + s.ServerVersion + ")") + + " - you can join again once its owner has updated it.", textStyle); if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle); GUILayout.Space(S(10)); @@ -1118,6 +1165,10 @@ namespace MSCO pillFeaturedStyle = new GUIStyle(pillStyle); pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4)); pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06); + pillDangerStyle = new GUIStyle(pillStyle); + pillDangerStyle.normal.background = UiTheme.Rounded(new Color(UiTheme.Danger.r, UiTheme.Danger.g, UiTheme.Danger.b, 0.25f), P(4)); + pillDangerStyle.normal.textColor = UiTheme.DangerHover; + newsTitleStyle = new GUIStyle(rowNameStyle) { wordWrap = true }; featuredRowBox = UiTheme.Card(Color.Lerp(Field, Gold, 0.14f), P(10)); chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.2f), new Color(Accent.r, Accent.g, Accent.b, 0.3f), AccentHover, P(11), FontStyle.Bold, P(11), new Color(Accent.r, Accent.g, Accent.b, 0.55f)); diff --git a/MSCO.cs b/MSCO.cs index 8b28c24..848acd9 100644 --- a/MSCO.cs +++ b/MSCO.cs @@ -11,13 +11,26 @@ namespace MSCO /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// - public const string ModVersion = "2.10.10"; + public const string ModVersion = "2.10.11"; /// /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// auch GameServer.ModVersion im Server. /// public const string ProtocolVersion = "2.8"; + /// + /// Sicherheitsstufe dieser Mod-Version - steigt mit jedem Sicherheitsupdate (gleiche Zählung wie + /// GameServer.SecurityLevel im Server). Als P2P-Host meldet die Mod sie den Mitspielern. + /// 1 = Oktober 2026. + /// + public const int SecurityLevel = 1; + + /// + /// Server und P2P-Hosts unter dieser Stufe fehlen Sicherheitsupdates: die Mod tritt ihnen nicht bei. Die + /// MSCO-Admins können das Minimum im Keymaster noch anheben (SiteStatus.RequiredServerSecurity). + /// + public const int MinServerSecurity = 1; + /// Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen wird die Netzwerk-Version. public static bool Compatible(string other) { diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 3ad9523..bac2cbd 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -71,6 +71,10 @@ namespace MSCO const float NeedsInterval = 10f; void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); } + + const string OutdatedServerText = "This server is outdated - it is missing MSCO security updates. You can join again once its owner has updated it (web panel -> Update)."; + const string OutdatedHostText = "The host uses an outdated MSCO version that is missing security updates. Ask them to update " + + "MSCO (start the game once through the MSCO Launcher), then join again."; void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); } readonly Overlays overlays = new Overlays(); bool isAdmin; // Server hat uns als Admin gemeldet @@ -108,6 +112,13 @@ namespace MSCO { Application.runInBackground = true; playerName = PlayerPrefs.GetString(PrefName, System.Environment.UserName); + // Neue Ankündigung der MSCO-Admins, während man spielt: kurz einblenden und in die Konsole (F7) + SiteStatus.NewAnnouncement += a => + { + toasts.Add(a.Title, a.Level == "critical" ? "Important announcement" : "MSCO announcement", + a.Level == "critical" ? UiTheme.Danger : a.Level == "warning" ? UiTheme.Gold : UiTheme.Accent); + MscoLog.Info("[MSCO] Announcement: " + a.Title + (string.IsNullOrEmpty(a.Text) ? "" : " - " + a.Text)); + }; objects.Send = SendGame; // Einkäufe / verbrauchte Items bei uns: allen melden objects.ItemAppeared = b => @@ -755,6 +766,13 @@ namespace MSCO } string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName; int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers; + // Fehlen dem Server Sicherheitsupdates, nicht beitreten (auch nicht nach Rückfrage) + if (info.Security < SiteStatus.RequiredServerSecurity) + { + DisconnectNotice = OutdatedServerText; + SetStatus("Server " + name + " is outdated (security level " + info.Security + ", required " + SiteStatus.RequiredServerSecurity + ")"); + yield break; + } if (!MSCO.Compatible(info.Version)) { @@ -1045,6 +1063,7 @@ namespace MSCO void Update() { CheckHitch(); + SiteStatus.Tick(); // Ankündigungen, Wartung, Mindest-Sicherheitsstufe vom Keymaster CheckJoinRequest(); // JOIN im Server-Browser (Launcher/Website): ohne Rückfrage verbinden, sobald das Hauptmenü steht. // Server-Mods fragen weiterhin extra nach (das sind Programme). @@ -1319,6 +1338,7 @@ namespace MSCO if (r.Contains("kick")) return "KICKED"; if (r.Contains("shutting down") || r.Contains("server stopped") || r.Contains("shut down")) return "SERVER CLOSED"; if (r.Contains("could not reconnect")) return "RECONNECT FAILED"; + if (r.Contains("outdated")) return "UPDATE REQUIRED"; // Server/Host ohne Sicherheitsupdates return "DISCONNECTED"; } } @@ -1509,6 +1529,9 @@ namespace MSCO w.Write(false); // kein dedizierter Server w.Write(list.Count); foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); } + // Unsere Sicherheitsstufe (aktuelle Mods treten veralteten Hosts nicht bei) + w.Write(MSCO.SecurityLevel); + w.Write(MSCO.ModVersion); })); session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId); ToastJoined(name); @@ -1548,6 +1571,18 @@ namespace MSCO int id = r.ReadInt32(); AddPlayer(id, r.ReadString(), id == localId); } + // Sicherheitsstufe des Servers/Hosts (ältere schicken sie nicht = 0): darunter gleich wieder gehen + int level = r.BaseStream.Position < r.BaseStream.Length ? r.ReadInt32() : 0; + if (level < SiteStatus.RequiredServerSecurity) + { + MscoLog.Warning("[MSCO] " + (dedicated ? "Server" : "Host") + " has security level " + level + ", required " + + SiteStatus.RequiredServerSecurity + " - leaving"); + serverReason = dedicated ? OutdatedServerText : OutdatedHostText; + reconnectMessage = null; // nicht immer wieder versuchen + session.Close("Outdated " + (dedicated ? "server" : "host")); + OnSessionEnded(session.CloseReason); + return; + } handshakeDone = true; reconnectMessage = null; // (wieder) verbunden reconnectAttempts = 0; diff --git a/ServerBrowser.cs b/ServerBrowser.cs index 57f1c88..e51a8b5 100644 --- a/ServerBrowser.cs +++ b/ServerBrowser.cs @@ -20,6 +20,10 @@ namespace MSCO public int Players; public int MaxPlayers; public string Version; + public string ServerVersion = ""; // Programmversion des Servers (z.B. 2.10.6), ältere melden sie nicht + public int Security; // Sicherheitsstufe des Servers (0 = alt, ohne Sicherheitsupdates) + /// Fehlen dem Server Sicherheitsupdates? Dann tritt die Mod nicht bei. + public bool Outdated { get { return Security < SiteStatus.RequiredServerSecurity; } } public bool RequireSteam = true; // false = Non-Steam-Server public string Owner; // Besitzer des Lizenzschlüssels public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben) @@ -135,6 +139,8 @@ namespace MSCO Players = MiniJson.Int(o, "players"), MaxPlayers = MiniJson.Int(o, "maxPlayers"), Version = MiniJson.Str(o, "version", ""), + ServerVersion = MiniJson.Str(o, "serverVersion", ""), + Security = MiniJson.Int(o, "security"), RequireSteam = MiniJson.Bool(o, "requireSteam", true), Owner = MiniJson.Str(o, "owner"), Featured = MiniJson.Bool(o, "featured"), @@ -192,6 +198,9 @@ namespace MSCO entry.MaxPlayers = info.MaxPlayers; entry.Version = info.Version; entry.Ping = info.Ping; + // Direkt vom Server - gilt auch für lokale/direkt eingegebene Server ohne Keymaster + entry.Security = info.Security; + if (!string.IsNullOrEmpty(info.ServerVersion)) entry.ServerVersion = info.ServerVersion; if (info.HasProfile) { entry.RequireSteam = info.RequireSteam; @@ -218,6 +227,9 @@ namespace MSCO public string Owner; public int UptimeSeconds; public List PlayerNames; + // Ab Server 2.10.6: Programmversion und Sicherheitsstufe (ältere Server: "" und 0) + public string ServerVersion; + public int Security; } /// Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen). @@ -261,6 +273,11 @@ namespace MSCO int count = r.ReadInt32(); info.PlayerNames = new List(); for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString()); + if (r.BaseStream.Position < r.BaseStream.Length) + { + info.ServerVersion = r.ReadString(); + info.Security = r.ReadInt32(); + } } } return true; diff --git a/SiteStatus.cs b/SiteStatus.cs new file mode 100644 index 0000000..216b574 --- /dev/null +++ b/SiteStatus.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace MSCO +{ + /// Eine Ankündigung der MSCO-Admins (Keymaster → Wartung & News, Ziel "Spiel"). + public class Announcement + { + public string Id, Title, Text, Level; // Level: info, warning, critical + } + + /// + /// Status vom Keymaster (/api/status?for=game): Wartungsmodus, Ankündigungen und die Mindest-Sicherheitsstufe der + /// Server. Wird im Hintergrund geladen (WinHTTP, blockiert das Spiel nicht) und alle paar Minuten aufgefrischt. + /// Ohne Verbindung gilt das Eingebaute (MSCO.MinServerSecurity), Ankündigungen fehlen dann einfach. + /// + public static class SiteStatus + { + const float RefreshSeconds = 300f; + + static readonly object gate = new object(); + static List announcements = new List(); + static bool maintenance; + static string maintenanceMessage = ""; + static int minServerLevel; // vom Keymaster (0 = unbekannt) + static float nextFetch; + static bool fetching; + + /// Aktuelle Ankündigungen (wichtigste zuerst). + public static List Announcements { get { lock (gate) return new List(announcements); } } + public static bool Maintenance { get { lock (gate) return maintenance; } } + public static string MaintenanceMessage { get { lock (gate) return maintenanceMessage; } } + + /// + /// Sicherheitsstufe, die Server und P2P-Hosts mindestens haben müssen: das Eingebaute oder - wenn höher - was die + /// MSCO-Admins im Keymaster eingestellt haben. + /// + public static int RequiredServerSecurity { get { lock (gate) return Math.Max(MSCO.MinServerSecurity, minServerLevel); } } + + /// Neue Ankündigung seit dem letzten Abruf (für einen Hinweis im Spiel). + public static event Action NewAnnouncement; + + static readonly HashSet seen = new HashSet(); + static readonly List fresh = new List(); // neu, noch nicht gemeldet (Hauptthread) + static bool firstLoad = true; + + /// Jedes Frame aufrufen (MultiplayerManager): lädt beim Start und dann alle 5 Minuten neu. + public static void Tick() + { + Action handler = NewAnnouncement; + List report = null; + lock (gate) + if (fresh.Count > 0) { report = new List(fresh); fresh.Clear(); } + if (report != null && handler != null) + foreach (Announcement a in report) + try { handler(a); } catch { } + if (fetching || Time.realtimeSinceStartup < nextFetch) return; + Refresh(); + } + + /// Sofort neu laden (z.B. beim Öffnen des Server-Browsers). + public static void Refresh() + { + if (fetching) return; + fetching = true; + nextFetch = Time.realtimeSinceStartup + RefreshSeconds; + var thread = new Thread(() => + { + try + { + string error; + string text = WinHttp.Get(ServerBrowser.MasterUrl + "/api/status?for=game", 8000, out error); + if (text != null) Apply(text); + } + catch { } + finally { fetching = false; } + }); + thread.IsBackground = true; + thread.Start(); + } + + static void Apply(string text) + { + var root = MiniJson.Parse(text) as Dictionary; + if (root == null) return; + object v; + var m = root.TryGetValue("maintenance", out v) ? v as Dictionary : null; + var sec = root.TryGetValue("security", out v) ? v as Dictionary : null; + var list = new List(); + var items = root.TryGetValue("announcements", out v) ? v as List : null; + if (items != null) + foreach (object item in items) + { + var a = item as Dictionary; + if (a == null) continue; + list.Add(new Announcement + { + Id = MiniJson.Str(a, "id", ""), + Title = Clip(MiniJson.Str(a, "title", ""), 120), + Text = Clip(MiniJson.Str(a, "text", ""), 600), + Level = MiniJson.Str(a, "level", "info"), + }); + if (list.Count >= 5) break; + } + lock (gate) + { + announcements = list; + maintenance = m != null && MiniJson.Bool(m, "active"); + maintenanceMessage = m != null ? Clip(MiniJson.Str(m, "message", ""), 500) : ""; + if (sec != null) minServerLevel = Math.Max(0, Math.Min(1000, MiniJson.Int(sec, "minServerLevel"))); + foreach (Announcement a in list) + if (seen.Add(a.Id) && !firstLoad) fresh.Add(a); + firstLoad = false; + } + } + + /// Kein Steuerzeichen/Rich-Text aus dem Netz in die Oberfläche (Unity-GUI wertet <color> usw. aus). + static string Clip(string s, int max) + { + if (string.IsNullOrEmpty(s)) return ""; + var sb = new System.Text.StringBuilder(Math.Min(s.Length, max)); + foreach (char c in s) + { + if (sb.Length >= max) break; + if (c == '<') sb.Append('‹'); + else if (c == '>') sb.Append('›'); + else if (c == '\n' || !char.IsControl(c)) sb.Append(c); + } + return sb.ToString(); + } + } +}