Skip to content

Capacity ​

EmakiStorage capacity is computed from the capacity section of config.yml, the four capacity sources, the three-level stackLimit, and what happens to occupancy when you shrink capacity

capacity ​

yaml
capacity:
  base_slots: 45
  max_slots: 1000
  warn_entry_count: 5000
  default_stack_limit: 100
FieldTypeRequiredDefaultAccepted valuesDescription
base_slotsintNo45≥ 0 (negatives are treated as 0)Default open slot count (45 = 5 × 9, matching the gui.storage_rows default). It is the baseline every player shares, not a total cap
max_slotsintNo1000≥ 0 (negatives are treated as 0); 0 means no capHard ceiling on total slots, applied after the four sources are summed. Lowering it immediately compresses every player above the new value, and the outcome is decided by unlock.overflow_policy
warn_entry_countintNo5000≥ 0 (negatives are treated as 0); 0 disables the warningLogs one console warning when a player's entry count exceeds this value as their data loads. It is advisory only: it never refuses deposits and never changes capacity
default_stack_limitlongNo100≥ 0 (negatives are treated as 0); 0 means unlimitedConfig-level per-slot ceiling. It applies when neither the player level nor the entry level sets one

For example, give the whole server a 90-slot baseline, drop the total cap, and raise the default per-slot ceiling to 1000

yaml
capacity:
  base_slots: 90
  max_slots: 0
  default_stack_limit: 1000

Four Capacity Sources ​

A player's effectiveSlots is the sum of four independently persisted sources, then clamped:

text
effective = clamp (base_slots + permission tier + grantedSlots + purchasedSlots, 0, max_slots)

Setting max_slots to 0 means no upper clamp. Because the four sources are stored separately, lowering base_slots never consumes slots a player was granted or purchased

SourceComes fromCan be negative
baseSlotscapacity.base_slotsNo (negatives treated as 0)
permissionSlotsHighest emakistorage.slots.<n> tierNo
grantedSlots/estorage slot grant, API grantSlotsAsync, action storage_grant_slotYes
purchasedSlotsIn-GUI paid expansion, action storage_unlock_slotNo

Remember: page count is derived from slot count, so you cannot configure it in reverse:

text
totalPages     = max (1, ceil (effectiveSlots / slots per page))
reachablePages = max (1, ceil (usedSlots / slots per page))

Page 1 is always reachable (an empty warehouse must still open and accept its first item); a page holding no entry at all cannot be paged into

Tip: before you set max_slots to 0 (unlimited), confirm you have enough memory. The figure given in the config comment is roughly 100–200MB of heap for 100,000 entries on one player; that is an estimate and has not been measured

Three-Level stackLimit ​

The per-slot ceiling resolves most-specific-first across three levels:

LevelStored inSet throughMeaning of 0
EntryThe matching entry in meta.yml/estorage stacklimit slot, API setSlotStackLimitAsync, action storage_set_stacklimit with slotInherit the player level
PlayerdefaultStackLimit in meta.yml/estorage stacklimit player, API setStackLimitAsync, action storage_set_stacklimit without slotInherit the config level
Configcapacity.default_stack_limit in config.ymlConfig fileUnlimited (equivalent to Long.MAX_VALUE)

The same 0 means different things per level, so do not mix them up: at entry and player level it means "inherit the next level", at config level it means "no limit". Resolution order is entry level > 0, then player level > 0, then the config level; a config level <= 0 yields Long.MAX_VALUE

You can also use the emakistorage.stacklimit.<n> permission tier to supply a player-level ceiling, again taking the highest value and not supporting wildcards

One Item Across Several Slots ​

By default a full slot refuses the remainder. Set behavior.multi_slot_stacking to true and one item type may occupy further slots and keep storing

yaml
behavior:
  multi_slot_stacking: false # true = one entry may span several slots

For example, with a per-slot ceiling of 100 and 120 oak logs in hand:

SettingResult
false (default)100 stored, the remaining 20 refused (slot_full)
true120 stored, displayed as the two slots [100] [20]

How the Occupied Span Is Derived ​

The occupied span is not persisted; it is derived from the amount on every read:

