namespace Aeshnidae.Trials; public static class Commands { [CommandHandler("trial", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, 0, "The Trials: timed solo runs of a fresh personal dungeon copy - the server record per tier, your best, this week's leader.", "/trial every Trial: records per tier, your best, the week's leader\n" + "/trial one dungeon in detail: the top 10, all tiers")] public static void HandleTrial(Session session, params string[] parameters) { var player = session.Player; if (player is null) return; var s = Mod.Settings; if (!s.Enabled) { Reply(session, "The Trials are switched off."); return; } if (!TrialDb.Ready) { Reply(session, "The Trials' records table is not open; nothing can be shown or recorded."); return; } if (parameters.Length > 0) { var wanted = string.Join(" ", parameters).Trim(); var match = Mod.FindTrial(wanted); if (match is null) { Reply(session, $"No Trial matches '{wanted}'. The Trials: {string.Join(", ", Mod.Trials.Keys.OrderBy(k => k).Select(Mod.TrialName))}."); return; } Detail(session, player, match.Value); return; } try { var sb = new StringBuilder(); var week = Weeks.KeyOf(DateTime.Now, s); var next = Weeks.NextAward(DateTime.Now, s); sb.AppendLine($"Trials - a fresh personal copy of a listed dungeon, entered through its portal, is a timed course: the clock runs until everything in it is dead, nothing respawns meanwhile. " + $"Leaving, dying, logging out or a teleport voids the run; one attempt per dungeon per {s.CooldownMinutes:0} minutes. " + $"{s.RadiancePerClear:N0} Radiance per clear (x{s.PersonalBestMultiplier:0.#} personal best, x{s.ServerRecordMultiplier:0.#} server record)."); var records = TrialDb.ServerBests(); var mine = TrialDb.PersonalBests(player.Guid.Full); foreach (var (landblock, _) in Mod.Trials.OrderBy(kv => kv.Key)) { var name = Mod.TrialName(landblock); var rows = records.Where(r => r.Landblock == landblock).OrderBy(r => r.Tier).ToList(); var yours = mine.Where(r => r.Landblock == landblock).OrderBy(r => r.Tier).ToList(); var leader = TrialDb.WeekBest(week, landblock); var recordText = rows.Count == 0 ? "no clear yet" : string.Join(", ", rows.Select(r => $"tier {r.Tier} {r.Time} by {r.CharacterName}")); var yourText = yours.Count == 0 ? "-" : string.Join(", ", yours.Select(r => $"tier {r.Tier} {r.Time}")); var weekText = leader is null ? "nobody yet" : $"{leader.CharacterName} {leader.Time} (tier {leader.Tier})"; sb.AppendLine($" {name} ({landblock:X4}): {recordText} | you: {yourText} | this week: {weekText}"); var wait = Runs.CooldownLeft(player, landblock); if (wait > TimeSpan.Zero) sb.AppendLine($" your next attempt here: in {Math.Ceiling(wait.TotalMinutes):0} min"); } var running = Runs.ByPlayer(player); if (running is not null) sb.AppendLine($" running now: {running.Name} tier {running.Tier}, {RunRow.Format(running.Elapsed)}, {(running.Counted ? $"{running.Alive.Count} left of {running.Initial}" : "counting")}"); sb.AppendLine($"Trophy: the week's fastest clear of each dungeon, any tier - a trophy and {s.WeeklyAwardXp:N0} experience, {next:dddd HH:mm} ({week}). /trial for the top 10 of one dungeon."); Reply(session, sb.ToString()); } catch (Exception ex) { Reply(session, $"The Trials could not be read: {ex.Message}"); } } private static void Detail(Session session, Player player, ushort landblock) { try { var s = Mod.Settings; var name = Mod.TrialName(landblock); var sb = new StringBuilder(); var week = Weeks.KeyOf(DateTime.Now, s); sb.AppendLine($"{name} ({landblock:X4}) - top 10, best per character, any tier:"); var top = TrialDb.Top(landblock, 10); if (top.Count == 0) sb.AppendLine(" no clear yet"); var rank = 0; foreach (var r in top) sb.AppendLine($" {++rank,2}. {r.Time} {r.CharacterName} (tier {r.Tier}, {r.Kills} kills, {r.FinishedAt.ToLocalTime():d MMM})"); var records = TrialDb.ServerBests().Where(r => r.Landblock == landblock).OrderBy(r => r.Tier).ToList(); if (records.Count > 0) sb.AppendLine(" records per tier: " + string.Join(", ", records.Select(r => $"tier {r.Tier} {r.Time} by {r.CharacterName}"))); var mine = TrialDb.PersonalBests(player.Guid.Full).Where(r => r.Landblock == landblock).OrderBy(r => r.Tier).ToList(); sb.AppendLine(" you: " + (mine.Count == 0 ? "no clear yet" : string.Join(", ", mine.Select(r => $"tier {r.Tier} {r.Time} ({r.Kills} kills)")))); var leader = TrialDb.WeekBest(week, landblock); sb.AppendLine($" this week ({week}): " + (leader is null ? "nobody yet" : $"{leader.CharacterName} {leader.Time} (tier {leader.Tier})")); Reply(session, sb.ToString()); } catch (Exception ex) { Reply(session, $"The Trials could not be read: {ex.Message}"); } } [CommandHandler("trial-award", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Run the weekly Trial award now for the week that last ended (or a named week). Safe to repeat: a week already awarded is skipped.", "/trial-award [2026-W38]")] public static void HandleAward(Session session, params string[] parameters) { if (!TrialDb.Ready) { Reply(session, "The Trials' records table is not open."); return; } var week = parameters.Length > 0 ? parameters[0].Trim() : Weeks.PreviousKey(DateTime.Now, Mod.Settings); if (!System.Text.RegularExpressions.Regex.IsMatch(week, @"^\d{4}-W\d{2}$")) { Reply(session, $"'{week}' is not a week key (2026-W38)."); return; } var by = session?.Player?.Name ?? "console"; var lines = Awards.AwardWeek(week, by); Reply(session, $"Trial award for {week} (week of {Weeks.Describe(week, Mod.Settings)}):\n " + string.Join("\n ", lines) + $"\nDiscord: {Discord.LastResult}"); ModManager.Log($"[{Mod.Name}] {by} ran the award for {week}: {string.Join("; ", lines)}"); } [CommandHandler("trial-void", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Void a player's running Trial.", "/trial-void ")] public static void HandleVoid(Session session, params string[] parameters) { var name = string.Join(" ", parameters).Trim('"'); var target = PlayerManager.GetOnlinePlayer(name); if (target is null) { Reply(session, $"'{name}' is not online."); return; } var by = session?.Player?.Name ?? "the console"; Reply(session, Runs.VoidFor(target, by) ? $"{target.Name}'s run is voided." : $"{target.Name} has no run going."); } [CommandHandler("trial-status", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Trials: bindings, runs in progress, the last award, the feed.", "/trial-status")] public static void HandleStatus(Session session, params string[] parameters) { var s = Mod.Settings; var sb = new StringBuilder(); sb.AppendLine($"{Mod.Name}: {(s.Enabled ? "on" : "off")}, table {(TrialDb.Ready ? "open" : "NOT open")}, InstancesNoDat {Instances.Status}, Bank {Bank.Status}, {Patches.Ticks:N0} world ticks seen"); sb.AppendLine($" trials: {string.Join(", ", Mod.Trials.OrderBy(kv => kv.Key).Select(kv => $"{Mod.TrialName(kv.Key)} ({kv.Key:X4}, {Instances.ScopeOf(kv.Key)})"))}"); sb.AppendLine($" week {Weeks.KeyOf(DateTime.Now, s)}, next award {Weeks.NextAward(DateTime.Now, s):yyyy-MM-dd HH:mm} local; last award: {Awards.LastResult}; Discord: {Discord.LastResult}"); sb.AppendLine($" feed: {(s.WriteJson ? $"{Feed.JsonPath} ({(Feed.LastWriteUtc == default ? "not written" : Feed.LastWriteUtc.ToString("HH:mm:ss") + " UTC")})" : "off")}"); var runs = Runs.Snapshot(); sb.AppendLine($" runs: {runs.Count}"); foreach (var r in runs) sb.AppendLine($" {r.Player.Name}: {r.Name} copy {r.Copy} tier {r.Tier}, {RunRow.Format(r.Elapsed)}, {(r.Counted ? $"{r.Alive.Count} left of {r.Initial}, {r.Kills} kills" : "not counted yet")}{(r.Staff ? " [staff]" : "")}"); Reply(session, sb.ToString()); } [CommandHandler("trial-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Re-read Settings.json for Aeshnidae.Trials without restarting it. Runs in progress are kept.", "/trial-reload")] public static void HandleReload(Session session, params string[] parameters) { try { Mod.ReloadSettings(); Reply(session, $"{Mod.Name}: settings re-read - {Mod.Trials.Count} trials, cooldown {Mod.Settings.CooldownMinutes:0} min, award {Mod.Settings.AwardDayOfWeek} {Mod.Settings.AwardHour:00}:00."); } catch (Exception ex) { Reply(session, $"{Mod.Name}: reload failed: {ex.Message}"); } } /// The client renders an embedded newline as a music note: one SendMessage per line. 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); } } }