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