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