text
span      = ceil (entry amount / per-slot ceiling)
usedSlots = sum of every entry's span

Why it must work this way: usedSlots has to count every entry's span, otherwise a player could fill 90 slots with 45 spanning entries while only being charged 45, walking straight past max_slots

When the per-slot ceiling is unlimited (default_stack_limit: 0 with no player- or entry-level override) span is always 1: a percentage of unlimited is meaningless, so there is no remainder to spill into a second slot

Withdrawal and Merging ​

The slots of a spanning entry share one amount pool, so clicking any of them debits the entry total. Because the span is derived, shrinking is automatic and there is no "merge" step:

text
120 units (ceiling 100)  →  [100] [20]      2 slots occupied
withdraw 20 from slot 1  →  [100]           1 slot occupied, later entries roll forward

Why it looks like items roll forward: entries themselves live in a gap-free compact list, so "later items roll forward" is the list's natural state and the window re-derives it on every repaint

Display ​

Each slot shows its own amount rather than the entry total (slot 1 100/100, slot 2 20/100), matching how players read a vanilla container. A spanning entry appends one extra gui.entry.span_total line stating the entry total and how many slots it occupies

Interaction With Other Mechanisms ​

SurfaceBehaviour
Total slot limitStill enforced. Whatever does not fit is refused as before and never exceeds capacity.max_slots
%used_slots% (placeholder, /estorage info, the window's capacity line)Follows the new semantics automatically: once enabled it reports occupied slots rather than entry count
/estorage stacklimit slot <n>, API setSlotStackLimitAsync, StorageEntrySnapshot.slotIndexStill the entry index, not the displayed slot number. The ceiling belongs to the entry, and the entry index is the stable identity. With the setting off the two coincide
Search hit count %visible%Still the number of matching entries, because players search for items rather than slots
Overflow lockingAccumulates the boundary by span. An entry crossing the capacity boundary is locked as a whole (read-only, withdrawable), preserving zero loss
PersistenceFormat unchanged. Spanning only makes the entry amount larger, and the amount was already a variable-length integer

After Turning It Back Off ​

Turning the setting from true back to false loses no data: entries already above the per-slot ceiling still occupy one slot, display truncated at 100%, refuse new deposits and can be emptied normally

Cost ​

Cost comparison: with the setting off, usedSlots is O (1) (the entry count directly). Once on, every entry must be walked to accumulate spans, so the per-repaint cost rises for players with very many entries. Capacity is resolved once per repaint rather than once per slot, but the walk itself is unavoidable

Shrinking and Overflow ​

When you lower capacity and occupancy ends up beyond the boundary, the outcome is decided by unlock.overflow_policy; the key itself is documented in the unlock section of Configuration

All four values follow the same idea: first compute the locked key set, then decide whether anything is handed back. The set accumulates by span, so the entry that crosses the capacity boundary is locked as a whole rather than being split in half

The Four overflow_policy Values ​

ValueBehaviour
lock_readonlyEntries beyond the boundary become read-only: withdrawable, not depositable, removed (and unlocked) as soon as they are emptied. Recommended
compactTakes the very same branch as lock_readonly and produces an identical locked key set. Entries already live in a gap-free compact list, so there are no holes to roll into; the two values behave identically today and compact is retained only as a semantic placeholder for "tidy up"
return_inventoryTries to push each overflowing entry into the player's inventory stack by stack, using the vanilla max stack size: what fits is logged as an OVERFLOW record and debited from the entry, which is removed once empty; whatever still does not fit stays locked. Nothing is returned while the player is offline, everything stays locked
reject_changeComputes the same locked key set as lock_readonly and sets an internal rejected flag to true; no caller reads that flag in the current implementation, so the effect is identical to lock_readonly — "refuse the shrink" does not actually take effect yet

Good news: all four policies are zero-loss. There is deliberately no drop or delete, because irreversible data loss must never be triggered implicitly by adjusting a config value

Overflow state is evaluated only on the GUI view refresh path (opening the window, refreshing after a deposit, withdrawal, page change, sort or search, and an admin opening a target's warehouse) and is not persisted. No locked set is maintained while the window is closed, so a stored flag can never drift out of sync with the facts