diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f787c..b9ce25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d - **Verkehr**: NPC-Autos fahren bei allen an derselben Stelle (der erste Spieler simuliert, die anderen folgen) - `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden +**Admin: Welt verwalten (neuer Reiter WORLD im Admin-Menü)** +- **RESET WORLD**: Weltzustand zurücksetzen, alle verbinden automatisch neu +- **BACKUP NOW**: aktuellen Server-Spielstand sichern (Server-Befehl: `backup`) +- **Backups** mit Datum/Uhrzeit, **RESTORE** holt einen älteren Stand zurück (der aktuelle wird vorher gesichert) +- Jede Aktion mit Sicherheitsabfrage; nur für Admins (vom Server geprüft) + **Trennen & Neu-Verbinden** - Gekickt, gebannt, Server beendet, Verbindung verloren: eigenes Fenster in der Bildmitte mit **Grund** und OK (im Spiel wird der Grund vorher 2,5 s groß angezeigt) diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index d9d3ed2..61dc8d9 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -422,6 +422,18 @@ namespace MSCO return true; } + /// Admin (nur dedizierter Server): Welt zurücksetzen (5), Backup anlegen (6), Backup wiederherstellen (7). + public void WorldAction(byte action, string backup) + { + if (session == null || !dedicated) return; + session.SendTo(NetSession.HostId, MsgType.AdminAction, NetSession.Build(w => + { + w.Write(action); + w.Write(-1); + w.Write(backup ?? ""); + })); + } + /// Admin: Bann aufheben (Server: SteamID oder name:..., P2P: Name). public void Unban(string id) { @@ -1268,6 +1280,13 @@ namespace MSCO for (int i = 0; i < bans; i++) banned.Add(r.ReadString()); } overlays.SetBanned(banned); + var backups = new List(); + if (r.BaseStream.Length - r.BaseStream.Position >= 4) + { + int n = r.ReadInt32(); + for (int i = 0; i < n; i++) backups.Add(r.ReadString()); + } + overlays.SetBackups(backups); break; } diff --git a/NetSession.cs b/NetSession.cs index f6a1cec..5a2560c 100644 --- a/NetSession.cs +++ b/NetSession.cs @@ -34,7 +34,7 @@ namespace MSCO AdminStatus = 23, // Server -> Client: bool, ist Admin AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin] - AdminAction = 26, // Client -> Server: byte (1 = kick, 2 = ban), int Spieler-ID, string Grund + AdminAction = 26, // Client -> Server: byte (1 kick, 2 ban, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup), int Spieler-ID, string Grund/Name AdminResult = 27, // Server -> Client: string Meldung Chat = 28, // Chatnachricht (string) VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert] diff --git a/Overlays.cs b/Overlays.cs index 4950267..669c593 100644 --- a/Overlays.cs +++ b/Overlays.cs @@ -95,6 +95,9 @@ namespace MSCO public void SetAdminPlayers(List players) { adminPlayers = players; } public void SetBanned(List ids) { bannedIds = ids; } + public void SetBackups(List names) { backupNames = names; } + List backupNames; + string confirmWorld; // "reset" oder Name des Backups, das wiederhergestellt werden soll public void CloseAll() { @@ -288,7 +291,7 @@ namespace MSCO // Reiter GUILayout.BeginHorizontal(); - string[] tabs = { "PLAYERS", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : "") }; + string[] tabs = { "PLAYERS", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" }; for (int i = 0; i < tabs.Length; i++) { if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(130))) adminTab = i; @@ -299,6 +302,7 @@ namespace MSCO if (adminTab == 1) DrawAdminItems(mp); else if (adminTab == 2) DrawAdminBans(mp); + else if (adminTab == 3) DrawAdminWorld(mp); else DrawAdminPlayers(mp); GUILayout.EndArea(); } @@ -393,6 +397,65 @@ namespace MSCO GUILayout.EndScrollView(); } + /// Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server). + void DrawAdminWorld(MultiplayerManager mp) + { + if (!mp.IsDedicated) + { + GUILayout.Label("World reset and save backups are only available on a dedicated server.", small); + return; + } + GUILayout.Label("Everyone reconnects after a reset or restore. The current save is backed up first.", small); + GUILayout.Space(8); + + GUILayout.BeginHorizontal(); + if (confirmWorld == "reset") + { + if (GUILayout.Button("REALLY RESET?", danger, GUILayout.Height(32), GUILayout.Width(170))) { mp.WorldAction(5, null); confirmWorld = null; } + GUILayout.Space(6); + if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(100))) confirmWorld = null; + } + else if (GUILayout.Button("RESET WORLD", danger, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "reset"; + GUILayout.Space(10); + if (GUILayout.Button("BACKUP NOW", primary, GUILayout.Height(32), GUILayout.Width(150))) mp.WorldAction(6, null); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Label("Reset: all moved objects, bought items, doors and vehicle values go back to the server's save game.", small); + GUILayout.Space(10); + + GUILayout.Label("SAVE BACKUPS (NEWEST FIRST)", small); + adminScroll = GUILayout.BeginScrollView(adminScroll); + if (backupNames == null) GUILayout.Label("Loading...", small); + else if (backupNames.Count == 0) GUILayout.Label("No backups yet. They are created automatically when the server takes over a save game, or with BACKUP NOW.", small); + else + { + foreach (string name in backupNames) + { + GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(40)); + GUILayout.Label(PrettyBackup(name), chatName); + GUILayout.FlexibleSpace(); + if (confirmWorld == name) + { + if (GUILayout.Button("REALLY?", danger, GUILayout.Height(28), GUILayout.Width(100))) { mp.WorldAction(7, name); confirmWorld = null; } + GUILayout.Space(4); + if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(28), GUILayout.Width(90))) confirmWorld = null; + } + else if (GUILayout.Button("RESTORE", primary, GUILayout.Height(28), GUILayout.Width(100))) confirmWorld = name; + GUILayout.EndHorizontal(); + GUILayout.Space(4); + } + } + GUILayout.EndScrollView(); + } + + /// "20260927-104626" -> "27.09.2026 10:46:26". + static string PrettyBackup(string name) + { + if (name == null || name.Length != 15 || name[8] != '-') return name; + return name.Substring(6, 2) + "." + name.Substring(4, 2) + "." + name.Substring(0, 4) + " " + + name.Substring(9, 2) + ":" + name.Substring(11, 2) + ":" + name.Substring(13, 2); + } + void DrawAdminBans(MultiplayerManager mp) { GUILayout.Label(mp.IsDedicated ? "Banned players cannot join this server. UNBAN lets them in again."