diff --git a/Server/BanLists.cs b/Server/BanLists.cs new file mode 100644 index 0000000..dbe0d12 --- /dev/null +++ b/Server/BanLists.cs @@ -0,0 +1,361 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +using Shared; +using Shared.Packet.Packets; + +namespace Server; + +using MUCH = Func failToFind, HashSet toActUpon, List<(string arg, IEnumerable amb)> ambig)>; + +public static class BanLists { + public static bool Enabled { + get { + return Settings.Instance.BanList.Enabled; + } + private set { + Settings.Instance.BanList.Enabled = value; + } + } + + private static ISet IPs { + get { + return Settings.Instance.BanList.IpAddresses; + } + } + + private static ISet Profiles { + get { + return Settings.Instance.BanList.Players; + } + } + + private static ISet Stages { + get { + return Settings.Instance.BanList.Stages; + } + } + + + private static bool IsIPv4(string str) { + return IPAddress.TryParse(str, out IPAddress? ip) + && ip != null + && ip.AddressFamily == AddressFamily.InterNetwork; + ; + } + + + public static bool IsIPv4Banned(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 == null) { return false; } + return IsIPv4Banned(ipv4.Address); + } + public static bool IsIPv4Banned(IPAddress ipv4) { + return IsIPv4Banned(ipv4.ToString()); + } + public static bool IsIPv4Banned(string ipv4) { + return IPs.Contains(ipv4); + } + + public static bool IsProfileBanned(Client user) { + return IsProfileBanned(user.Id); + } + public static bool IsProfileBanned(string str) { + if (!Guid.TryParse(str, out Guid id)) { return false; } + return IsProfileBanned(id); + } + public static bool IsProfileBanned(Guid id) { + return Profiles.Contains(id); + } + + public static bool IsStageBanned(string stage) { + return Stages.Contains(stage); + } + + public static bool IsClientBanned(Client user) { + return IsProfileBanned(user) || IsIPv4Banned(user); + } + + + private static void BanIPv4(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 != null) { + BanIPv4(ipv4.Address); + } + } + private static void BanIPv4(IPAddress ipv4) { + BanIPv4(ipv4.ToString()); + } + private static void BanIPv4(string ipv4) { + IPs.Add(ipv4); + } + + private static void BanProfile(Client user) { + BanProfile(user.Id); + } + private static void BanProfile(string str) { + if (!Guid.TryParse(str, out Guid id)) { return; } + BanProfile(id); + } + private static void BanProfile(Guid id) { + Profiles.Add(id); + } + + private static void BanStage(string stage) { + Stages.Add(stage); + } + + private static void BanClient(Client user) { + BanProfile(user); + BanIPv4(user); + } + + + private static void UnbanIPv4(Client user) { + IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint; + if (ipv4 != null) { + UnbanIPv4(ipv4.Address); + } + } + private static void UnbanIPv4(IPAddress ipv4) { + UnbanIPv4(ipv4.ToString()); + } + private static void UnbanIPv4(string ipv4) { + IPs.Remove(ipv4); + } + + private static void UnbanProfile(Client user) { + UnbanProfile(user.Id); + } + private static void UnbanProfile(string str) { + if (!Guid.TryParse(str, out Guid id)) { return; } + UnbanProfile(id); + } + private static void UnbanProfile(Guid id) { + Profiles.Remove(id); + } + + private static void UnbanStage(string stage) { + Stages.Remove(stage); + } + + + private static void Save() { + Settings.SaveSettings(true); + } + + + public static void Crash( + Client user, + bool permanent = false, + bool dispose_user = true, + int delay_ms = 0 + ) { + user.Ignored = true; + Task.Run(async () => { + if (delay_ms > 0) { + await Task.Delay(delay_ms); + } + await user.Send(new ChangeStagePacket { + Id = (permanent ? "$agogus/ban4lyfe" : "$among$us/cr4sh%"), + Stage = (permanent ? "$ejected" : "$agogusStage"), + Scenario = (sbyte) (permanent ? 69 : 21), + SubScenarioType = (byte) (permanent ? 21 : 69), + }); + if (dispose_user) { + user.Dispose(); + } + }); + } + + private static void CrashMultiple(string[] args, MUCH much) { + foreach (Client user in much(args).toActUpon) { + Crash(user, true); + } + } + + + public static string HandleBanCommand(string[] args, MUCH much) { + if (args.Length == 0) { + return "Usage: ban {list|enable|disable|player|profile|ip|stage} ..."; + } + + string cmd = args[0]; + args = args.Skip(1).ToArray(); + + switch (cmd) { + default: + return "Usage: ban {list|enable|disable|player|profile|ip|stage} ..."; + + case "list": + if (args.Length != 0) { + return "Usage: ban list"; + } + StringBuilder list = new StringBuilder(); + list.Append("BanList: " + (Enabled ? "enabled" : "disabled")); + + if (IPs.Count > 0) { + list.Append("\nBanned IPv4 addresses:\n- "); + list.Append(string.Join("\n- ", IPs)); + } + + if (Profiles.Count > 0) { + list.Append("\nBanned profile IDs:\n- "); + list.Append(string.Join("\n- ", Profiles)); + } + + if (Stages.Count > 0) { + list.Append("\nBanned stages:\n- "); + list.Append(string.Join("\n- ", Stages)); + } + + return list.ToString(); + + case "enable": + if (args.Length != 0) { + return "Usage: ban enable"; + } + Enabled = true; + Save(); + return "BanList enabled."; + + case "disable": + if (args.Length != 0) { + return "Usage: ban disable"; + } + Enabled = false; + Save(); + return "BanList disabled."; + + case "player": + if (args.Length == 0) { + return "Usage: ban player <* | !* (usernames to not ban...) | (usernames to ban...)>"; + } + + var res = much(args); + + StringBuilder sb = new StringBuilder(); + sb.Append(res.toActUpon.Count > 0 ? "Banned players: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : ""); + sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : ""); + if (res.ambig.Count > 0) { + res.ambig.ForEach(x => { + sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}"); + }); + } + + foreach (Client user in res.toActUpon) { + BanClient(user); + Crash(user, true); + } + + Save(); + return sb.ToString(); + + case "profile": + if (args.Length != 1) { + return "Usage: ban profile "; + } + if (!Guid.TryParse(args[0], out Guid id)) { + return "Invalid profile ID value!"; + } + if (IsProfileBanned(id)) { + return "Profile " + id.ToString() + " is already banned."; + } + BanProfile(id); + CrashMultiple(args, much); + Save(); + return "Banned profile: " + id.ToString(); + + case "ip": + if (args.Length != 1) { + return "Usage: ban ip "; + } + if (!IsIPv4(args[0])) { + return "Invalid IPv4 address!"; + } + if (IsIPv4Banned(args[0])) { + return "IP " + args[0] + " is already banned."; + } + BanIPv4(args[0]); + CrashMultiple(args, much); + Save(); + return "Banned ip: " + args[0]; + + case "stage": + if (args.Length != 1) { + return "Usage: ban stage "; + } + string? stage = Shared.Stages.Input2Stage(args[0]); + if (stage == null) { + return "Invalid stage name!"; + } + if (IsStageBanned(stage)) { + return "Stage " + stage + " is already banned."; + } + var stages = Shared.Stages + .StagesByInput(args[0]) + .Where(s => !IsStageBanned(s)) + .ToList() + ; + foreach (string s in stages) { + BanStage(s); + } + Save(); + return "Banned stage: " + string.Join(", ", stages); + } + } + + + public static string HandleUnbanCommand(string[] args) { + if (args.Length != 2) { + return "Usage: unban {profile|ip|stage} "; + } + + string cmd = args[0]; + string val = args[1]; + + switch (cmd) { + default: + return "Usage: unban {profile|ip|stage} "; + + case "profile": + if (!Guid.TryParse(val, out Guid id)) { + return "Invalid profile ID value!"; + } + if (!IsProfileBanned(id)) { + return "Profile " + id.ToString() + " is not banned."; + } + UnbanProfile(id); + Save(); + return "Unbanned profile: " + id.ToString(); + + case "ip": + if (!IsIPv4(val)) { + return "Invalid IPv4 address!"; + } + if (!IsIPv4Banned(val)) { + return "IP " + val + " is not banned."; + } + UnbanIPv4(val); + Save(); + return "Unbanned ip: " + val; + + case "stage": + string stage = Shared.Stages.Input2Stage(val) ?? val; + if (!IsStageBanned(stage)) { + return "Stage " + stage + " is not banned."; + } + var stages = Shared.Stages + .StagesByInput(val) + .Where(IsStageBanned) + .ToList() + ; + foreach (string s in stages) { + UnbanStage(s); + } + Save(); + return "Unbanned stage: " + string.Join(", ", stages); + } + } +} diff --git a/Server/Client.cs b/Server/Client.cs index 5f3336f..4b53525 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -12,6 +12,7 @@ namespace Server; public class Client : IDisposable { public readonly ConcurrentDictionary Metadata = new ConcurrentDictionary(); // can be used to store any information about a player public bool Connected = false; + public bool Ignored = false; public CostumePacket? CurrentCostume = null; // required for proper client sync public string Name { get => Logger.Name; @@ -76,6 +77,17 @@ public class Client : IDisposable { await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None); } + public void CleanMetadataOnNewConnection() { + object? tmp; + Metadata.TryRemove("time", out tmp); + Metadata.TryRemove("seeking", out tmp); + Metadata.TryRemove("lastCostumePacket", out tmp); + Metadata.TryRemove("lastCapturePacket", out tmp); + Metadata.TryRemove("lastTagPacket", out tmp); + Metadata.TryRemove("lastGamePacket", out tmp); + Metadata.TryRemove("lastPlayerPacket", out tmp); + } + public static bool operator ==(Client? left, Client? right) { return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id; } diff --git a/Server/Program.cs b/Server/Program.cs index 93f1aa9..55c60c4 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -62,23 +62,11 @@ async Task LoadShines() await LoadShines(); server.ClientJoined += (c, _) => { - if (Settings.Instance.BanList.Enabled - && (Settings.Instance.BanList.Players.Contains(c.Id) - || Settings.Instance.BanList.IpAddresses.Contains( - ((IPEndPoint) c.Socket!.RemoteEndPoint!).Address.ToString()))) - throw new Exception($"Banned player attempted join: {c.Name}"); c.Metadata["shineSync"] = new ConcurrentBag(); c.Metadata["loadedSave"] = false; c.Metadata["scenario"] = (byte?) 0; c.Metadata["2d"] = false; c.Metadata["speedrun"] = false; - foreach (Client client in server.ClientsConnected) { - try { - c.Send((GamePacket) client.Metadata["lastGamePacket"]!, client).Wait(); - } catch { - // lol who gives a fuck - } - } }; async Task ClientSyncShineBag(Client client) { @@ -115,13 +103,41 @@ timer.Start(); float MarioSize(bool is2d) => is2d ? 180 : 160; +void flipPlayer(Client c, ref PlayerPacket pp) { + pp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); + pp.Rotation *= ( + Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) + * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)) + ); +}; + +void logError(Task x) { + if (x.Exception != null) { + consoleLogger.Error(x.Exception.ToString()); + } +}; + server.PacketHandler = (c, p) => { switch (p) { case GamePacket gamePacket: { + if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) { + c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}."); + BanLists.Crash(c, false, false, 500); + return false; + } c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); + + // reset lastPlayerPacket on stage changes + object? old = null; + c.Metadata.TryGetValue("lastGamePacket", out old); + if (old != null && ((GamePacket) old).Stage != gamePacket.Stage) { + c.Metadata["lastPlayerPacket"] = null; + } + c.Metadata["scenario"] = gamePacket.ScenarioNum; c.Metadata["2d"] = gamePacket.Is2d; c.Metadata["lastGamePacket"] = gamePacket; + switch (gamePacket.Stage) { case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0: c.Metadata["speedrun"] = true; @@ -145,8 +161,7 @@ server.PacketHandler = (c, p) => { server.BroadcastReplace(gamePacket, c, (from, to, gp) => { gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200; #pragma warning disable CS4014 - to.Send(gp, from) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + to.Send(gp, from).ContinueWith(logError); #pragma warning restore CS4014 }); return false; @@ -156,14 +171,23 @@ server.PacketHandler = (c, p) => { } case TagPacket tagPacket: { + // c.Logger.Info($"Got tag packet: {tagPacket.IsIt}"); + c.Metadata["lastTagPacket"] = tagPacket; if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt; if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0) c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now); break; } + case CapturePacket capturePacket: { + // c.Logger.Info($"Got capture packet: {capturePacket.ModelName}"); + c.Metadata["lastCapturePacket"] = capturePacket; + break; + } + case CostumePacket costumePacket: c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}"); + c.Metadata["lastCostumePacket"] = costumePacket; c.CurrentCostume = costumePacket; #pragma warning disable CS4014 ClientSyncShineBag(c); //no point logging since entire def has try/catch @@ -183,33 +207,35 @@ server.PacketHandler = (c, p) => { break; } - case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled - && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others - && Settings.Instance.Flip.Players.Contains(c.Id): { - playerPacket.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); - playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) - * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)); + case PlayerPacket playerPacket: { + c.Metadata["lastPlayerPacket"] = playerPacket; + // flip for all + if ( Settings.Instance.Flip.Enabled + && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others + && Settings.Instance.Flip.Players.Contains(c.Id) + ) { + flipPlayer(c, ref playerPacket); #pragma warning disable CS4014 - server.Broadcast(playerPacket, c) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + server.Broadcast(playerPacket, c).ContinueWith(logError); #pragma warning restore CS4014 - return false; - } - case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled - && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self - && !Settings.Instance.Flip.Players.Contains(c.Id): { - server.BroadcastReplace(playerPacket, c, (from, to, sp) => { - if (Settings.Instance.Flip.Players.Contains(to.Id)) { - sp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); - sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) - * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI)); - } + return false; + } + // flip only for specific clients + if ( Settings.Instance.Flip.Enabled + && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self + && !Settings.Instance.Flip.Players.Contains(c.Id) + ) { + server.BroadcastReplace(playerPacket, c, (from, to, sp) => { + if (Settings.Instance.Flip.Players.Contains(to.Id)) { + flipPlayer(c, ref sp); + } #pragma warning disable CS4014 - to.Send(sp, from) - .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); + to.Send(sp, from).ContinueWith(logError); #pragma warning restore CS4014 - }); - return false; + }); + return false; + } + break; } } @@ -220,38 +246,49 @@ server.PacketHandler = (c, p) => { HashSet failToFind = new(); HashSet toActUpon; List<(string arg, IEnumerable amb)> ambig = new(); - if (args[0] == "*") + if (args[0] == "*") { toActUpon = new(server.Clients.Where(c => c.Connected)); + } else { toActUpon = args[0] == "!*" ? new(server.Clients.Where(c => c.Connected)) : new(); for (int i = (args[0] == "!*" ? 1 : 0); i < args.Length; i++) { string arg = args[i]; - IEnumerable search = server.Clients.Where(c => c.Connected && - (c.Name.ToLower().StartsWith(arg.ToLower()) || (Guid.TryParse(arg, out Guid res) && res == c.Id))); - if (!search.Any()) + IEnumerable search = server.Clients.Where(c => c.Connected && ( + c.Name.ToLower().StartsWith(arg.ToLower()) + || (Guid.TryParse(arg, out Guid res) && res == c.Id) + || (IPAddress.TryParse(arg, out IPAddress? ip) && ip.Equals(((IPEndPoint) c.Socket!.RemoteEndPoint!).Address)) + )); + if (!search.Any()) { failToFind.Add(arg); //none found + } else if (search.Count() > 1) { Client? exact = search.FirstOrDefault(x => x.Name == arg); if (!ReferenceEquals(exact, null)) { //even though multiple matches, since exact match, it isn't ambiguous - if (args[0] == "!*") + if (args[0] == "!*") { toActUpon.Remove(exact); - else + } + else { toActUpon.Add(exact); + } } else { - if (!ambig.Any(x => x.arg == arg)) + if (!ambig.Any(x => x.arg == arg)) { ambig.Add((arg, search.Select(x => x.Name))); //more than one match - foreach (var rem in search.ToList()) //need copy because can't remove from list while iterating over it + } + foreach (var rem in search.ToList()) { //need copy because can't remove from list while iterating over it toActUpon.Remove(rem); + } } } else { //only one match, so autocomplete - if (args[0] == "!*") + if (args[0] == "!*") { toActUpon.Remove(search.First()); - else + } + else { toActUpon.Add(search.First()); + } } } } @@ -298,54 +335,14 @@ CommandHandler.RegisterCommand("crash", args => { } foreach (Client user in res.toActUpon) { - Task.Run(async () => { - await user.Send(new ChangeStagePacket { - Id = "$among$us/SubArea", - Stage = "$agogusStage", - Scenario = 21, - SubScenarioType = 69 // invalid id - }); - user.Dispose(); - }); + BanLists.Crash(user); } return sb.ToString(); }); -CommandHandler.RegisterCommand("ban", args => { - if (args.Length == 0) { - return "Usage: ban <* | !* (usernames to not ban...) | (usernames to ban...)>"; - } - - var res = MultiUserCommandHelper(args); - - StringBuilder sb = new StringBuilder(); - sb.Append(res.toActUpon.Count > 0 ? "Banned: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : ""); - sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : ""); - if (res.ambig.Count > 0) { - res.ambig.ForEach(x => { - sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}"); - }); - } - - foreach (Client user in res.toActUpon) { - Task.Run(async () => { - await user.Send(new ChangeStagePacket { - Id = "$agogus/banned4lyfe", - Stage = "$ejected", - Scenario = 69, - SubScenarioType = 21 // invalid id - }); - IPEndPoint? endpoint = (IPEndPoint?) user.Socket?.RemoteEndPoint; - Settings.Instance.BanList.Players.Add(user.Id); - if (endpoint != null) Settings.Instance.BanList.IpAddresses.Add(endpoint.ToString()); - user.Dispose(); - }); - } - - Settings.SaveSettings(); - return sb.ToString(); -}); +CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); }); +CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); }); CommandHandler.RegisterCommand("send", args => { const string optionUsage = "Usage: send "; @@ -651,7 +648,7 @@ Task.Run(() => { } } } -}).ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); +}).ContinueWith(logError); #pragma warning restore CS4014 await server.Listen(cts.Token); diff --git a/Server/Server.cs b/Server/Server.cs index dd376e6..5cf8a41 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -29,6 +29,11 @@ public class Server { Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync(); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { + Logger.Warn($"Ignoring banned IPv4 address {socket.RemoteEndPoint}"); + continue; + } + Logger.Notify($"Accepted connection for client {socket.RemoteEndPoint}"); try { @@ -64,7 +69,7 @@ public class Server { public static void FillPacket(PacketHeader header, T packet, Memory memory) where T : struct, IPacket { Span data = memory.Span; - + header.Serialize(data[..Constants.HeaderSize]); packet.Serialize(data[Constants.HeaderSize..]); } @@ -167,6 +172,11 @@ public class Server { break; } + if (client.Ignored) { + memory.Dispose(); + continue; + } + // connection initialization if (first) { first = false; @@ -174,6 +184,18 @@ public class Server { ConnectPacket connect = new ConnectPacket(); connect.Deserialize(memory.Memory.Span[packetRange]); + + bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; + + if (BanLists.Enabled && BanLists.IsProfileBanned(header.Id)) { + client.Id = header.Id; + client.Name = connect.ClientName; + client.Ignored = true; + client.Logger.Warn($"Ignoring banned profile ID {header.Id}"); + memory.Dispose(); + continue; + } + lock (Clients) { if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { client.Logger.Error($"Turned away as server is at max clients"); @@ -218,27 +240,35 @@ public class Server { // done disconnecting and removing stale clients with the same id ClientJoined?.Invoke(client, connect); + // a new connection, not a reconnect, for an existing client + } else if (wasFirst) { + client.CleanMetadataOnNewConnection(); } } + // for all other clients that are already connected List otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null); await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { IMemoryOwner tempBuffer = MemoryPool.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size)); + + // make the other client known to the (new) client PacketHeader connectHeader = new PacketHeader { - Id = other.Id, - Type = PacketType.Connect, - PacketSize = connect.Size + Id = other.Id, + Type = PacketType.Connect, + PacketSize = connect.Size, }; connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]); ConnectPacket connectPacket = new ConnectPacket { ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is - MaxPlayers = Settings.Instance.Server.MaxPlayers, - ClientName = other.Name + MaxPlayers = Settings.Instance.Server.MaxPlayers, + ClientName = other.Name, }; connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]); await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null); + + // tell the (new) client what costume the other client has if (other.CurrentCostume.HasValue) { - connectHeader.Type = PacketType.Costume; + connectHeader.Type = PacketType.Costume; connectHeader.PacketSize = other.CurrentCostume.Value.Size; connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]); other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]); @@ -246,9 +276,17 @@ public class Server { } tempBuffer.Dispose(); + + // make the other client reset their puppet cache for this client, if it is a new connection (after restart) + if (wasFirst) { + await SendEmptyPackets(client, other); + } }); Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected."); + + // send missing or outdated packets from others to the new client + await ResendPackets(client); } else if (header.Id != client.Id && client.Id != Guid.Empty) { throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}"); } @@ -310,6 +348,38 @@ public class Server { #pragma warning restore CS4014 } + private async Task ResendPackets(Client client) { + async Task trySend(Client other, string packetType) where T : struct, IPacket { + if (! other.Metadata.ContainsKey(packetType)) { return; } + try { + await client.Send((T) other.Metadata[packetType]!, other); + } + catch { + // lol who gives a fuck + } + }; + await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => { + if (client.Id == other.Id) { return; } + await trySend(other, "lastCostumePacket"); + await trySend(other, "lastCapturePacket"); + await trySend(other, "lastTagPacket"); + await trySend(other, "lastGamePacket"); + await trySend(other, "lastPlayerPacket"); + }); + } + + private async Task SendEmptyPackets(Client client, Client other) { + await other.Send(new TagPacket { + UpdateType = TagPacket.TagUpdate.State | TagPacket.TagUpdate.Time, + IsIt = false, + Seconds = 0, + Minutes = 0, + }, client); + await other.Send(new CapturePacket { + ModelName = "", + }, client); + } + private static PacketHeader GetHeader(Span data) { //no need to error check, the client will disconnect when the packet is invalid :) PacketHeader header = new PacketHeader(); diff --git a/Server/Settings.cs b/Server/Settings.cs index 25a6e4c..2ded6b7 100644 --- a/Server/Settings.cs +++ b/Server/Settings.cs @@ -30,10 +30,10 @@ public class Settings { LoadHandler?.Invoke(); } - public static void SaveSettings() { + public static void SaveSettings(bool silent = false) { try { File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy()))); - Logger.Info("Saved settings to settings.json"); + if (!silent) { Logger.Info("Saved settings to settings.json"); } } catch (Exception e) { Logger.Error($"Failed to save settings.json {e}"); @@ -43,7 +43,7 @@ public class Settings { public ServerTable Server { get; set; } = new ServerTable(); public FlipTable Flip { get; set; } = new FlipTable(); public ScenarioTable Scenario { get; set; } = new ScenarioTable(); - public BannedPlayers BanList { get; set; } = new BannedPlayers(); + public BanListTable BanList { get; set; } = new BanListTable(); public DiscordTable Discord { get; set; } = new DiscordTable(); public ShineTable Shines { get; set; } = new ShineTable(); public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable(); @@ -58,15 +58,16 @@ public class Settings { public bool MergeEnabled { get; set; } = false; } - public class BannedPlayers { + public class BanListTable { public bool Enabled { get; set; } = false; - public List Players { get; set; } = new List(); - public List IpAddresses { get; set; } = new List(); + public ISet Players { get; set; } = new SortedSet(); + public ISet IpAddresses { get; set; } = new SortedSet(); + public ISet Stages { get; set; } = new SortedSet(); } public class FlipTable { public bool Enabled { get; set; } = true; - public List Players { get; set; } = new List(); + public ISet Players { get; set; } = new SortedSet(); public FlipOptions Pov { get; set; } = FlipOptions.Both; } @@ -95,4 +96,4 @@ public class Settings { public bool Enabled { get; set; } = false; public string Filename { get; set; } = "./moons.json"; } -} \ No newline at end of file +} diff --git a/Shared/Stages.cs b/Shared/Stages.cs index bbb8171..04315d5 100644 --- a/Shared/Stages.cs +++ b/Shared/Stages.cs @@ -11,7 +11,7 @@ public static class Stages { return mapName; } // exact stage value - if (Stage2Alias.ContainsKey(input)) { + if (IsStage(input)) { return input; } // force input value with a ! @@ -29,6 +29,32 @@ public static class Stages { return result; } + public static bool IsAlias(string input) { + return Alias2Stage.ContainsKey(input); + } + + public static bool IsStage(string input) { + return Stage2Alias.ContainsKey(input); + } + + public static IEnumerable StagesByInput(string input) { + if (IsAlias(input)) { + var stages = Stage2Alias + .Where(e => e.Value == input) + .Select(e => e.Key) + ; + foreach (string stage in stages) { + yield return stage; + } + } + else { + string? stage = Input2Stage(input); + if (stage != null) { + yield return stage; + } + } + } + public static readonly Dictionary Alias2Stage = new Dictionary() { { "cap", "CapWorldHomeStage" }, { "cascade", "WaterfallWorldHomeStage" }, @@ -38,8 +64,8 @@ public static class Stages { { "cloud", "CloudWorldHomeStage" }, { "lost", "ClashWorldHomeStage" }, { "metro", "CityWorldHomeStage" }, - { "sea", "SeaWorldHomeStage" }, { "snow", "SnowWorldHomeStage" }, + { "sea", "SeaWorldHomeStage" }, { "lunch", "LavaWorldHomeStage" }, { "ruined", "BossRaidWorldHomeStage" }, { "bowser", "SkyWorldHomeStage" }, @@ -59,8 +85,8 @@ public static class Stages { { "cloud", "Cloud Kingdom" }, { "lost", "Lost Kingdom" }, { "metro", "Metro Kingdom" }, - { "sea", "Snow Kingdom" }, - { "snow", "Seaside Kingdom" }, + { "snow", "Snow Kingdom" }, + { "sea", "Seaside Kingdom" }, { "lunch", "Luncheon Kingdom" }, { "ruined", "Ruined Kingdom" }, { "bowser", "Bowser's Kingdom" },