namespace Aeshnidae.Mule; /// /// Two patches. One warms a vault so the first /mule is instant; the other stops ACE's own /// maintenance from deleting it. /// [HarmonyPatch] public static class Patches { /// Purges this mod has refused, for mule-admin and the log. public static int PurgesRefused; /// /// Read a vault off disk as its owner logs in, so the first /mule finds it there /// rather than being told to try again. The load runs on its own task; this returns /// immediately. /// [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))] public static void AfterEnterWorld(Player __instance) { try { var accountId = __instance?.Account?.AccountId ?? 0; if (accountId != 0) Vault.Warm(accountId); } catch (Exception ex) { // Never throw out of a hot-path patch. ModManager.Log($"[{Mod.Name}] warming a vault at login failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// /// ACE WOULD DELETE THE VAULT. This stops it. /// /// `ShardDatabaseOfflineTools.PurgeOrphanedBiotasInParallel` deletes every biota that /// has no parent Container, no Wielder and no Location, with one exemption /// (WeenieType.Allegiance). A vault container is exactly that: it is in nobody's /// inventory, nobody wields it, and it has no position because it is in no landblock. /// It would be purged - and on the NEXT run of the same tool every item that had been /// in it would be purged too, by the "Parent container not found" pass. /// /// The tool only runs when `Offline.PurgeOrphanedBiotas` is set in Config.js, it runs /// at startup, and its own documentation calls it a once-every-few-months job. That is /// precisely the kind of switch that gets flipped a year from now by someone who has /// never heard of the vault, so the protection is in code and not in a note: /// ModManager.Initialize runs at Program.cs:158 and the purge at :173, so this patch is /// in place before the tool can run, and MuleDb has already read every vault container /// id out of `aeshnidae_mule` using its own connection (the mod does not need /// DatabaseManager, which is not started until :259). /// /// Giving the containers a dummy Location row would also satisfy the tool, and was /// rejected: a biota with a Location in a landblock's range is exactly what /// GetDynamicObjectsByLandblock loads when that landblock wakes up, and a vault /// appearing on the ground somewhere is worse than the problem. /// [HarmonyPrefix] [HarmonyPatch(typeof(ACE.Database.ShardDatabaseOfflineTools), nameof(ACE.Database.ShardDatabaseOfflineTools.PurgeBiota), new[] { typeof(ACE.Database.Models.Shard.ShardDbContext), typeof(uint), typeof(string) })] public static bool BeforePurgeBiota(uint id, string reason, ref bool __result) { try { if (!MuleDb.IsVaultContainer(id)) return true; Interlocked.Increment(ref PurgesRefused); ModManager.Log($"[{Mod.Name}] REFUSED a purge of 0x{id:X8} - it is an account's vault container (reason given: {reason ?? "none"}). ACE's orphaned-biota pass cannot see that a vault has an owner; aeshnidae_mule can.", ModManager.LogLevel.Warn); __result = false; return false; } catch (Exception ex) { // If the guard itself cannot answer, let ACE through rather than wedging a // maintenance tool - but say so loudly, because this is the one patch whose // failure can cost an account its gear. ModManager.Log($"[{Mod.Name}] the vault purge guard threw on 0x{id:X8} and ACE's purge was NOT blocked: {ex.Message}", ModManager.LogLevel.Error); return true; } } }