diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e7cbc41..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -/Server/bin/ -/Server/obj/ -/Shared/bin/ -/Shared/obj/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b581b3..74ffcb5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,35 +73,3 @@ jobs: platforms : linux/amd64,linux/arm/v7,linux/arm64/v8 cache-from : type=gha,scope=${{ github.workflow }} cache-to : type=gha,scope=${{ github.workflow }},mode=max - - - name: Build binary files - run: | - ./docker-build.sh all - - - name : Upload Server - uses : actions/upload-artifact@v3 - with: - name : Server - path : ./bin/Server - if-no-files-found : error - - - name : Upload Server.arm - uses : actions/upload-artifact@v3 - with: - name : Server.arm - path : ./bin/Server.arm - if-no-files-found : error - - - name : Upload Server.arm64 - uses : actions/upload-artifact@v3 - with: - name : Server.arm64 - path : ./bin/Server.arm64 - if-no-files-found : error - - - name : Upload Server.exe - uses : actions/upload-artifact@v3 - with: - name : Server.exe - path : ./bin/Server.exe - if-no-files-found : error diff --git a/.gitignore b/.gitignore index 2baf885..64e0e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,3 @@ riderModule.iml .idea/ settings.json .vs/ - -/cache/ -/data/ diff --git a/Server/BanLists.cs b/Server/BanLists.cs deleted file mode 100644 index 3f16a61..0000000 --- a/Server/BanLists.cs +++ /dev/null @@ -1,359 +0,0 @@ -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, - int delay_ms = 0 - ) { - user.Ignored = true; - Task.Run(async () => { - if (delay_ms > 0) { - await Task.Delay(delay_ms); - } - bool permanent = user.Banned; - 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), - }); - }); - } - - private static void CrashMultiple(string[] args, MUCH much) { - foreach (Client user in much(args).toActUpon) { - user.Banned = true; - Crash(user); - } - } - - - 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) { - user.Banned = true; - BanClient(user); - Crash(user); - } - - 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 8e7ba2d..5f3336f 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -12,8 +12,6 @@ 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 bool Banned = false; public CostumePacket? CurrentCostume = null; // required for proper client sync public string Name { get => Logger.Name; @@ -42,9 +40,8 @@ public Client(Client other, Socket socket) { } public void Dispose() { - if (Socket?.Connected is true) { + if (Socket?.Connected is true) Socket.Disconnect(false); - } } @@ -53,14 +50,9 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)]; try { - // don't send most packets to ignored players - if (Ignored && packetAttribute.Type != PacketType.Init && packetAttribute.Type != PacketType.ChangeStage) { - memory.Dispose(); - return; - } Server.FillPacket(new PacketHeader { - Id = sender?.Id ?? Id, - Type = packetAttribute.Type, + Id = sender?.Id ?? Id, + Type = packetAttribute.Type, PacketSize = packet.Size }, packet, memory.Memory); } @@ -76,42 +68,14 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa public async Task Send(Memory data, Client? sender) { PacketHeader header = new PacketHeader(); header.Deserialize(data.Span); - - if (!Connected && !Ignored && header.Type != PacketType.Connect) { + if (!Connected && header.Type is not PacketType.Connect) { Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet"); return; } - // don't send most packets to ignored players - if (Ignored && header.Type != PacketType.Init && header.Type != PacketType.ChangeStage) { - return; - } - 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("lastGamePacket", out tmp); - Metadata.TryRemove("lastPlayerPacket", out tmp); - } - - public TagPacket? GetTagPacket() { - var time = (Time?) (this.Metadata.ContainsKey("time") ? this.Metadata["time"] : null); - var seek = (bool?) (this.Metadata.ContainsKey("seeking") ? this.Metadata["seeking"] : null); - if (time == null && seek == null) { return null; } - return new TagPacket { - UpdateType = (seek != null ? TagPacket.TagUpdate.State : 0) | (time != null ? TagPacket.TagUpdate.Time: 0), - IsIt = seek ?? false, - Seconds = (byte) (time?.Seconds ?? 0), - Minutes = (ushort) (time?.Minutes ?? 0), - }; - } - 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 a0a7dd3..7e46b1e 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -62,11 +62,23 @@ 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) { @@ -74,7 +86,7 @@ async Task ClientSyncShineBag(Client client) { try { if ((bool?) client.Metadata["speedrun"] ?? false) return; ConcurrentBag clientBag = (ConcurrentBag) (client.Metadata["shineSync"] ??= new ConcurrentBag()); - foreach (int shine in shineBag.Except(clientBag).Except(Settings.Instance.Shines.Excluded).ToArray()) { + foreach (int shine in shineBag.Except(clientBag).ToArray()) { if (!client.Connected) return; await client.Send(new ShinePacket { ShineId = shine @@ -103,50 +115,13 @@ async void SyncShineBag() { 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: { - // crash ignored player - if (c.Ignored) { - c.Logger.Info($"Crashing ignored player after entering stage {gamePacket.Stage}."); - BanLists.Crash(c, 500); - return false; - } - - // crash player entering a banned stage - if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) { - c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}."); - BanLists.Crash(c, 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; @@ -170,7 +145,8 @@ void logError(Task x) { server.BroadcastReplace(gamePacket, c, (from, to, gp) => { gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200; #pragma warning disable CS4014 - to.Send(gp, from).ContinueWith(logError); + to.Send(gp, from) + .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 }); return false; @@ -179,42 +155,24 @@ void logError(Task x) { break; } - // ignore all other packets from ignored players - case IPacket pack when c.Ignored: { - return false; - } - case TagPacket tagPacket: { - // c.Logger.Info($"Got tag packet: {tagPacket.IsIt}"); 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: { + 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 #pragma warning restore CS4014 c.Metadata["loadedSave"] = true; break; - } case ShinePacket shinePacket: { if (!Settings.Instance.Shines.Enabled) return false; - if (Settings.Instance.Shines.Excluded.Contains(shinePacket.ShineId)) { - c.Logger.Info($"Got moon {shinePacket.ShineId} (excluded)"); - return false; - } if (c.Metadata["loadedSave"] is false) break; ConcurrentBag playerBag = (ConcurrentBag)c.Metadata["shineSync"]!; shineBag.Add(shinePacket.ShineId); @@ -225,35 +183,33 @@ void logError(Task x) { break; } - 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); + 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)); #pragma warning disable CS4014 - server.Broadcast(playerPacket, c).ContinueWith(logError); + server.Broadcast(playerPacket, c) + .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 - 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); - } + 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)); + } #pragma warning disable CS4014 - to.Send(sp, from).ContinueWith(logError); + to.Send(sp, from) + .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 - }); - return false; - } - break; + }); + return false; } } @@ -264,49 +220,38 @@ void logError(Task x) { 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) - || (IPAddress.TryParse(arg, out IPAddress? ip) && ip.Equals(((IPEndPoint) c.Socket!.RemoteEndPoint!).Address)) - )); - 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))); + 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()); - } } } } @@ -353,14 +298,54 @@ void logError(Task x) { } foreach (Client user in res.toActUpon) { - BanLists.Crash(user); + Task.Run(async () => { + await user.Send(new ChangeStagePacket { + Id = "$among$us/SubArea", + Stage = "$agogusStage", + Scenario = 21, + SubScenarioType = 69 // invalid id + }); + user.Dispose(); + }); } return sb.ToString(); }); -CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); }); -CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); }); +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("send", args => { const string optionUsage = "Usage: send "; @@ -577,16 +562,12 @@ await server.Broadcast(new TagPacket { }); CommandHandler.RegisterCommand("shine", args => { - const string optionUsage = "Valid options: list, clear, sync, send, set, include, exclude"; + const string optionUsage = "Valid options: list, clear, sync, send, set"; if (args.Length < 1) return optionUsage; switch (args[0]) { case "list" when args.Length == 1: - return $"Shines: {string.Join(", ", shineBag)}" + ( - Settings.Instance.Shines.Excluded.Count() > 0 - ? "\nExcluded Shines: " + string.Join(", ", Settings.Instance.Shines.Excluded) - : "" - ); + return $"Shines: {string.Join(", ", shineBag)}"; case "clear" when args.Length == 1: shineBag.Clear(); Task.Run(async () => { @@ -623,21 +604,6 @@ await c.Send(new ShinePacket { return optionUsage; } - case "exclude" when args.Length == 2: - case "include" when args.Length == 2: { - if (int.TryParse(args[1], out int sid)) { - if (args[0] == "exclude") { - Settings.Instance.Shines.Excluded.Add(sid); - Settings.SaveSettings(); - return $"Exclude shine {sid} from syncing."; - } else { - Settings.Instance.Shines.Excluded.Remove(sid); - Settings.SaveSettings(); - return $"No longer exclude shine {sid} from syncing."; - } - } - return optionUsage; - } default: return optionUsage; } @@ -685,7 +651,7 @@ await c.Send(new ShinePacket { } } } -}).ContinueWith(logError); +}).ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 await server.Listen(cts.Token); diff --git a/Server/Server.cs b/Server/Server.cs index ef05d8a..30646d7 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -30,7 +30,6 @@ public async Task Listen(CancellationToken? token = null) { Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}"); - // start sub thread to handle client try { #pragma warning disable CS4014 Task.Run(() => HandleSocket(socket)) @@ -64,7 +63,7 @@ public async Task Listen(CancellationToken? token = null) { 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..]); } @@ -73,29 +72,27 @@ public static void FillPacket(PacketHeader header, T packet, Memory mem public delegate void PacketReplacer(Client from, Client to, T value); // replacer must send public void BroadcastReplace(T packet, Client sender, PacketReplacer packetReplacer) where T : struct, IPacket { - foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.Id)) { - packetReplacer(sender, client, packet); - } + foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet); } public async Task Broadcast(T packet, Client sender) where T : struct, IPacket { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = sender?.Id ?? Guid.Empty, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size, + Id = sender?.Id ?? Guid.Empty, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size }; FillPacket(header, packet, memory.Memory); await Broadcast(memory, sender); } public Task Broadcast(T packet) where T : struct, IPacket { - return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).Select(async client => { + return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = client.Id, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size, + Id = client.Id, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size }; FillPacket(header, packet, memory.Memory); await client.Send(memory.Memory, client); @@ -109,7 +106,7 @@ public Task Broadcast(T packet) where T : struct, IPacket { /// Memory owner to dispose once done /// Optional sender to not broadcast data to public async Task Broadcast(IMemoryOwner data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory, sender))); data.Dispose(); } @@ -119,7 +116,7 @@ public async Task Broadcast(IMemoryOwner data, Client? sender = null) { /// Memory to send to the clients /// Optional sender to not broadcast data to public async void Broadcast(Memory data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data, sender))); } public Client? FindExistingClient(Guid id) { @@ -131,7 +128,9 @@ private async void HandleSocket(Socket socket) { Client client = new Client(socket) {Server = this}; var remote = socket.RemoteEndPoint; IMemoryOwner memory = null!; - + await client.Send(new InitPacket { + MaxPlayers = Settings.Instance.Server.MaxPlayers + }); bool first = true; try { while (true) { @@ -154,9 +153,8 @@ async Task Read(Memory readMem, int readSize, int readOffset) { return true; } - if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) { + if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) break; - } PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]); Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize); if (header.PacketSize > 0) { @@ -164,100 +162,54 @@ async Task Read(Memory readMem, int readSize, int readOffset) { memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize); memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]); memTemp.Dispose(); - if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) { + if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) break; - } } // connection initialization if (first) { - first = false; // only do this once - - // first client packet has to be the client init - if (header.Type != PacketType.Connect) { - throw new Exception($"First packet was not init, instead it was {header.Type} ({remote})"); - } + first = false; + if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}"); ConnectPacket connect = new ConnectPacket(); connect.Deserialize(memory.Memory.Span[packetRange]); - - client.Id = header.Id; - client.Name = connect.ClientName; - - // is the IPv4 address banned? - if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { - Logger.Warn($"Ignoring banned IPv4 address for {client.Name} ({client.Id}/{remote})"); - client.Ignored = true; - client.Banned = true; - } - // is the profile ID banned? - else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) { - client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})"); - client.Ignored = true; - client.Banned = true; - } - // is the server full? - else if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { - client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); - client.Ignored = true; - } - - // send server init (required to crash ignored players later) - await client.Send(new InitPacket { - MaxPlayers = (client.Ignored ? (ushort) 1 : Settings.Instance.Server.MaxPlayers), - }); - - // don't init or announce an ignored client to other players any further - if (client.Ignored) { - memory.Dispose(); - continue; - } - - bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; - - // add client to the set of connected players lock (Clients) { - // is the server full? (check again, to prevent race conditions) - if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { - client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); - client.Ignored = true; + if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { + client.Logger.Error($"Turned away as server is at max clients"); memory.Dispose(); - continue; + goto disconnect; } - // detect and handle reconnections - bool isClientNew = true; + bool firstConn = true; switch (connect.ConnectionType) { case ConnectPacket.ConnectionTypes.FirstConnection: case ConnectPacket.ConnectionTypes.Reconnecting: { - if (FindExistingClient(client.Id) is { } oldClient) { - isClientNew = false; + client.Id = header.Id; + if (FindExistingClient(header.Id) is { } oldClient) { + firstConn = false; client = new Client(oldClient, socket); - client.Name = connect.ClientName; Clients.Remove(oldClient); Clients.Add(client); if (oldClient.Connected) { oldClient.Logger.Info($"Disconnecting already connected client {oldClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}"); oldClient.Dispose(); } - } - else { + } else { connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection; } break; } - default: { - throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})"); - } + default: + throw new Exception($"Invalid connection type {connect.ConnectionType}"); } + client.Name = connect.ClientName; client.Connected = true; - - if (isClientNew) { + if (firstConn) { // do any cleanup required when it comes to new clients - List toDisconnect = Clients.FindAll(c => c.Id == client.Id && c.Connected && c.Socket != null); - Clients.RemoveAll(c => c.Id == client.Id); + List toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null); + Clients.RemoveAll(c => c.Id == header.Id); client.Id = header.Id; Clients.Add(client); @@ -267,35 +219,26 @@ await client.Send(new InitPacket { ClientJoined?.Invoke(client, connect); } - // a known client reconnects, but with a new first connection (e.g. after a restart) - else if (wasFirst) { - client.CleanMetadataOnNewConnection(); - } } - // for all other clients that are already connected - List otherConnectedPlayers = Clients.FindAll(c => c.Id != client.Id && c.Connected && c.Socket != null); + 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)]); @@ -303,19 +246,10 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } tempBuffer.Dispose(); - - // make the other client reset their puppet cache for this new 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) { + Logger.Info($"Client {client.Name} ({client.Id}/{socket.RemoteEndPoint}) connected."); + } 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}"); } @@ -328,13 +262,9 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } try { - // parse the packet IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!; packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]); - - // process the packet if (PacketHandler?.Invoke(client, packet) is false) { - // don't broadcast the packet to everyone memory.Dispose(); continue; } @@ -342,9 +272,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { client.Logger.Error($"Packet handler warning: {e}"); } - #pragma warning disable CS4014 - // broadcast the packet to everyone Broadcast(memory, client) .ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 @@ -353,8 +281,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) { client.Logger.Info($"Disconnected from the server: Connection reset"); - } - else { + } else { client.Logger.Error($"Disconnecting due to exception: {e}"); if (socket.Connected) { #pragma warning disable CS4014 @@ -367,22 +294,10 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { memory?.Dispose(); } -<<<<<<< HEAD disconnect: Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server"); // Clients.Remove(client) -======= - // client disconnected - if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) { - Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server"); - } - else { - Logger.Info($"Client {remote} disconnected from the server"); - } - - bool wasConnected = client.Connected; ->>>>>>> Sanae6-master client.Connected = false; try { client.Dispose(); @@ -395,42 +310,6 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { #pragma warning restore CS4014 } - private async Task ResendPackets(Client client) { - async Task trySendPack(Client other, T? packet) where T : struct, IPacket { - if (packet == null) { return; } - try { - await client.Send((T) packet, other); - } - catch { - // lol who gives a fuck - } - }; - async Task trySendMeta(Client other, string packetType) where T : struct, IPacket { - if (!other.Metadata.ContainsKey(packetType)) { return; } - await trySendPack(other, (T) other.Metadata[packetType]!); - }; - await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => { - if (client.Id == other.Id) { return; } - await trySendMeta(other, "lastCostumePacket"); - await trySendMeta(other, "lastCapturePacket"); - await trySendPack(other, other.GetTagPacket()); - await trySendMeta(other, "lastGamePacket"); - await trySendMeta(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 f696a7d..fe075e5 100644 --- a/Server/Settings.cs +++ b/Server/Settings.cs @@ -30,10 +30,10 @@ public static void LoadSettings() { LoadHandler?.Invoke(); } - public static void SaveSettings(bool silent = false) { + public static void SaveSettings() { try { File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy()))); - if (!silent) { Logger.Info("Saved settings to settings.json"); } + Logger.Info("Saved settings to settings.json"); } catch (Exception e) { Logger.Error($"Failed to save settings.json {e}"); @@ -43,7 +43,7 @@ public static void SaveSettings(bool silent = false) { public ServerTable Server { get; set; } = new ServerTable(); public FlipTable Flip { get; set; } = new FlipTable(); public ScenarioTable Scenario { get; set; } = new ScenarioTable(); - public BanListTable BanList { get; set; } = new BanListTable(); + public BannedPlayers BanList { get; set; } = new BannedPlayers(); public DiscordTable Discord { get; set; } = new DiscordTable(); public ShineTable Shines { get; set; } = new ShineTable(); public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable(); @@ -58,16 +58,15 @@ public class ScenarioTable { public bool MergeEnabled { get; set; } = false; } - public class BanListTable { + public class BannedPlayers { public bool Enabled { get; set; } = false; - public ISet Players { get; set; } = new SortedSet(); - public ISet IpAddresses { get; set; } = new SortedSet(); - public ISet Stages { get; set; } = new SortedSet(); + public List Players { get; set; } = new List(); + public List IpAddresses { get; set; } = new List(); } public class FlipTable { public bool Enabled { get; set; } = true; - public ISet Players { get; set; } = new SortedSet(); + public List Players { get; set; } = new List(); public FlipOptions Pov { get; set; } = FlipOptions.Both; } @@ -80,7 +79,6 @@ public class DiscordTable { public class ShineTable { public bool Enabled { get; set; } = true; - public ISet Excluded { get; set; } = new SortedSet { 496 }; } public class PersistShinesTable @@ -88,4 +86,4 @@ public class PersistShinesTable 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 04315d5..5eb03b4 100644 --- a/Shared/Stages.cs +++ b/Shared/Stages.cs @@ -11,7 +11,7 @@ public static class Stages { return mapName; } // exact stage value - if (IsStage(input)) { + if (Stage2Alias.ContainsKey(input)) { return input; } // force input value with a ! @@ -29,32 +29,6 @@ public static string KingdomAliasMapping() { 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" }, diff --git a/docker-build.sh b/docker-build.sh deleted file mode 100755 index d1b9d07..0000000 --- a/docker-build.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -euo pipefail - -if [[ "$#" == "0" ]] || [[ "$#" > "1" ]] || ! [[ "$1" =~ ^(all|x64|arm|arm64|win64)$ ]] ; then - echo "Usage: docker-build.sh {all|x64|arm|arm64|win64}" - exit 1 -fi - -DIR=$(dirname "$(realpath $0)") -cd "$DIR" - -declare -A archs=( - ["x64"]="linux-x64" - ["arm"]="linux-arm" - ["arm64"]="linux-arm64" - ["win64"]="win-x64" -) - -for sub in "${!archs[@]}" ; do - arch="${archs[$sub]}" - - if [[ "$1" != "all" ]] && [[ "$1" != "$sub" ]] ; then - continue - fi - - docker run \ - -u `id -u`:`id -g` \ - -v "/$DIR/"://app/ \ - -w //app/ \ - -e DOTNET_CLI_HOME=//app/cache/ \ - -e XDG_DATA_HOME=//app/cache/ \ - mcr.microsoft.com/dotnet/sdk:6.0 \ - dotnet publish \ - ./Server/Server.csproj \ - -r $arch \ - -c Release \ - -o /app/bin/$sub/ \ - --self-contained \ - -p:publishSingleFile=true \ - ; - - filename="Server" - ext="" - if [[ "$sub" == "arm" ]] ; then filename="Server.arm"; - elif [[ "$sub" == "arm64" ]] ; then filename="Server.arm64"; - elif [[ "$sub" == "win64" ]] ; then filename="Server.exe"; ext=".exe"; - fi - - mv ./bin/$sub/Server$ext ./bin/$filename - rm -rf ./bin/$sub/ -done