using System.Numerics; namespace Aeshnidae.Invasions; /// /// /invasion for players; the rest are admin and console-safe (the remote console runs them). /// Anything that changes the world is queued for the world thread (Invasions.Enqueue) and /// answers from there. /// public static class Commands { [CommandHandler("invasion", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, 0, "Town invasions: which town is under attack, how the defence is going, or when the next is expected.", "/invasion")] public static void HandleInvasion(Session session, params string[] parameters) { if (!Mod.Settings.Enabled) { Reply(session, "Town invasions are switched off."); return; } Reply(session, Invasions.Describe(detail: false)); } [CommandHandler("invasion-start", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Start an invasion of a town now (ignores the town's cooldown and the player count).", "/invasion-start [warning minutes] default warning: the settings' WarningMinutes; 0 = the siege at once")] public static void HandleStart(Session session, params string[] parameters) { var args = parameters.ToList(); double? minutes = null; if (args.Count > 1 && double.TryParse(args[^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var m) && m >= 0) { minutes = m; args.RemoveAt(args.Count - 1); } var wanted = string.Join(" ", args); var town = Mod.Settings.FindTown(wanted); if (town is null) { Reply(session, $"No town matches '{wanted}'. Towns: {string.Join(", ", Mod.Settings.Towns.Select(t => t.Name))}."); return; } var by = session?.Player?.Name ?? "the console"; var warning = TimeSpan.FromMinutes(minutes ?? Mod.Settings.WarningMinutes); Invasions.Enqueue(() => Reply(session, Invasions.Begin(town, by, warning))); } [CommandHandler("invasion-stop", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Call off the running invasion: the invaders are removed and nobody is paid.", "/invasion-stop")] public static void HandleStop(Session session, params string[] parameters) { var by = session?.Player?.Name ?? "the console"; Invasions.Enqueue(() => Reply(session, Invasions.Stop(by))); } [CommandHandler("invasion-status", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Town invasions in detail: the timer, every town and why it can or cannot be chosen, the running invasion and its defenders.", "/invasion-status")] public static void HandleStatus(Session session, params string[] parameters) { Invasions.Enqueue(() => Reply(session, Invasions.Describe(detail: true))); } [CommandHandler("invasion-next", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Set when the timer next looks for a town to invade.", "/invasion-next (the timer must be on: AutoSchedule)")] public static void HandleNext(Session session, params string[] parameters) { if (!double.TryParse(parameters[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var minutes) || minutes < 0) { Reply(session, "Usage: /invasion-next "); return; } var when = DateTime.UtcNow.AddMinutes(minutes); Invasions.Enqueue(() => { Invasions.SetNext(when); Reply(session, $"The timer next looks at {when:HH:mm:ss} UTC{(Mod.Settings.AutoSchedule ? "" : " - but AutoSchedule is off, so it will not fire")}."); }); } [CommandHandler("invasion-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Re-read the invasions' Settings.json. A running invasion keeps its town and waves.", "/invasion-reload")] public static void HandleReload(Session session, params string[] parameters) { Invasions.Enqueue(() => { Mod.ReloadSettings(); Reply(session, $"Settings re-read: {Mod.Settings.Towns.Count} town(s), {Mod.Settings.WaveSets.Count} wave set(s), enabled {Mod.Settings.Enabled}, timer {(Mod.Settings.AutoSchedule ? "on" : "off")}."); }); } [CommandHandler("invasion-smite", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Testing: kill the standing invaders, as /smite does (nobody is credited).", "/invasion-smite every standing invader\n" + "/invasion-smite that many of them (the boss last)")] public static void HandleSmite(Session session, params string[] parameters) { int? count = parameters.Length > 0 && int.TryParse(parameters[0], out var n) && n > 0 ? n : null; Invasions.Enqueue(() => Reply(session, Invasions.Smite(count))); } [CommandHandler("invasion-skip", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Testing: end the warning (the siege starts) or the siege (the boss arrives) now.", "/invasion-skip")] public static void HandleSkip(Session session, params string[] parameters) { Invasions.Enqueue(() => Reply(session, Invasions.Skip())); } [CommandHandler("invasion-fx", AccessLevel.Admin, CommandHandlerFlag.RequiresWorld, 1, "Preview a client effect (an ACE PlayScript name) on yourself - to choose the invasion effects.", "/invasion-fx e.g. PortalExit, PortalStorm, WeddingBliss, LevelUp, BaelZharonSmite\n" + "/invasion-fx list every effect name")] public static void HandleFx(Session session, params string[] parameters) { var name = string.Join("", parameters).Trim(); if (name.Equals("list", StringComparison.OrdinalIgnoreCase)) { Reply(session, string.Join(", ", Enum.GetNames().Where(n => n != nameof(PlayScript.Invalid)))); return; } if (Effects.Parse(name) is null) { Reply(session, $"'{name}' is not an effect. /invasion-fx list shows them all."); return; } var player = session.Player; Invasions.Enqueue(() => Reply(session, Effects.Play(player, name) ? $"Playing {name} on you." : $"Could not play {name}.")); } [CommandHandler("invasion-fireworks", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Preview the victory fireworks: round where you stand, or round a town's centre.", "/invasion-fireworks round you\n" + "/invasion-fireworks round that town's centre (console-safe)")] public static void HandleFireworks(Session session, params string[] parameters) { Vector2? center; string where; if (parameters.Length > 0) { var town = Mod.Settings.FindTown(string.Join(" ", parameters)); center = town is null ? null : Placement.ParseMapCoords(town.Center); where = town?.Name ?? ""; } else { center = Placement.Global(session?.Player?.Location); where = "you"; } if (center is null) { Reply(session, parameters.Length > 0 ? "No such town." : "Stand outdoors, or name a town."); return; } var at = center.Value; Invasions.Enqueue(() => Reply(session, $"{Effects.Celebrate(at)} firework display(s) round {where}, for {Mod.Settings.CelebrationSeconds:0} s.")); } [CommandHandler("invasion-here", AccessLevel.Admin, CommandHandlerFlag.RequiresWorld, 0, "For writing a town entry: your map coordinates, and how far you stand from each town's centre.", "/invasion-here")] public static void HandleHere(Session session, params string[] parameters) { var player = session.Player; var here = Placement.Global(player?.Location); if (here is null) { Reply(session, "You are indoors; a town centre has to be outdoors."); return; } var sb = new StringBuilder(); sb.AppendLine($"You are at {Placement.MapText(here.Value)} ({player!.Location.ToLOCString()})."); foreach (var t in Mod.Settings.Towns) { var c = Placement.ParseMapCoords(t.Center); sb.AppendLine(c is null ? $" {t.Name}: centre '{t.Center}' does not parse" : $" {t.Name} ({t.Center}): {Vector2.Distance(c.Value, here.Value):0} m from its centre (safe {t.SafeRadius:0}, spawn {t.SpawnMin:0}-{t.SpawnMax:0}, town {t.TownRadius:0})"); } Reply(session, sb.ToString()); } private static void Reply(Session? session, string message) { if (session?.Player is null) { foreach (var line in (message ?? "").Split('\n')) if (!string.IsNullOrWhiteSpace(line)) ModManager.Log(line.TrimEnd('\r')); return; } foreach (var line in (message ?? "").Split('\n')) { var text = line.TrimEnd('\r'); if (!string.IsNullOrWhiteSpace(text)) session.Player.SendMessage(text, ChatMessageType.Broadcast); } } }