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