namespace Aeshnidae.Mule; /// /// Settings.json in the deployed mod folder. Edit and `mule-admin reload`. /// /// Every number that decides what the vault will and will not do is here, at the top, /// with what it costs to change it. /// public class Settings { public const string FileName = "Settings.json"; // ---- THE TWO NUMBERS THAT MATTER ----------------------------------------- /// /// Master switch. False and every /mule verb refuses politely; nothing is loaded, /// nothing is saved, no table is created. Items already in a vault are untouched - /// they sit in the shard database pointing at their vault container and come back /// when this is turned on again. /// public bool Enabled { get; set; } = true; /// /// How many items one account's vault may hold, in total, across the whole chain. /// /// 1,000 (Tom, 2026-10-09). This is OUR limit and it is the one a player is refused /// by, with a message naming the number. ACE's own per-container capacity is a /// separate, lower backstop - see SlotsPerContainer. /// /// Raising it costs memory: every item in a loaded vault is a live WorldObject for as /// long as any character on the account is online. Lowering it below what an account /// already holds does NOT delete anything - deposits are refused until the count is /// back under the limit, withdrawals always work. /// public int MaxItems { get; set; } = 1000; // ---- THE CONTAINER CHAIN -------------------------------------------------- /// /// The hidden container weenie the vault is made of: 3000136, `aelrynthmulevault`, /// WeenieType Storage. Never placed in the world, never in anyone's inventory, /// never shown to a client. /// /// It is a Storage and not a plain Container on purpose. ACE's Container.Reset -> /// ClearUnmanagedInventory DESTROYS every item in a container whose GeneratorId is /// null - which is every item a player ever deposited - and that method's one /// exemption is `if (this is Storage) return false`. Storage also pins /// Default_ChestResetInterval at infinity and saves itself on every add and remove /// (Storage.OnAddItem / OnRemoveItem), which is a second belt under our own saves. /// public uint VaultWcid { get; set; } = 3000136; /// /// Items per container in the chain. MUST be 1..255. /// /// 250, and the ceiling is not ours: ACE exposes ItemsCapacity through /// `WorldObject.ItemCapacity`, which is a `byte?` cast of the int property, and /// Container.TryAddToInventory compares against it. A weenie row saying 1000 is read /// back as (byte)1000 = 232 with no error anywhere - silent, and it would wedge the /// last 768 slots of a vault shut. So one logical vault is a CHAIN of containers of /// 250, created one at a time as they fill, and the player is never shown the seam: /// MaxItems 1000 is four of them. /// /// Each link is its own top-level container (no container_Id), not nested inside the /// first: ACE's Container ctor loads a sub-container's inventory twice over /// (SortWorldObjectsIntoInventory sets a side pack's InventoryLoaded while the side /// pack's own ctor callback is still in flight), and a vault is not the place to /// find out what that does. /// public int SlotsPerContainer { get; set; } = 250; // ---- WHAT MAY MOVE -------------------------------------------------------- /// /// Attuned items may be deposited. /// /// True, and it is safe TODAY only because a vault belongs to one account and /// nothing crosses an ownership line: what goes in comes back out to the same /// account's characters. ACE refuses to move an attuned item off a player /// (Player_Inventory's AttunedItem check) because that is how it stops them being /// handed to someone else. /// /// IF SHARED ACCESS OR A MARKET IS EVER ADDED, THIS MUST GO BACK TO FALSE, or /// attuned gear becomes tradeable through the vault. There is a deliberate check for /// that in Movement.Deposit - find it before you add sharing. /// public bool AllowAttuned { get; set; } = true; /// /// Packs and foci (anything ACE flags UseBackpackSlot) may be deposited. /// /// FALSE. A pack inside a vault container is a nested container, and ACE loads a /// nested container's contents down two different paths that race each other on /// reload (see SlotsPerContainer). Until that is understood and tested, the vault /// stays flat: deposit what is IN the pack, not the pack. The vault weenie also /// carries ContainersCapacity 0 so ACE refuses it even if this is turned on by /// mistake - turning this on means changing the weenie too, and testing reload. /// public bool AllowPacksAndFoci { get; set; } = false; /// /// Weenie ids that may never be deposited, whatever else is true. Empty by default. /// For anything that turns out to misbehave in storage without a landblock. /// public uint[] RefuseWcids { get; set; } = Array.Empty(); /// /// Most items one `/mule deposit pack N` may move in one go. 250. /// A bulk move holds the account's lock and the world thread for its whole length, /// so this is a fairness dial, not a safety one - every item in it is moved by the /// same single-item sequence, one at a time, each with its own commit and its own /// ledger row. /// public int MaxBulk { get; set; } = 250; // ---- KEEPING IT ON DISK --------------------------------------------------- /// /// Seconds between safety-net saves of every loaded vault. 60. /// /// A vault container is in no landblock, so ACE never ticks it and never saves it: /// Landblock.SaveDB only ever walks its own worldObjects. Every movement already /// forces the moved item's biota to disk before the player is told it worked, so /// this timer exists only to pick up a write that failed and was marked for retry, /// and to write the containers' own rows. 0 switches it off. /// public int SaveIntervalSeconds { get; set; } = 60; /// /// Seconds a loaded vault is kept in memory after the last character on its account /// logs out. 300. /// /// Releasing a vault DROPS REFERENCES ONLY. It must never call Destroy() - that /// cascades into every item in the container, removes their biotas and recycles /// their guids. See Vault.Release. /// public int ReleaseAfterSeconds { get; set; } = 300; /// /// Seconds to wait for a vault's contents to come back from the shard database /// before giving up and refusing the command. 20. /// /// The load is ACE's own: the Container ctor asks the database on its thread and /// queues the sort onto WorldManager's action queue, which the world thread drains. /// A vault that has not finished loading is NEVER treated as empty - every verb /// refuses instead, because "empty" is the one wrong answer that could let a /// deposit overwrite a slot or a count lie to a player. /// public int LoadTimeoutSeconds { get; set; } = 20; // ---- WHAT A PLAYER SEES --------------------------------------------------- /// Items per page of `/mule list`. 20 - about one screen of chat. public int PageSize { get; set; } = 20; /// Movements shown by `/mule log`. 20. public int LogLines { get; set; } = 20; /// Verbose per-movement lines in the server log. Off on live. public bool Trace { get; set; } = false; /// /// The `mule-test` console harness is allowed to run. /// /// FALSE, and it must stay false on live: those verbs mint and destroy real items, and /// they move them through a stand-in container rather than a player. Staging turns it /// on. Every verb refuses with a line saying so when this is off, so there is no way to /// run one by accident. /// public bool AllowTestHarness { get; set; } = false; // ---- LOADING -------------------------------------------------------------- public static Settings Load(string modPath) { var file = Path.Combine(modPath, FileName); try { if (File.Exists(file)) { var loaded = JsonSerializer.Deserialize(File.ReadAllText(file), new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); if (loaded is not null) return loaded.Clamped(); } } catch (Exception ex) { ModManager.Log($"[Aeshnidae.Mule] {FileName} could not be read, defaults are in force: {ex.Message}", ModManager.LogLevel.Error); } return new Settings().Clamped(); } /// /// Pulls any setting that would be unsafe back to something that is not. A bad /// number in a settings file must not be able to eat an item, so this is loud. /// private Settings Clamped() { if (SlotsPerContainer is < 1 or > 255) { ModManager.Log($"[Aeshnidae.Mule] SlotsPerContainer {SlotsPerContainer} is outside 1..255 - ACE reads that capacity as a byte and would silently truncate it. Using 250.", ModManager.LogLevel.Error); SlotsPerContainer = 250; } if (MaxItems < 0) MaxItems = 0; if (PageSize < 1) PageSize = 20; if (LogLines < 1) LogLines = 20; if (MaxBulk < 1) MaxBulk = 1; if (LoadTimeoutSeconds < 1) LoadTimeoutSeconds = 20; return this; } /// How many chain links MaxItems needs. public int ChainLength => MaxItems <= 0 ? 0 : (MaxItems + SlotsPerContainer - 1) / SlotsPerContainer; }