namespace Aeshnidae.CombatWindow; /// /// Chat / console commands added by this mod. /// /// ModContainer scans the assembly for [CommandHandler] methods and registers them /// when the mod is enabled (Meta.json "RegisterCommands": true), then removes them /// again on Disable. Handlers must be public static and match ACE's CommandHandler /// delegate exactly: (Session session, params string[] parameters). /// /// Putting a [CommandCategory("...")] attribute on this class would exclude it from /// automatic registration, leaving it for ModContainer.RegisterCommandCategory(). /// public static class Commands { [CommandHandler("combatwindow", AccessLevel.Player, CommandHandlerFlag.None, -1, "The evade and resist window in force, and your chance to land at a given skill gap.", "/combatwindow the terms\n" + "/combatwindow chance to land and to resist at that gap, e.g. /combatwindow -50")] public static void HandleStatus(Session session, params string[] parameters) { var s = Mod.Settings; var sb = new StringBuilder(); if (!s.Enabled) { Reply(session, "Combat window: off - retail's curve (slope 0.03, no floor, no ceiling)."); return; } sb.AppendLine($"Combat window: evade slope {s.EvadeSlope:0.###} (retail 0.03), land chance clamped {s.MinLandChance:P0}-{s.MaxLandChance:P0}; " + $"resist slope {s.ResistSlope:0.###}, spell land chance clamped {s.MinSpellLandChance:P0}-{s.MaxSpellLandChance:P0}."); if (parameters.Length > 0 && int.TryParse(parameters[0], out var gap)) { sb.AppendLine($" attack {gap:+#;-#;0} over defense: melee/missile lands {s.LandChance(gap, 0, false):P1} (retail {Retail(gap):P1}); " + $"a spell lands {s.LandChance(gap, 0, true):P1} (retail {Retail(gap):P1})."); } else { foreach (var g in new[] { -150, -100, -50, 0, 50, 100, 150 }) sb.AppendLine($" gap {g,4:+#;-#;0}: lands {s.LandChance(g, 0, false),6:P1} retail {Retail(g),6:P1}"); } Reply(session, sb.ToString().TrimEnd()); } private static double Retail(int gap) => Math.Clamp(1.0 - 1.0 / (1.0 + Math.Exp(0.03 * gap)), 0, 1); [CommandHandler("combatwindow-watch", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Write every attack on or by an online player to the server journal, with ACE's numbers (Watch.cs). Console-safe.", "/combatwindow-watch [minutes] watch for that long (default 10)\n" + "/combatwindow-watch off stop\n" + "/combatwindow-watch who is being watched")] public static void HandleWatch(Session session, params string[] parameters) { if (parameters.Length == 0) { Reply(session, Watch.Status); return; } if (parameters.Length == 1 && parameters[0].Equals("off", StringComparison.OrdinalIgnoreCase)) { Watch.Stop(); Reply(session, "watch stopped"); return; } var minutes = 10.0; var nameParts = parameters; if (parameters.Length > 1 && double.TryParse(parameters[^1], out var m) && m > 0) { minutes = Math.Min(m, 120); nameParts = parameters[..^1]; } var name = string.Join(" ", nameParts).Trim(); var player = PlayerManager.GetOnlinePlayer(name) ?? PlayerManager.GetOnlinePlayer("+" + name); if (player is null) { Reply(session, $"{name} is not online."); return; } Watch.Start(player, minutes); Reply(session, $"{Watch.Status} - lines go to the server journal, prefixed [{Mod.Name}] watch:"); } [CommandHandler("combatwindow-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Restart the Aeshnidae.CombatWindow mod, re-reading Settings.json.", "/combatwindow-reload")] public static void HandleReload(Session session, params string[] parameters) { var container = Mod.Container; if (container is null) { Reply(session, $"{Mod.Name} is not loaded."); return; } container.Restart(); Reply(session, $"{Mod.Name} restarted."); } /// /// The client renders an embedded newline as a music note, so a multi-line /// message has to go out as one SendMessage per line. /// private static void Reply(Session? session, string message) { if (session?.Player is null) { ModManager.Log(message); return; } foreach (var line in (message ?? "").Split('\n')) { var text = line.TrimEnd('\r'); if (!string.IsNullOrWhiteSpace(text)) session.Player.SendMessage(text); } } }