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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:26:52 +02:00
co-authored by Claude Opus 5.5
parent ceaa2e29aa
commit 8bac7d194a
6 changed files with 263 additions and 4 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # 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 ## 2.10.10
- **Mods im Singleplayer:** Startet der Launcher mit SINGLEPLAYER, lädt MSCO nur noch die eigenen Mods - **Mods im Singleplayer:** Startet der Launcher mit SINGLEPLAYER, lädt MSCO nur noch die eigenen Mods
+54 -3
View File
@@ -72,7 +72,12 @@ namespace MSCO
string layoutError; string layoutError;
GUIStyle dangerStyle; GUIStyle dangerStyle;
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle; 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<Announcement> shownNews;
bool shownMaintenance;
string shownMaintenanceText;
readonly HashSet<string> dismissedNews = new HashSet<string>();
GUIStyle betaPillStyle, betaNoteStyle; GUIStyle betaPillStyle, betaNoteStyle;
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle, GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle, fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
@@ -141,6 +146,7 @@ namespace MSCO
GUI.skin.verticalScrollbar = scrollbarStyle; GUI.skin.verticalScrollbar = scrollbarStyle;
GUI.skin.verticalScrollbarThumb = scrollThumbStyle; GUI.skin.verticalScrollbarThumb = scrollThumbStyle;
GUILayout.BeginArea(content); GUILayout.BeginArea(content);
if (!versionWarning && !pendingJoin && !mp.IsInSession) DrawAnnouncements();
if (versionWarning) DrawVersionWarning(mp); if (versionWarning) DrawVersionWarning(mp);
else if (pendingJoin) DrawJoinConfirm(mp); else if (pendingJoin) DrawJoinConfirm(mp);
else if (mp.IsInSession) DrawSession(mp); else if (mp.IsInSession) DrawSession(mp);
@@ -358,6 +364,7 @@ namespace MSCO
// Server auf diesem PC (localhost) // Server auf diesem PC (localhost)
nameX += Pill(nameX, row, "LOCAL", pillStyle); 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)) if (!string.IsNullOrEmpty(s.Version) && !MSCO.Compatible(s.Version))
{ {
nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version
@@ -408,7 +415,43 @@ namespace MSCO
static bool CanJoin(MultiplayerManager mp, ServerListEntry s) 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);
}
/// <summary>
/// Oben im MSCO-Menü: Wartungshinweis und Ankündigungen der MSCO-Admins (Keymaster → Wartung &amp; News).
/// Ankündigungen lassen sich mit × für diese Sitzung ausblenden.
/// </summary>
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) void DrawNameField(MultiplayerManager mp, float width)
@@ -613,6 +656,7 @@ namespace MSCO
if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle); if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle);
bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle); bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle); 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 (!MSCO.Compatible(s.Version)) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle);
if (s.Tags != null) if (s.Tags != null)
foreach (string tag in s.Tags) 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); Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s);
GUILayout.Space(S(8)); GUILayout.Space(S(8));
GUI.enabled = CanJoin(mp, s); 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); mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
GUI.enabled = true; GUI.enabled = true;
GUILayout.EndHorizontal(); 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); if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
GUILayout.Space(S(10)); GUILayout.Space(S(10));
@@ -1118,6 +1165,10 @@ namespace MSCO
pillFeaturedStyle = new GUIStyle(pillStyle); pillFeaturedStyle = new GUIStyle(pillStyle);
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4)); pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06); 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)); 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)); 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));
+14 -1
View File
@@ -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 /// 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. /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary> /// </summary>
public const string ModVersion = "2.10.10"; public const string ModVersion = "2.10.11";
/// <summary> /// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server. /// auch GameServer.ModVersion im Server.
/// </summary> /// </summary>
public const string ProtocolVersion = "2.8"; public const string ProtocolVersion = "2.8";
/// <summary>
/// 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.
/// </summary>
public const int SecurityLevel = 1;
/// <summary>
/// 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).
/// </summary>
public const int MinServerSecurity = 1;
/// <summary>Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen wird die Netzwerk-Version.</summary> /// <summary>Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen wird die Netzwerk-Version.</summary>
public static bool Compatible(string other) public static bool Compatible(string other)
{ {
+35
View File
@@ -71,6 +71,10 @@ namespace MSCO
const float NeedsInterval = 10f; const float NeedsInterval = 10f;
void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); } 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); } void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); }
readonly Overlays overlays = new Overlays(); readonly Overlays overlays = new Overlays();
bool isAdmin; // Server hat uns als Admin gemeldet bool isAdmin; // Server hat uns als Admin gemeldet
@@ -108,6 +112,13 @@ namespace MSCO
{ {
Application.runInBackground = true; Application.runInBackground = true;
playerName = PlayerPrefs.GetString(PrefName, System.Environment.UserName); 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; objects.Send = SendGame;
// Einkäufe / verbrauchte Items bei uns: allen melden // Einkäufe / verbrauchte Items bei uns: allen melden
objects.ItemAppeared = b => objects.ItemAppeared = b =>
@@ -755,6 +766,13 @@ namespace MSCO
} }
string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName; string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName;
int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers; 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)) if (!MSCO.Compatible(info.Version))
{ {
@@ -1045,6 +1063,7 @@ namespace MSCO
void Update() void Update()
{ {
CheckHitch(); CheckHitch();
SiteStatus.Tick(); // Ankündigungen, Wartung, Mindest-Sicherheitsstufe vom Keymaster
CheckJoinRequest(); CheckJoinRequest();
// JOIN im Server-Browser (Launcher/Website): ohne Rückfrage verbinden, sobald das Hauptmenü steht. // JOIN im Server-Browser (Launcher/Website): ohne Rückfrage verbinden, sobald das Hauptmenü steht.
// Server-Mods fragen weiterhin extra nach (das sind Programme). // Server-Mods fragen weiterhin extra nach (das sind Programme).
@@ -1319,6 +1338,7 @@ namespace MSCO
if (r.Contains("kick")) return "KICKED"; 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("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("could not reconnect")) return "RECONNECT FAILED";
if (r.Contains("outdated")) return "UPDATE REQUIRED"; // Server/Host ohne Sicherheitsupdates
return "DISCONNECTED"; return "DISCONNECTED";
} }
} }
@@ -1509,6 +1529,9 @@ namespace MSCO
w.Write(false); // kein dedizierter Server w.Write(false); // kein dedizierter Server
w.Write(list.Count); w.Write(list.Count);
foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); } 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); session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId);
ToastJoined(name); ToastJoined(name);
@@ -1548,6 +1571,18 @@ namespace MSCO
int id = r.ReadInt32(); int id = r.ReadInt32();
AddPlayer(id, r.ReadString(), id == localId); 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; handshakeDone = true;
reconnectMessage = null; // (wieder) verbunden reconnectMessage = null; // (wieder) verbunden
reconnectAttempts = 0; reconnectAttempts = 0;
+17
View File
@@ -20,6 +20,10 @@ namespace MSCO
public int Players; public int Players;
public int MaxPlayers; public int MaxPlayers;
public string Version; 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)
/// <summary>Fehlen dem Server Sicherheitsupdates? Dann tritt die Mod nicht bei.</summary>
public bool Outdated { get { return Security < SiteStatus.RequiredServerSecurity; } }
public bool RequireSteam = true; // false = Non-Steam-Server public bool RequireSteam = true; // false = Non-Steam-Server
public string Owner; // Besitzer des Lizenzschlüssels public string Owner; // Besitzer des Lizenzschlüssels
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben) public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
@@ -135,6 +139,8 @@ namespace MSCO
Players = MiniJson.Int(o, "players"), Players = MiniJson.Int(o, "players"),
MaxPlayers = MiniJson.Int(o, "maxPlayers"), MaxPlayers = MiniJson.Int(o, "maxPlayers"),
Version = MiniJson.Str(o, "version", ""), Version = MiniJson.Str(o, "version", ""),
ServerVersion = MiniJson.Str(o, "serverVersion", ""),
Security = MiniJson.Int(o, "security"),
RequireSteam = MiniJson.Bool(o, "requireSteam", true), RequireSteam = MiniJson.Bool(o, "requireSteam", true),
Owner = MiniJson.Str(o, "owner"), Owner = MiniJson.Str(o, "owner"),
Featured = MiniJson.Bool(o, "featured"), Featured = MiniJson.Bool(o, "featured"),
@@ -192,6 +198,9 @@ namespace MSCO
entry.MaxPlayers = info.MaxPlayers; entry.MaxPlayers = info.MaxPlayers;
entry.Version = info.Version; entry.Version = info.Version;
entry.Ping = info.Ping; 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) if (info.HasProfile)
{ {
entry.RequireSteam = info.RequireSteam; entry.RequireSteam = info.RequireSteam;
@@ -218,6 +227,9 @@ namespace MSCO
public string Owner; public string Owner;
public int UptimeSeconds; public int UptimeSeconds;
public List<string> PlayerNames; public List<string> PlayerNames;
// Ab Server 2.10.6: Programmversion und Sicherheitsstufe (ältere Server: "" und 0)
public string ServerVersion;
public int Security;
} }
/// <summary>Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen).</summary> /// <summary>Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen).</summary>
@@ -261,6 +273,11 @@ namespace MSCO
int count = r.ReadInt32(); int count = r.ReadInt32();
info.PlayerNames = new List<string>(); info.PlayerNames = new List<string>();
for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString()); 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; return true;
+134
View File
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace MSCO
{
/// <summary>Eine Ankündigung der MSCO-Admins (Keymaster → Wartung &amp; News, Ziel "Spiel").</summary>
public class Announcement
{
public string Id, Title, Text, Level; // Level: info, warning, critical
}
/// <summary>
/// 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.
/// </summary>
public static class SiteStatus
{
const float RefreshSeconds = 300f;
static readonly object gate = new object();
static List<Announcement> announcements = new List<Announcement>();
static bool maintenance;
static string maintenanceMessage = "";
static int minServerLevel; // vom Keymaster (0 = unbekannt)
static float nextFetch;
static bool fetching;
/// <summary>Aktuelle Ankündigungen (wichtigste zuerst).</summary>
public static List<Announcement> Announcements { get { lock (gate) return new List<Announcement>(announcements); } }
public static bool Maintenance { get { lock (gate) return maintenance; } }
public static string MaintenanceMessage { get { lock (gate) return maintenanceMessage; } }
/// <summary>
/// Sicherheitsstufe, die Server und P2P-Hosts mindestens haben müssen: das Eingebaute oder - wenn höher - was die
/// MSCO-Admins im Keymaster eingestellt haben.
/// </summary>
public static int RequiredServerSecurity { get { lock (gate) return Math.Max(MSCO.MinServerSecurity, minServerLevel); } }
/// <summary>Neue Ankündigung seit dem letzten Abruf (für einen Hinweis im Spiel).</summary>
public static event Action<Announcement> NewAnnouncement;
static readonly HashSet<string> seen = new HashSet<string>();
static readonly List<Announcement> fresh = new List<Announcement>(); // neu, noch nicht gemeldet (Hauptthread)
static bool firstLoad = true;
/// <summary>Jedes Frame aufrufen (MultiplayerManager): lädt beim Start und dann alle 5 Minuten neu.</summary>
public static void Tick()
{
Action<Announcement> handler = NewAnnouncement;
List<Announcement> report = null;
lock (gate)
if (fresh.Count > 0) { report = new List<Announcement>(fresh); fresh.Clear(); }
if (report != null && handler != null)
foreach (Announcement a in report)
try { handler(a); } catch { }
if (fetching || Time.realtimeSinceStartup < nextFetch) return;
Refresh();
}
/// <summary>Sofort neu laden (z.B. beim Öffnen des Server-Browsers).</summary>
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<string, object>;
if (root == null) return;
object v;
var m = root.TryGetValue("maintenance", out v) ? v as Dictionary<string, object> : null;
var sec = root.TryGetValue("security", out v) ? v as Dictionary<string, object> : null;
var list = new List<Announcement>();
var items = root.TryGetValue("announcements", out v) ? v as List<object> : null;
if (items != null)
foreach (object item in items)
{
var a = item as Dictionary<string, object>;
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;
}
}
/// <summary>Kein Steuerzeichen/Rich-Text aus dem Netz in die Oberfläche (Unity-GUI wertet &lt;color&gt; usw. aus).</summary>
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();
}
}
}