namespace Aeshnidae.Healing; /// /// `healing selftest` (console, meant for staging): runs ACE's own heal-spell path on a creature that is /// never placed in the world, with retail rules and then with a factor of exactly 3, and checks the /// patches are attached. Heals must land at 3x retail's range, once per cast; harm must not change. /// The kits cannot be driven without a player online - their patches are only shown attached. /// public static class SelfTest { private const int Casts = 12; private const double TestFactor = 3; public static string Run(uint wcid) { var sb = new StringBuilder(); var saved = Mod.Settings; Creature? subject = null; try { foreach (var (label, method) in Targets()) { var owners = method is null ? null : Harmony.GetPatchInfo(method)?.Owners; sb.AppendLine($" {label}: {(owners?.Contains(Mod.HarmonyId) == true ? "patched" : "NOT PATCHED")}"); } sb.AppendLine($" HandleCastSpell_Boost calls rewritten: {Scaling.RewrittenCalls} (ACE has 3)"); subject = WorldObjectFactory.CreateNewWorldObject(wcid) as Creature; if (subject is null) return sb.AppendLine($" wcid {wcid} is not a creature").ToString(); // A big pool, so a tripled heal is never cut short by the missing amount. subject.Health.StartingValue = 3000; subject.Stamina.StartingValue = 3000; var boost = AccessTools.Method(typeof(WorldObject), "HandleCastSpell_Boost"); var boostResist = AccessTools.Method(typeof(WorldObject), "GetBoostResistanceType", new[] { typeof(DamageType) }); var drainResist = AccessTools.Method(typeof(WorldObject), "GetDrainResistanceType", new[] { typeof(DamageType) }); var retail = new Settings { Enabled = false }; var scaled = new Settings { Enabled = true, ScaleSpells = true, ScaleMonsterHeals = true, ReferenceHealth = subject.Health.MaxValue / TestFactor, ReferenceStamina = subject.Stamina.MaxValue / TestFactor, }; var pass = true; foreach (var id in new[] { SpellId.HealSelf6, SpellId.RevitalizeSelf6, SpellId.HarmSelf1 }) { var spell = new Spell(id); var heal = spell.Boost > 0; var vital = spell.VitalDamageType == DamageType.Stamina ? subject.Stamina : subject.Health; var resist = (ResistanceType)(heal ? boostResist : drainResist)!.Invoke(null, new object[] { spell.VitalDamageType })!; var mod = subject.GetResistanceMod(resist); var lo = Math.Round(Math.Min(spell.Boost, spell.MaxBoost) * mod); var hi = Math.Round(Math.Max(spell.Boost, spell.MaxBoost) * mod); if (heal) { lo *= TestFactor; hi *= TestFactor; } Mod.Settings = retail; var r = Cast(subject, vital, boost, spell, heal); Mod.Settings = scaled; var before = Scaling.SpellsScaled; var s = Cast(subject, vital, boost, spell, heal); var counted = Scaling.SpellsScaled - before; Mod.Settings = saved; var ok = s.All(d => d >= lo - 1 && d <= hi + 1) && counted == (heal ? Casts : 0); pass &= ok; sb.AppendLine($" {spell.Name}: retail {r.Min()}..{r.Max()}, with x{TestFactor} {s.Min()}..{s.Max()} " + $"(expected {lo}..{hi}), scaled {counted} of {Casts} - {(ok ? "ok" : "WRONG")}"); } sb.Insert(0, $"Healing self-test on {subject.Name} ({subject.Health.MaxValue} health, {subject.Stamina.MaxValue} stamina): {(pass ? "PASS" : "FAIL")}\n"); } catch (Exception ex) { sb.AppendLine($" self-test threw: {ex.Message}"); } finally { Mod.Settings = saved; try { subject?.Destroy(); } catch { } } return sb.ToString(); } // One cast at a time, from 1 point (a heal) or a full pool (harm), measuring the change. private static List Cast(Creature subject, CreatureVital vital, MethodInfo boost, Spell spell, bool heal) { var deltas = new List(); for (var i = 0; i < Casts; i++) { subject.UpdateVital(vital, heal ? 1 : (int)vital.MaxValue); var start = (int)vital.Current; boost.Invoke(subject, new object[] { spell, subject }); deltas.Add((int)vital.Current - start); } return deltas; } private static IEnumerable<(string, MethodBase?)> Targets() { yield return ("Healer.DoSkillCheck", AccessTools.Method(typeof(Healer), nameof(Healer.DoSkillCheck))); yield return ("Healer.GetHealAmount", AccessTools.Method(typeof(Healer), nameof(Healer.GetHealAmount))); yield return ("WorldObject.HandleCastSpell_Boost", AccessTools.Method(typeof(WorldObject), "HandleCastSpell_Boost")); } }