using System.Collections.Concurrent; using System.Net; using System.Numerics; using System.Text; using System.Text.Json; using Server; using Shared; using Shared.Packet.Packets; using Timer = System.Timers.Timer; Server.Server server = new Server.Server(); HashSet shineBag = new HashSet(); CancellationTokenSource cts = new CancellationTokenSource(); bool restartRequested = false; Task listenTask = server.Listen(cts.Token); Logger consoleLogger = new Logger("Console"); DiscordBot bot = new DiscordBot(); await bot.Run(); async Task PersistShines() { if (!Settings.Instance.PersistShines.Enabled) { return; } try { string shineJson = JsonSerializer.Serialize(shineBag); await File.WriteAllTextAsync(Settings.Instance.PersistShines.Filename, shineJson); } catch (Exception ex) { consoleLogger.Error(ex); } } async Task LoadShines() { if (!Settings.Instance.PersistShines.Enabled) { return; } try { string shineJson = await File.ReadAllTextAsync(Settings.Instance.PersistShines.Filename); var loadedShines = JsonSerializer.Deserialize>(shineJson); if (loadedShines is not null) shineBag = loadedShines; } catch (FileNotFoundException) { // Ignore } catch (Exception ex) { consoleLogger.Error(ex); } } // Load shines table from file 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) { if (!Settings.Instance.Shines.Enabled) return; try { if ((bool?) client.Metadata["speedrun"] ?? false) return; ConcurrentBag clientBag = (ConcurrentBag) (client.Metadata["shineSync"] ??= new ConcurrentBag()); foreach (int shine in shineBag.Except(clientBag).ToArray()) { clientBag.Add(shine); await client.Send(new ShinePacket { ShineId = shine }); } } catch { // errors that can happen when sending will crash the server :) } } async void SyncShineBag() { try { await PersistShines(); await Parallel.ForEachAsync(server.Clients.ToArray(), async (client, _) => await ClientSyncShineBag(client)); } catch { // errors that can happen shines change will crash the server :) } } Timer timer = new Timer(120000); timer.AutoReset = true; timer.Enabled = true; timer.Elapsed += (_, _) => { SyncShineBag(); }; timer.Start(); float MarioSize(bool is2d) => is2d ? 180 : 160; server.PacketHandler = (c, p) => { switch (p) { case GamePacket gamePacket: { c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); 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; ((ConcurrentBag) (c.Metadata["shineSync"] ??= new ConcurrentBag())).Clear(); shineBag.Clear(); Task.Run(async () => { await PersistShines(); }); c.Logger.Info("Entered Cap on new save, preventing moon sync until Cascade"); break; case "WaterfallWorldHomeStage": bool wasSpeedrun = (bool) c.Metadata["speedrun"]!; c.Metadata["speedrun"] = false; if (wasSpeedrun) Task.Run(async () => { c.Logger.Info("Entered Cascade with moon sync disabled, enabling moon sync"); await Task.Delay(15000); await ClientSyncShineBag(c); }); break; } if (Settings.Instance.Scenario.MergeEnabled) { 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()); } }); #pragma warning restore CS4014 }); return false; } break; } case TagPacket 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 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 (c.Metadata["loadedSave"] is false) break; ConcurrentBag playerBag = (ConcurrentBag)c.Metadata["shineSync"]!; shineBag.Add(shinePacket.ShineId); if (playerBag.Contains(shinePacket.ShineId)) break; c.Logger.Info($"Got moon {shinePacket.ShineId}"); playerBag.Add(shinePacket.ShineId); SyncShineBag(); 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)); #pragma warning disable CS4014 server.Broadcast(playerPacket, c) .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #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)); } #pragma warning disable CS4014 to.Send(sp, from) .ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 }); return false; } } return true; }; (HashSet failToFind, HashSet toActUpon, List<(string arg, IEnumerable amb)> ambig) MultiUserCommandHelper(string[] args) { HashSet failToFind = new(); HashSet toActUpon; List<(string arg, IEnumerable amb)> ambig = new(); 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()) 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] == "!*") toActUpon.Remove(exact); else toActUpon.Add(exact); } else { 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 toActUpon.Remove(rem); } } else { //only one match, so autocomplete if (args[0] == "!*") toActUpon.Remove(search.First()); else toActUpon.Add(search.First()); } } } return (failToFind, toActUpon, ambig); } CommandHandler.RegisterCommand("rejoin", args => { if (args.Length == 0) { return "Usage: rejoin <* | !* (usernames to not rejoin...) | (usernames to rejoin...)>"; } var res = MultiUserCommandHelper(args); StringBuilder sb = new StringBuilder(); sb.Append(res.toActUpon.Count > 0 ? "Rejoined: " + 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.Dispose(); } return sb.ToString(); }); CommandHandler.RegisterCommand("crash", args => { if (args.Length == 0) { return "Usage: crash <* | !* (usernames to not crash...) | (usernames to crash...)>"; } var res = MultiUserCommandHelper(args); StringBuilder sb = new StringBuilder(); sb.Append(res.toActUpon.Count > 0 ? "Crashed: " + 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 = "$among$us/SubArea", Stage = "$agogusStage", Scenario = 21, SubScenarioType = 69 // invalid id }); user.Dispose(); }); } 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("send", args => { const string optionUsage = "Usage: send "; if (args.Length < 4) return optionUsage; string stage = args[0]; string id = args[1]; if (Constants.MapNames.TryGetValue(stage.ToLower(), out string? mapName)) { stage = mapName; } if (!stage.Contains("Stage") && !stage.Contains("Zone")) { return "Invalid Stage Name! ```cap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side```"; } if (!sbyte.TryParse(args[2], out sbyte scenario) || scenario < -1) return $"Invalid scenario number {args[2]} (range: [-1 to 127])"; Client[] players = args[3] == "*" ? server.Clients.Where(c => c.Connected).ToArray() : server.Clients.Where(c => c.Connected && args[3..].Any(x => c.Name.StartsWith(x) || (Guid.TryParse(x, out Guid result) && result == c.Id))) .ToArray(); Parallel.ForEachAsync(players, async (c, _) => { await c.Send(new ChangeStagePacket { Stage = stage, Id = id, Scenario = scenario, SubScenarioType = 0 }); }).Wait(); return $"Sent players to {stage}:{scenario}"; }); CommandHandler.RegisterCommand("sendall", args => { const string optionUsage = "Usage: sendall "; if (args.Length < 1) return optionUsage; string stage = args[0]; if (Constants.MapNames.TryGetValue(stage.ToLower(), out string? mapName)) { stage = mapName; } if (!stage.Contains("Stage") && !stage.Contains("Zone")) { return "Invalid Stage Name! ```cap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side```"; } Client[] players = server.Clients.Where(c => c.Connected).ToArray(); Parallel.ForEachAsync(players, async (c, _) => { await c.Send(new ChangeStagePacket { Stage = stage, Id = "", Scenario = -1, SubScenarioType = 0 }); }).Wait(); return $"Sent players to {stage}:{-1}"; }); CommandHandler.RegisterCommand("scenario", args => { const string optionUsage = "Valid options: merge [true/false]"; if (args.Length < 1) return optionUsage; switch (args[0]) { case "merge" when args.Length == 2: { if (bool.TryParse(args[1], out bool result)) { Settings.Instance.Scenario.MergeEnabled = result; Settings.SaveSettings(); return result ? "Enabled scenario merge" : "Disabled scenario merge"; } return optionUsage; } case "merge" when args.Length == 1: { return $"Scenario merging is {Settings.Instance.Scenario.MergeEnabled}"; } default: return optionUsage; } }); CommandHandler.RegisterCommand("tag", args => { const string optionUsage = "Valid options:\n\ttime \n\tseeking \n\tstart