namespace Aeshnidae.CombatWindow; /// /// `combatwindow-watch <player> [minutes]` (Admin, console-safe): every attack on or by that /// player is written to the server journal, one line each with ACE's own numbers - the land /// chance and the two skills behind it, then base damage and every multiplier - until the time /// runs out (default 10 minutes) or 400 lines. `combatwindow-watch off` stops. Nothing is sent to /// the player and the fight is not touched. Added 2026-09-29 for "these Olthoi are hitting too /// hard" (Matron Hive South): numbers instead of a feeling. ACE's own /debugdamage shows the same /// breakdown, but only in game, to the admin, 25 lines a hit. /// [HarmonyPatch] public static class Watch { private const int MaxLines = 400; private static uint _guid; private static string _name = ""; private static DateTime _until; private static int _lines; public static void Start(Player player, double minutes) { _name = player.Name; _until = DateTime.UtcNow.AddMinutes(minutes); _lines = 0; _guid = player.Guid.Full; } public static void Stop() => _guid = 0; public static string Status => _guid == 0 ? "not watching anyone" : $"watching {_name} until {_until:HH:mm} UTC - {_lines} line(s) so far"; // On the private DoCalculateDamage, not the public static CalculateDamage around it: that // wrapper is small enough for the JIT to inline into its callers, and a patch on an inlined // method never runs (the first version, 2026-09-29, logged nothing while Lucy fought). [HarmonyPostfix] [HarmonyPatch(typeof(DamageEvent), "DoCalculateDamage")] public static void PostCalculateDamage(DamageEvent __instance, Creature attacker, Creature defender) { var __result = __instance; if (_guid == 0 || __result is null) return; try { if (defender?.Guid.Full != _guid && attacker?.Guid.Full != _guid) return; if (DateTime.UtcNow > _until || _lines >= MaxLines) { ModManager.Log($"[{Mod.Name}] watch on {_name} ended ({_lines} line(s))"); _guid = 0; return; } _lines++; var e = __result; var who = $"{attacker?.Name} -> {defender?.Name}"; var odds = $"land {1 - e.EvasionChance:P0} (attack {e.EffectiveAttackSkill} vs defense {e.EffectiveDefenseSkill})"; var line = e.Evaded ? $"{who}: evaded, {odds}" : $"{who}: {e.Damage:0} {e.DamageType} to {e.BodyPart}{(e.IsCritical ? " CRIT" : "")} = base {e.BaseDamage:0.#}" + $" x attribute {e.AttributeMod:0.###} x rating {e.DamageRatingMod:0.###} = {e.DamageBeforeMitigation:0} before" + $" armor {e.ArmorMod:0.###} x resist {e.ResistanceMod:0.###} x shield {e.ShieldMod:0.###} x DR {e.DamageResistanceRatingMod:0.###};" + $" {odds}; {defender?.Name} had {defender?.Health.Current}/{defender?.Health.MaxValue}"; ModManager.Log($"[{Mod.Name}] watch: {line}"); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] watch failed: {ex.Message}", ModManager.LogLevel.Warn); } } }