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; 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, _) => { c.Metadata["shineSync"] = new ConcurrentBag(); c.Metadata["loadedSave"] = false; c.Metadata["scenario"] = (byte?) 0; c.Metadata["2d"] = false; c.Metadata["speedrun"] = false; }; 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).Except(Settings.Instance.Shines.Excluded).ToArray()) { if (!client.Connected) return; await client.Send(new ShinePacket { ShineId = shine }); clientBag.Add(shine); } } catch { // errors that can happen when sending will crash the server :) } } async void SyncShineBag() { try { await PersistShines(); await Parallel.ForEachAsync(server.ClientsConnected.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; 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; ((ConcurrentBag) (c.Metadata["shineSync"] ??= new ConcurrentBag())).Clear(); shineBag.Clear(); 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(logError); #pragma warning restore CS4014 }); return false; } 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: { 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); if (playerBag.Contains(shinePacket.ShineId)) break; c.Logger.Info($"Got moon {shinePacket.ShineId}"); playerBag.Add(shinePacket.ShineId); SyncShineBag(); 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); #pragma warning disable CS4014 server.Broadcast(playerPacket, c).ContinueWith(logError); #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); } #pragma warning disable CS4014 to.Send(sp, from).ContinueWith(logError); #pragma warning restore CS4014 }); return false; } break; } } return true; // Broadcast packet to all other clients }; (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) || (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] == "!*") { 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) { BanLists.Crash(user); } 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 "; if (args.Length < 4) { return optionUsage; } string? stage = Stages.Input2Stage(args[0]); if (stage == null) { return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```"; } string id = args[1]; 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 = Stages.Input2Stage(args[0]); if (stage == null) { return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```"; } 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