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