namespace Aeshnidae.BuffBot; /// /// Entry point ACE looks for. /// /// ModContainer resolves the type purely by convention: /// assembly "Aeshnidae.BuffBot.dll" -> type "Aeshnidae.BuffBot.Mod" /// so this class must be named Mod and live in a namespace matching the /// assembly name. Renaming one without the other is the most common reason a mod /// loads its assembly and then logs "Missing IHarmonyMod Type". /// public class Mod : IHarmonyMod { /// /// "Aeshnidae.BuffBot". Taken from the assembly rather than written out, so a /// rename cannot leave a stale literal behind. /// (Note: nameof(Aeshnidae.BuffBot) would yield just "Template" - nameof on a /// namespace returns only its last segment.) /// public static readonly string Name = typeof(Mod).Assembly.GetName().Name!; /// Harmony owner id. Unique per mod - it is what UnpatchAll targets. public static readonly string HarmonyId = $"mod.{Name.ToLowerInvariant()}"; /// Folder this mod was loaded from, e.g. C:\ACEPublic\Mods\Aeshnidae.BuffBot public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name); public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath); /// Live settings, re-read every time the mod is enabled. public static Settings Settings { get; private set; } = new(); private static Harmony? _harmony; private bool _disposed; /// /// Called by ModContainer.Enable() once the assembly is loaded. /// The world is not open yet at this point, so keep this cheap and defensive - /// anything that throws leaves the mod stuck Inactive. /// public void Initialize() { Settings = Settings.Load(ModPath); Bands.Forget(); Tiers.Forget(); WardenDb.Initialize(); Wardens.Reload(); TinkerDb.Initialize(); Tinkers.Reload(); _harmony = new Harmony(HarmonyId); // Applies [HarmonyPatch] classes that are NOT in a [HarmonyPatchCategory]. // Categorized patches stay dormant until you call _harmony.PatchCategory(...) // yourself - a convenient switch for optional features. // Pass the assembly explicitly. The parameterless overload finds its target // by walking the stack, and the JIT inlines a small Initialize(), so the walk // lands on ACE.Server instead of this mod and silently patches nothing. _harmony.PatchAllUncategorized(typeof(Mod).Assembly); // After the patches, because it builds a reverse patch of its own, and separately // from them, because a crafting feature must not be able to throw out of Initialize // and leave the buff bots and the wardens Inactive with it. Formula.Verify(_harmony); ModManager.Log($"[{Name}] ready - banes from bot(s) {string.Join(", ", Settings.Bots)} land on every armour piece; " + $"town warden(s) {string.Join(", ", Settings.Wardens ?? new List())} in {Settings.Towns?.Count ?? 0} town(s), " + $"{Wardens.Rows.Count} tier row(s), ledger {(WardenDb.Ready ? "ready" : "UNAVAILABLE")}, " + $"ForcedTier {Settings.ForcedTier}{(Wardens.Forced ? " (every town held at this tier, nothing can be paid)" : " (off)")}; " + $"town tinker(s) {string.Join(", ", Settings.Tinkers ?? new List())} within {Settings.TinkerRange:N0} m, " + $"{Tinkers.Rows.Count} skill row(s), ledger {(TinkerDb.Ready ? "ready" : "UNAVAILABLE")}, " + $"ForcedTinkerSkill {Settings.ForcedTinkerSkill}, formula guard {(Formula.Ok ? "OK" : "OFF")} ({Formula.Status})"); } /// /// Called by ModContainer.Disable() on shutdown, hot-reload, and /mod disable. /// Every patch and event handler added above must come off here, or the old /// assembly cannot be collected and the next load will double-patch. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _harmony?.UnpatchAll(HarmonyId); _harmony = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }