namespace Aeshnidae.Mule; /// /// The mule vault: account-wide item storage with no client window. /// /// Items are moved by command (/mule), browsed on the website, and they stay real /// WorldObjects the whole way - a deposit is a change of container and nothing else. The /// vault itself is a chain of hidden Storage containers of our own weenie (3000136) that /// are never placed in the world and never put in anyone's inventory; `aeshnidae_mule` /// maps an account to them. Movement.cs carries the ordering and what a crash at each step /// leaves behind; Vault.cs carries why a container with no landblock needs this mod to save /// it at all. /// 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 static Timer? _sweep; private bool _disposed; public void Initialize() { Settings = Settings.Load(ModPath); _harmony = new Harmony(HarmonyId); // The assembly is passed explicitly: the parameterless overload finds its target by // walking the stack, and a small Initialize() gets inlined, so the walk lands on // ACE.Server and silently patches nothing. _harmony.PatchAllUncategorized(typeof(Mod).Assembly); try { // ModManager.Initialize runs at Program.cs:158 - before DatabaseManager starts // at :259, but after ConfigManager at :154, and that is all MuleDb needs to // open its own connection. It has to be this early: the orphaned-biota purge // runs at :173 and the guard in Patches needs the container ids before then. MuleDb.Initialize(); MuleTest.Initialize(); if (Settings.SaveIntervalSeconds > 0) { var period = TimeSpan.FromSeconds(Settings.SaveIntervalSeconds); _sweep = new Timer(_ => Vault.Sweep(), null, period, period); } // A hot reload rebuilds this assembly's statics while players stay online, so // anyone already in the world has no warm vault. Read them back now, or the // first /mule after a reload is a "try again". foreach (var online in PlayerManager.GetAllOnline()) { var accountId = online?.Account?.AccountId ?? 0; if (accountId != 0) Vault.Warm(accountId); } ModManager.Log($"[{Name}] ready - vaults in `{MuleDb.TableName}` on the shard database, ledger `{MuleDb.LedgerTableName}`; " + $"{MuleDb.ProtectedCount} vault container(s) protected from the orphaned-biota purge; " + $"limit {Settings.MaxItems:N0} items in {Settings.ChainLength} link(s) of {Settings.SlotsPerContainer}"); } catch (Exception ex) { ModManager.Log($"[{Name}] could not open vault storage, /mule will refuse to run: {ex.Message}", ModManager.LogLevel.Error); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Unpatch FIRST so no login can warm a vault while it is being let go, then // save and release. _harmony?.UnpatchAll(HarmonyId); _harmony = null; _sweep?.Dispose(); _sweep = null; try { // SYNCHRONOUSLY, and this is not a style choice. ACE's shutdown runs // ModManager.Shutdown() AFTER WorldManager.StopWorld(), so anything queued // onto the world thread here is dropped on the floor - but the shard's own // write queue is drained afterwards, so a direct save does land. A vault is // in no landblock, so nothing else in the shutdown sequence saves it. var rows = Vault.SaveAll("shut down"); Vault.ReleaseAll(); ModManager.Log($"[{Name}] shut down - {rows} vault row(s) written"); } catch (Exception ex) { ModManager.Log($"[{Name}] shutting down could not save every vault: {ex.Message}", ModManager.LogLevel.Error); } } _disposed = true; } /// `mule-admin reload`. Only reads the file; nothing loaded is disturbed. public static void ReloadSettings() => Settings = Settings.Load(ModPath); public static void Trace(string line) { if (Settings.Trace) ModManager.Log($"[{Name}] {line}"); } }