namespace Aeshnidae.Challenge; /// /// Challenge runs: a personal copy of a retail shell whose cast is DRAWN rather than /// written down - N species whose creature-power-index score sits in a band, placed at the /// shell's own spawn spots, nothing leaving a corpse, a boss at the far end and one chest /// behind it. /// /// The three things worth knowing before reading any of it: /// /// 1. Difficulty is the BAND, not the creatures. Resolve (PropertyInt 9053, owned by /// Aeshnidae.Awakens and never written here) multiplies the target score the roster is /// drawn around. No creature's numbers are touched, by this mod or by InstancesNoDat's /// tier scaler, which is exactly why the index was built - see Settings.cs and /// C:\Aeshnidae\ops\build-creature-index.md. /// /// 2. Reward follows the DRAW. An unlucky soft draw inside a high band pays the soft /// draw's reward, because what the player fought is the roster, not the dial. /// /// 3. InstancesNoDat does the work. This mod hands it a variant as JSON when a copy opens /// (Variants.Filling) and it does the rest with the machinery a hand-written variant /// already used. The only ACE methods patched here are Creature.Die and the world tick. /// public class Mod : IHarmonyMod { public static readonly string Name = typeof(Mod).Assembly.GetName().Name!; public static readonly string HarmonyId = $"mod.{Name.ToLowerInvariant()}"; public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name); public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath); public static Settings Settings { get; private set; } = new(); private static Harmony? _harmony; private bool _disposed; public void Initialize() { try { Settings = Settings.Load(ModPath); _harmony = new Harmony(HarmonyId); _harmony.PatchAllUncategorized(typeof(Mod).Assembly); var scored = Index.Load(); var shells = Shell.LoadAll(); // Binding installs the filling hook when the mod is enabled. It is retried on // every use, so a load order that puts this mod before InstancesNoDat is not a // failure - the first copy anybody opens binds it. Instances.Refresh(); ModManager.Log($"[{Name}] loaded - {(Settings.Enabled ? "ENABLED" : "disabled")}, " + $"{scored:N0} scored creatures, {shells} shell(s), " + $"InstancesNoDat: {Instances.Status}"); } catch (Exception ex) { // Never throw out of Initialize: the mod would stick Inactive with no // explanation, and a challenge shell would then open as the retail dungeon // with its copies gated to a portal nobody can use. ModManager.Log($"[{Name}] failed to initialize: {ex}", ModManager.LogLevel.Error); } } /// Re-reads Settings.json, the index and every shell, without a restart. public static string Reload() { Settings = Settings.Load(ModPath); var scored = Index.Load(); var shells = Shell.LoadAll(); Runs.Clear(); Instances.Refresh(); Instances.Hook(); return $"{(Settings.Enabled ? "enabled" : "disabled")}, {scored:N0} scored creatures, " + $"{shells} shell(s), InstancesNoDat: {Instances.Status}"; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // The hooks first, and before the patches: they are delegates over methods in // THIS assembly stored on static fields in InstancesNoDat's, so leaving one // behind would keep this load context alive and would have a copy opened after // the reload calling into the old build. try { Instances.Unhook(); } catch { /* InstancesNoDat may already be gone */ } Runs.Clear(); Shell.Forget(); Index.Unload(); _harmony?.UnpatchAll(HarmonyId); _harmony = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }