Admin: spawn items (saved on the server), unban (menu tab + console), tabbed admin menu; dashboard needles and steering wheel synced; server browser row opens the profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:15:35 +02:00
co-authored by Claude Opus 5.5
parent ba396146f2
commit 85fbb8a3ce
5 changed files with 77 additions and 3 deletions
+33 -2
View File
@@ -240,7 +240,8 @@ public sealed class GameServer
static bool IsGameMessage(MsgType t) =>
t is MsgType.Player or MsgType.ObjStates or MsgType.ObjOwn or MsgType.ObjRelease or MsgType.FsmEvent
or MsgType.VehicleFx or MsgType.WorldTime or MsgType.MoneyDelta or MsgType.DoorState or MsgType.Chat or MsgType.VarSync;
or MsgType.VehicleFx or MsgType.WorldTime or MsgType.MoneyDelta or MsgType.DoorState or MsgType.Chat or MsgType.VarSync
or MsgType.SpawnItem;
/// <summary>Uhrzeit/Wetter gibt der Spieler mit der kleinsten ID vor (so rechnen es auch die Clients).</summary>
int WorldAuthorityId => clients.Values.Where(c => c.State == ClientState.Joined).Select(c => c.Id).DefaultIfEmpty(-1).Min();
@@ -295,6 +296,9 @@ public sealed class GameServer
{
w.Write(joinedPlayers.Count);
foreach (var p in joinedPlayers) { w.Write(p.Id); w.Write(p.Name); w.Write(p.SteamId); w.Write(p.IsAdmin); }
// Gebannte Spieler (für UNBAN) - am Ende angehängt, ältere Mods lesen hier nicht weiter
w.Write(config.BannedSteamIds.Count);
foreach (string id in config.BannedSteamIds) w.Write(id);
}));
return;
@@ -307,6 +311,7 @@ public sealed class GameServer
string reason = r.ReadString();
string result;
if (!c.IsAdmin) result = "You are not an admin on this server";
else if (action == 4) result = Unban(reason, c.Name); // "reason" enthält hier die gebannte ID
else if (!clients.TryGetValue(targetId, out var target) || target.State != ClientState.Joined) result = "Player not found";
else if (action == 3)
{
@@ -339,6 +344,11 @@ public sealed class GameServer
case MsgType.MoneyDelta: world.ApplyMoney(m.Payload); break;
case MsgType.DoorState: world.ApplyDoor(m.Payload); break;
case MsgType.VarSync: world.ApplyVars(m.Payload); break;
case MsgType.SpawnItem:
if (!c.IsAdmin) return; // nur Admins dürfen Items spawnen
world.AddSpawn(m.Payload);
Log($"{c.Name} spawned {SpawnName(m.Payload)}");
break;
case MsgType.Chat:
if (m.Payload.Length > 1024) return; // Spam/Unsinn nicht weiterleiten
Log($"[chat] {c.Name}: {m.Reader().ReadString()}");
@@ -447,6 +457,21 @@ public sealed class GameServer
}
/// <summary>Chatnachricht vom Server an alle (Absender-ID 0 = "Server").</summary>
string Unban(string id, string by)
{
id = (id ?? "").Trim();
if (!config.BannedSteamIds.Remove(id)) return $"{id} is not banned";
config.Save(configPath);
Log($"{id} was unbanned by {by}");
return $"{id} is no longer banned";
}
static string SpawnName(byte[] payload)
{
try { return new BinaryReader(new MemoryStream(payload)).ReadString(); }
catch { return "an item"; }
}
void ServerChat(string text)
{
Broadcast(MsgType.Chat, Wire.WithSender(ServerId, Wire.Build(w => w.Write(text))));
@@ -470,7 +495,7 @@ public sealed class GameServer
switch (args[0].ToLowerInvariant())
{
case "help":
Log("status | players | kick <id> [reason] | ban <id> [reason] | say <text> | op <id> | deop <id> | admins | save | reset-world | reload-save | stop");
Log("status | players | kick <id> [reason] | ban <id> [reason] | bans | unban <SteamID|name:x> | say <text> | op <id> | deop <id> | admins | save | reset-world | reload-save | stop");
break;
case "say":
if (args.Length < 2) { Log("say <text>"); break; }
@@ -526,6 +551,12 @@ public sealed class GameServer
foreach (var c in clients.Values.Where(c => c.State == ClientState.Joined))
Log($" [{c.Id}] {c.Name} SteamID {c.SteamId} {c.Conn.RemoteAddress}");
break;
case "bans":
Log("Banned: " + (config.BannedSteamIds.Count == 0 ? "(none)" : string.Join(", ", config.BannedSteamIds)));
break;
case "unban":
Log(args.Length > 1 ? Unban(args[1], "Console") : "unban <SteamID64 or name:playername> (see 'bans')");
break;
case "kick":
case "ban":
{
+2 -1
View File
@@ -35,7 +35,8 @@ public enum MsgType : byte
Chat = 28, // Chatnachricht (string)
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
Pong = 31 // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
Pong = 31, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
SpawnItem = 32 // Admin spawnt ein Item: string Item-Name, 3 float Position, 4 float Drehung
}
public readonly record struct Message(MsgType Type, byte[] Payload)
+1
View File
@@ -125,6 +125,7 @@ Fehlende Einstellungen ergänzt der Server beim Start automatisch. Umgebungsvari
|---|---|
| `status`, `players` | Übersicht, Spielerliste mit IDs |
| `kick <id> [grund]`, `ban <id> [grund]` | Spieler entfernen / sperren |
| `bans`, `unban <SteamID64 oder name:spieler>` | Gesperrte anzeigen / Sperre aufheben |
| `say <text>` | Chatnachricht an alle |
| `admins`, `op <id oder SteamID64>`, `deop <...>` | Admins anzeigen / geben / nehmen |
| `save` | Weltzustand jetzt speichern |
+21
View File
@@ -135,6 +135,24 @@ try
Check(spawn != null && Math.Abs(spawn[0] - 250f) < 0.01f && Math.Abs(spawn[2] + 120f) < 0.01f,
"Rejoining returns the last real position, not the save's start point or a flung-away one: " + (spawn == null ? "none" : string.Join(", ", spawn)));
pos2.Close();
Console.WriteLine("== Admin: spawn items / unban");
var noAdmin = await TestClient.Join("NoAdmin", 0, null);
var watcher = await TestClient.Join("Watcher", 0, null);
noAdmin.Send(MsgType.SpawnItem, Wire.Build(w => { w.Write("beer case"); w.Write(1f); w.Write(2f); w.Write(3f); w.Write(0f); w.Write(0f); w.Write(0f); w.Write(1f); }));
Check(await watcher.WaitFor(MsgType.SpawnItem, 800) == null, "Non-admin cannot spawn items (not relayed)");
watcher.Send(MsgType.Ready, Array.Empty<byte>());
Check(Snapshot.Parse(await watcher.WaitFor(MsgType.WorldSnapshot)).Spawns == 0, "Non-admin spawn is not stored");
server.EnqueueCommand("ban " + noAdmin.Id + " test");
await Task.Delay(500);
var bannedTry = await TestClient.Join("NoAdmin", 0, null, expectReject: true);
Check(bannedTry.RejectReason != null && bannedTry.RejectReason.Contains("banned"), "Banned player cannot join: " + bannedTry.RejectReason);
server.EnqueueCommand("unban name:noadmin");
await Task.Delay(500);
var unbanned = await TestClient.Join("NoAdmin", 0, null);
Check(unbanned.Welcome != null, "After unban the player can join again");
unbanned.Close();
watcher.Close();
}
catch (Exception e)
{
@@ -169,6 +187,7 @@ sealed class Snapshot
public string Info;
public HashSet<int> ObjectIds = new();
public float[] Spawn;
public int Spawns = -1;
public int Vars = -1;
public static Snapshot Parse(byte[] data)
@@ -188,6 +207,8 @@ sealed class Snapshot
for (int i = 0; i < doors; i++) { r.ReadString(); r.ReadString(); }
s.Vars = r.ReadInt32();
for (int i = 0; i < s.Vars; i++) { r.ReadInt32(); byte t = r.ReadByte(); r.ReadBytes(t == 3 ? 1 : 4); }
s.Spawns = r.ReadInt32();
for (int i = 0; i < s.Spawns; i++) r.ReadBytes(r.ReadInt32());
s.Ok = r.BaseStream.Position == r.BaseStream.Length;
s.Info = $"{objects} objects, {events} events, money {money}, {doors} doors, {s.Vars} vars";
}
+20
View File
@@ -27,6 +27,9 @@ public sealed class WorldState
public Dictionary<string, string> Doors { get; set; } = new();
/// <summary>Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert].</summary>
public Dictionary<int, string> Vars { get; set; } = new();
/// <summary>Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.</summary>
public List<string> Spawns { get; set; } = new();
public const int MaxSpawns = 2000;
public bool Dirty { get; private set; }
@@ -48,6 +51,7 @@ public sealed class WorldState
public void Reset()
{
Spawns.Clear();
Objects.Clear();
Events.Clear();
PlayerPositions.Clear();
@@ -146,6 +150,13 @@ public sealed class WorldState
public const int MaxVars = 50_000;
public const int MaxObjects = 20_000;
public void AddSpawn(byte[] payload)
{
if (Spawns.Count >= MaxSpawns || payload.Length > 512) return;
Spawns.Add(Convert.ToBase64String(payload));
Dirty = true;
}
public void AddFsmEvent(byte[] payload)
{
if (Events.Count >= MaxEvents) return;
@@ -209,6 +220,15 @@ public sealed class WorldState
w.Write(kv.Key);
w.Write(Convert.FromBase64String(kv.Value));
}
// Von Admins gespawnte Items: int n, je [int Länge, SpawnItem-Payload]
w.Write(Spawns.Count);
foreach (string sp in Spawns)
{
byte[] bytes = Convert.FromBase64String(sp);
w.Write(bytes.Length);
w.Write(bytes);
}
});
}
}