namespace Aeshnidae.DatBackup; /// /// Entry point ACE looks for. /// /// ModContainer resolves the type purely by convention: /// assembly "Aeshnidae.DatBackup.dll" -> type "Aeshnidae.DatBackup.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.DatBackup". Taken from the assembly rather than written out, so a /// rename cannot leave a stale literal behind. /// (Note: nameof(Aeshnidae.DatBackup) 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.DatBackup 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); ModManager.Log($"[{Name}] initialized from {ModPath}"); if (Settings.SnapshotOnStartup) StartupSnapshot(); } /// /// 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. /// /// /// The startup snapshot, off the initialisation thread. /// /// Hashing 1.4 GB of dats takes seconds, and Initialize() runs while the server /// is still coming up - blocking it would delay the world opening for a backup /// that nothing is waiting on. Failures are logged and dropped for the same /// reason: a server that will not start because a backup failed is a worse /// outcome than a server with one missing snapshot. /// private static void StartupSnapshot() { Task.Run(() => { try { var source = Settings.ResolvedDatDirectory; var store = Settings.ResolvedBackupDirectory; var (snapshot, message) = SnapshotStore.Take( store, source, "server startup", Settings.KeepSnapshots, m => ModManager.Log($"[{Name}] {m}")); if (snapshot is null) { ModManager.Log($"[{Name}] {message}", ModManager.LogLevel.Warn); return; } if (!Settings.VerifyOnStartup) return; var (ok, problems) = SnapshotStore.Verify(store, snapshot); if (ok) ModManager.Log($"[{Name}] verified snapshot {snapshot.Id}"); else foreach (var p in problems) ModManager.Log($"[{Name}] verify: {p}", ModManager.LogLevel.Error); } catch (Exception ex) { ModManager.Log($"[{Name}] startup snapshot failed: {ex.Message}", ModManager.LogLevel.Error); } }); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _harmony?.UnpatchAll(HarmonyId); _harmony = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }