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
+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();
}
}
}