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);
}
}
}