diff --git a/GameServer.cs b/GameServer.cs
index dd1d65c..f40c65b 100644
--- a/GameServer.cs
+++ b/GameServer.cs
@@ -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;
/// Uhrzeit/Wetter gibt der Spieler mit der kleinsten ID vor (so rechnen es auch die Clients).
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
}
/// Chatnachricht vom Server an alle (Absender-ID 0 = "Server").
+ 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 [reason] | ban [reason] | say | op | deop | admins | save | reset-world | reload-save | stop");
+ Log("status | players | kick [reason] | ban [reason] | bans | unban | say | op | deop | admins | save | reset-world | reload-save | stop");
break;
case "say":
if (args.Length < 2) { Log("say "); 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 (see 'bans')");
+ break;
case "kick":
case "ban":
{
diff --git a/Protocol.cs b/Protocol.cs
index 451963e..40fb757 100644
--- a/Protocol.cs
+++ b/Protocol.cs
@@ -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)
diff --git a/README.md b/README.md
index 89bef1f..d611d7c 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,7 @@ Fehlende Einstellungen ergänzt der Server beim Start automatisch. Umgebungsvari
|---|---|
| `status`, `players` | Übersicht, Spielerliste mit IDs |
| `kick [grund]`, `ban [grund]` | Spieler entfernen / sperren |
+| `bans`, `unban ` | Gesperrte anzeigen / Sperre aufheben |
| `say ` | Chatnachricht an alle |
| `admins`, `op `, `deop <...>` | Admins anzeigen / geben / nehmen |
| `save` | Weltzustand jetzt speichern |
diff --git a/Tests/Program.cs b/Tests/Program.cs
index c357024..fdc6696 100644
--- a/Tests/Program.cs
+++ b/Tests/Program.cs
@@ -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());
+ 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 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";
}
diff --git a/WorldState.cs b/WorldState.cs
index 895e130..9a130e4 100644
--- a/WorldState.cs
+++ b/WorldState.cs
@@ -27,6 +27,9 @@ public sealed class WorldState
public Dictionary Doors { get; set; } = new();
/// Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert].
public Dictionary Vars { get; set; } = new();
+ /// Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.
+ public List 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);
+ }
});
}
}