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."