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;
}
}