namespace Aeshnidae.AwakenedRewards; /// /// Entry point ACE looks for. /// /// ModContainer resolves the type purely by convention: /// assembly "Aeshnidae.AwakenedRewards.dll" -> type "Aeshnidae.AwakenedRewards.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.AwakenedRewards". Taken from the assembly rather than written out, /// so a rename cannot leave a stale literal behind. /// 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.AwakenedRewards 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); // 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); // InstancesNoDat is looked up lazily, at the first corpse, and again whenever // its assembly changes (a /mod find reloads every mod) - so nothing here can // fail because of the order the mods came up in. This first look is only for // the log line. Instances.Refresh(); // Whoever is online is told their tier now: what an earlier load sent is not known here. TierFeed.CheckAllOnline("load"); var s = Settings; ModManager.Log($"[{Name}] ready - copy loot {(s.ScaleCopyLoot ? $"{(s.LootQualityPercentPerTier > 0 ? $"+{s.LootQualityPercentPerTier:0.##}% quality/tier (cap {s.MaxLootQuality:0.##})" : "retail quality")}, " + $"{(s.LootTierStepEveryTiers > 0 ? $"+1 loot tier per {s.LootTierStepEveryTiers} tiers" : "retail loot tier")}, " + $"item stats +{s.PercentPerAwakening:0.##}%/tier{(s.MaxPercent > 0 ? $" to +{s.MaxPercent:0.##}%" : " uncapped")}, " + $"wield gate {(s.MinimumAwakeningsToWieldLoot ? "on" : "off")}" : "off")}; " + $"attuned items {(s.ScaleAttunedItems ? $"+{s.PercentPerAwakening:0.##}%/awakening of the first owner" : "off")}; " + $"names {(s.StampNames ? "stamped" : "untouched")}; " + $"tier feed {(s.SendTierToClient ? $"on (PropertyInt {TierFeed.PropertyId})" : "off")}; InstancesNoDat {Instances.Status}"); } /// /// Called by ModContainer.Disable() on shutdown, hot-reload, and /mod disable. /// Every patch 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; Instances.Unbind(); TierFeed.Reset(); ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }