namespace Aeshnidae.Awakens; /// /// Entry point ACE looks for. /// /// ModContainer resolves the type purely by convention: /// assembly "Aeshnidae.Awakens.dll" -> type "Aeshnidae.Awakens.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.Awakens". Taken from the assembly rather than written out, so a /// rename cannot leave a stale literal behind. /// (Note: nameof(Aeshnidae.Awakens) 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.Awakens 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); _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); // On a hot reload the dats are already in, so grow the table now; on a cold // start PortalDat is still null and the DatManager postfix does it later. LevelCap.Extend("mod enabled"); ModManager.Log($"[{Name}] ready - awaken at your level cap, +{Settings.LevelsPerAwakening} level per awakening, " + $"{(Settings.MaxAwakenings > 0 ? Settings.MaxAwakenings + " times at most" : "uncapped")}; count is PropertyInt {(int)Awakening.Property}"); ModManager.Log($"[{Name}] Resolve {(Settings.ResolveEnabled ? "on" : "OFF")} - PropertyInt {(int)Resolve.Property} " + $"(9054, seals paid for, is RETIRED), scroll wcid {Settings.ResolveScrollWcid}, " + $"seller wcids [{string.Join(", ", Settings.ResolveSellerWcids ?? new List())}]" + $"{(Settings.ResolveSellerRequired ? "" : " (not required for a scroll)")}"); foreach (var kind in Seals.Kinds) ModManager.Log($"[{Name}] seal '{kind.Key}' = {kind.Label}: one step a use, " + $"max {(kind.Max > 0 ? kind.Max.ToString() : "none")}, " + $"step 1 takes {Math.Max(1, kind.SealsForFirstStep)} seal(s) x{kind.SealsPerStepGrowth} a step " + $"(saturates at step {Seals.SaturatesAt(kind)}), " + $"seal {kind.SealPyreals:N0} / eraser {kind.EraserPyreals:N0} pyreals at the shop, " + $"wcids {kind.SealWcid}/{kind.EraserWcid}, property {kind.PropertyId}"); } /// /// 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; LevelCap.Revert(); ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }