namespace Aeshnidae.AllegianceBounty; /// /// Entry point ACE looks for. /// /// ModContainer resolves the type purely by convention: /// assembly "Aeshnidae.AllegianceBounty.dll" -> type "Aeshnidae.AllegianceBounty.Mod" /// so this class must be named Mod and live in a namespace matching the /// assembly name. /// /// The allegiance bounty: a quest one member completes is a day of better kills for /// the whole allegiance. Quests.json is the list, Settings.json the terms, the shard /// table `aeshnidae_allegiance_bounty` the memory across restarts. /// public class Mod : IHarmonyMod { 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.AllegianceBounty public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name); public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath); /// Live settings, re-read on enable and by /bounty-reload. public static Settings Settings { get; private set; } = new(); private static Harmony? _harmony; private bool _disposed; public void Initialize() { Settings = Settings.Load(ModPath); QuestList.Load(ModPath, Settings.QuestsFile); _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); // ModManager.Initialize runs before DatabaseManager is started, but ConfigManager // is already up, and that is all the table needs to open its own connection. try { BountyDb.Initialize(); Bounty.Load(); // A hot reload rebuilds the statics while players stay online and the personal // day is only read at login; read it for everyone already in the world. foreach (var online in PlayerManager.GetAllOnline()) Personal.Load(online); ModManager.Log($"[{Name}] ready - {QuestList.Detectable} detectable quests, +{Settings.QuestPercent:0.##}% per quest, " + $"+{Settings.KillTaskPercent:0.##}% per kill task, {Settings.HoursActive:0.#}h each, " + $"{(Settings.MaxPercent > 0 ? $"capped at {Settings.MaxPercent:0.##}%" : "uncapped")}, " + $"{Bounty.ActiveEntries} entries active across {Bounty.ActiveAllegiances} allegiances (`{BountyDb.TableName}` on the shard database); " + $"personal {(Settings.PersonalEnabled ? $"+{Settings.PersonalPercentPerStamp:0.##}% per distinct stamp to {Settings.PersonalMaxPercent:0.##}%, reset {Settings.PersonalResetHour:00}:00 (`{Personal.TableName}`)" : "off")}"); } catch (Exception ex) { ModManager.Log($"[{Name}] could not open the bounty table; nothing will be recorded or applied: {ex.Message}", ModManager.LogLevel.Error); } } /// /bounty-reload: the files, not the patches. Active entries are re-read from the table too. public static void ReloadFiles() { Settings = Settings.Load(ModPath); QuestList.Load(ModPath, Settings.QuestsFile); if (BountyDb.Ready) { Bounty.Load(); Personal.ForgetAll(); // re-read lazily: the reset hour may have changed foreach (var online in PlayerManager.GetAllOnline()) Personal.Load(online); } } 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; } }