English
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| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
base_slots | int | No | 45 | ≥ 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_slots | int | No | 1000 | ≥ 0 (negatives are treated as 0); 0 means no cap | Hard 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_count | int | No | 5000 | ≥ 0 (negatives are treated as 0); 0 disables the warning | Logs 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_limit | long | No | 100 | ≥ 0 (negatives are treated as 0); 0 means unlimited | Config-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: 1000Four 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
| Source | Comes from | Can be negative |
|---|---|---|
baseSlots | capacity.base_slots | No (negatives treated as 0) |
permissionSlots | Highest emakistorage.slots.<n> tier | No |
grantedSlots | /estorage slot grant, API grantSlotsAsync, action storage_grant_slot | Yes |
purchasedSlots | In-GUI paid expansion, action storage_unlock_slot | No |
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:
| Level | Stored in | Set through | Meaning of 0 |
|---|---|---|---|
| Entry | The matching entry in meta.yml | /estorage stacklimit slot, API setSlotStackLimitAsync, action storage_set_stacklimit with slot | Inherit the player level |
| Player | defaultStackLimit in meta.yml | /estorage stacklimit player, API setStackLimitAsync, action storage_set_stacklimit without slot | Inherit the config level |
| Config | capacity.default_stack_limit in config.yml | Config file | Unlimited (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 slotsFor example, with a per-slot ceiling of 100 and 120 oak logs in hand:
| Setting | Result |
|---|---|
false (default) | 100 stored, the remaining 20 refused (slot_full) |
true | 120 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 spanWhy 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 forwardWhy 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
| Surface | Behaviour |
|---|---|
| Total slot limit | Still 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.slotIndex | Still 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 locking | Accumulates the boundary by span. An entry crossing the capacity boundary is locked as a whole (read-only, withdrawable), preserving zero loss |
| Persistence | Format 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
| Value | Behaviour |
|---|---|
lock_readonly | Entries beyond the boundary become read-only: withdrawable, not depositable, removed (and unlocked) as soon as they are emptied. Recommended |
compact | Takes 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_inventory | Tries 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_change | Computes 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
Related Pages
- Configuration: the remaining
config.ymlsections such asunlock,behaviorandpersistence - GUI and Display: how
gui.storage_rowsdecides the slots per page - Unlock Costs: pricing and the purchase flow behind
purchasedSlots - Commands and Permissions:
/estorage slot,/estorage stacklimitand the permission tiers - API and Integration: how
StorageCapacityfields map onto configuration keys