namespace Aeshnidae.Trials; /// /// Entry point ACE looks for (assembly "Aeshnidae.Trials.dll" -> type /// "Aeshnidae.Trials.Mod"). /// /// The Trials: a fresh personal dungeon copy (Aeshnidae.InstancesNoDat), entered /// through its portal, is a timed course - the clock runs until everything in it is /// dead, nothing respawns meanwhile, and leaving, dying or a teleport voids the run. /// Bests per character and per dungeon and tier on the shard database, Radiance for /// a clear through Aeshnidae.Bank, a trophy and experience for the week's fastest /// clear of each dungeon, trials.json for the website, a page in the Codex. /trial. /// 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.Trials 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 /trial-reload. public static Settings Settings { get; private set; } = new(); /// landblock -> trial, from the settings. public static Dictionary Trials { get; private set; } = new(); private static Harmony? _harmony; private bool _disposed; public void Initialize() { Settings = Settings.Load(ModPath); Trials = Settings.TrialMap(); _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 tables need to open their own connection. try { TrialDb.Initialize(); } catch (Exception ex) { ModManager.Log($"[{Name}] could not open the records tables; runs will be timed and told but nothing recorded: {ex.Message}", ModManager.LogLevel.Error); } // The siblings may not be up yet on a cold start; every use re-checks. Instances.Refresh(); _ = Bank.Available; try { Feed.Write(); } catch { /* logged inside */ } ModManager.Log($"[{Name}] ready - {Trials.Count} trial(s): {string.Join(", ", Trials.OrderBy(kv => kv.Key).Select(kv => $"{TrialName(kv.Key)} ({kv.Key:X4})"))}; " + $"cooldown {Settings.CooldownMinutes:0} min, {Settings.RadiancePerClear:N0} Radiance per clear, " + $"trophy {Settings.AwardDayOfWeek} {Settings.AwardHour:00}:00 with {Settings.WeeklyAwardXp:N0} xp (weenie {Settings.TrophyWcid}); " + $"tables {(TrialDb.Ready ? "open" : "NOT open")}; InstancesNoDat {Instances.Status}; Bank {Bank.Status}"); } /// The name players read for a Trial: the settings' name, else InstancesNoDat's, else the id. public static string TrialName(ushort landblock) => Trials.TryGetValue(landblock, out var t) && !string.IsNullOrWhiteSpace(t.Name) ? t.Name : Instances.NameFor(landblock); /// A Trial by name prefix, name fragment, or hex id. public static ushort? FindTrial(string text) { var wanted = (text ?? "").Trim(); if (wanted.Length == 0) return null; if (ushort.TryParse(wanted.Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, null, out var id) && Trials.ContainsKey(id)) return id; var keys = Trials.Keys.OrderBy(k => k).ToList(); foreach (var k in keys) if (TrialName(k).Equals(wanted, StringComparison.OrdinalIgnoreCase)) return k; // "the Citadel Below" and "Citadel" should both find it. foreach (var k in keys) if (TrialName(k).StartsWith(wanted, StringComparison.OrdinalIgnoreCase)) return k; foreach (var k in keys) if (TrialName(k).Contains(wanted, StringComparison.OrdinalIgnoreCase)) return k; return null; } /// /trial-reload: the settings, not the patches. Runs in progress are kept. public static void ReloadSettings() { Settings = Settings.Load(ModPath); Trials = Settings.TrialMap(); try { Feed.Write(); } catch { /* logged inside */ } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { Instances.Unbind(); Runs.ForgetAll(); _harmony?.UnpatchAll(HarmonyId); _harmony = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }