English
Configuration
config.yml keys are covered one by one. EmakiStorage's configuration is split across three files, and the capacity domain model has a page of its own:
| Topic | Page |
|---|---|
The gui/storage_gui.yml template plus the gui and display sections | GUI and Display |
The capacity section, four capacity sources, three-level stackLimit, shrinking and overflow | Capacity |
The unlock_costs.yml paid tiers | Unlock Costs |
After you edit, run /estorage reload to reload config, language and GUI templates
config.yml Top Level
yaml
version: "1.1.5"
language: "zh_CN"
release_default_data: true
op_bypass: false
debug: false| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
version | string | No | 1.1.5 (falls back to 1.0.6 when the key is absent) | — (not validated) | Default-resource version marker, maintained by resource sync. When the key is absent the source falls back to AppConfig.CURRENT_VERSION (the config-migration structure constant, 1.0.6), which differs from the shipped value; do not conflate the two |
language | string | No | zh_CN | File name of lang/<language>.yml | Language of GUI and command text |
release_default_data | boolean | No | true | true / false | Whether the bundled business data (unlock_costs.yml) is released once when it is missing; an existing file is never touched |
op_bypass | boolean | No | false | true / false | When true, OPs are treated as holding emakistorage.admin, which unlocks every admin subcommand and replaces the dedicated reload / debug permissions |
debug | boolean | No | false | true / false | Parsed into the config object, but no branch in the current implementation reads it; manage debugging through /estorage debug |
For example, switch the language to English and temporarily let OPs bypass the permission checks
yaml
language: "en_US"
op_bypass: trueunlock
yaml
unlock:
overflow_policy: lock_readonly
purchase_enabled: true
cost_file: "unlock_costs.yml"| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
overflow_policy | enum | No | lock_readonly | lock_readonly / compact / return_inventory / reject_change | How to handle occupancy that exceeds a lowered capacity; the behavioural differences between the four values are documented in Capacity. An unknown value falls back to the default |
purchase_enabled | boolean | No | true | true / false | Whether in-GUI paid expansion is available. Also requires the emakistorage.unlock.purchase permission; when false the unlock button reports that the feature is disabled |
cost_file | string | No | unlock_costs.yml | A relative path under the plugin data folder; existence is not validated | File name of the paid tier configuration. When the file is missing or empty, paid expansion is entirely unavailable. Documented key by key in Unlock Costs |
For example, disable paid expansion and switch the shrink policy to inventory return
yaml
unlock:
overflow_policy: return_inventory
purchase_enabled: falsebehavior
yaml
behavior:
overflow_on_withdraw: return
withdraw_amounts:
left: 1
right: 16
shift_left: 32
shift_right: 64
withdraw_prompt:
enabled: true
mode: "auto"
dialog:
title: "dialog.withdraw.title"
can_close_with_escape: true
pause: false
body:
- "dialog.withdraw.body"
inputs:
- type: "text"
key: "amount"
label: "dialog.withdraw.label"
max_length: 32
buttons:
- label: "dialog.confirm"
deposit_filter:
mode: blacklist
entries: []
allow_unique_items: true
multi_slot_stacking: false
batch_max_ops: 200
default_sort: amount_desc
player_sort_enabled: true| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
overflow_on_withdraw | enum | No | return | return / drop | What happens to the part the inventory refuses: return credits it back to storage and rolls back the matching part of the debit, drop drops it on the ground and it is not counted towards this withdrawal. An unknown value falls back to return |
withdraw_amounts.left | long | No | 1 | ≥ 0 (negatives are treated as 0) | Left-click withdrawal amount. 0 makes that click kind a no-op |
withdraw_amounts.right | long | No | 16 | ≥ 0 (negatives are treated as 0) | Right-click withdrawal amount |
withdraw_amounts.shift_left | long | No | 32 | ≥ 0 (negatives are treated as 0) | Shift + left-click withdrawal amount |
withdraw_amounts.shift_right | long | No | 64 | ≥ 0 (negatives are treated as 0) | Shift + right-click withdrawal amount |
withdraw_prompt.enabled | boolean | No | true | true / false | Whether middle-click enables custom-amount input. When false middle-click is a no-op and the entry lore no longer appends the custom-amount hint line |
withdraw_prompt.mode | enum | No | auto | auto / dialog / chat | Input method: auto uses the dialog when available and falls back to chat input otherwise; dialog forces the dialog and disables the interaction when unavailable; chat forces chat input. An unknown value falls back to auto |
withdraw_prompt.dialog | section | No | see example | CoreLib dialog key set, see Dialog Configuration Block | Dialog configuration. The input with key: amount must be kept or the dialog is treated as unusable |
deposit_filter.mode | enum | No | blacklist | blacklist / whitelist / off | Deposit filter mode. An unknown value falls back to blacklist |
deposit_filter.entries | list<string> | No | [] | List of CoreLib ItemSource tokens | A hit on any token counts as a hit; item sources use hyphens, component IDs/paths use colons |
deposit_filter.matcher | section | No | none | CoreLib item-matcher key set | Selects items by component, PDC, lore and similar. Combined with entries using OR: a hit on either counts as a hit, and mode then decides allow or block. See Item Matcher |
allow_unique_items | boolean | No | true | true / false | Whether items carrying a per-instance marker (non-empty PDC on the item itself) may be deposited. It is only checked when a new entry would be created; an existing entry keeps accumulating |
multi_slot_stacking | boolean | No | false | true / false | Whether one item type spills into a new slot once its slot is full. When false the remainder is refused. See Capacity |
batch_max_ops | int | No | 200 | 1–2000 (values outside are clamped into range) | Maximum operations allowed in one applyBatchAsync. A request above the effective limit returns batch_too_large for the whole batch without executing anything; it only affects the batch API, not the GUI or commands |
default_sort | enum | No | amount_desc | material_asc / material_desc / name_asc / name_desc / amount_asc / amount_desc | Default sort for new players and players who never changed it. An unknown value falls back to amount_desc |
player_sort_enabled | boolean | No | true | true / false | Whether players may change the sort in the GUI. When false the sort button reports it is disabled and default_sort is enforced |
Keep the two empty cases apart: when deposit_filter.mode is whitelist with both entries and matcher empty, nothing can be deposited; when it is blacklist with both empty the filter is effectively off and everything passes. The unique-marker test is structural: it checks whether the item's own PersistentDataContainer is non-empty rather than reading its display name or lore
Note: withdraw_amounts only accepts non-negative values. A negative value is parsed as 0, which turns that click kind into a no-op rather than falling back to the default amount
These keys decide the amount each click moves and whether it is allowed at all; the full click semantics table, the rejected click kinds and the search-mode restriction live in GUI and Display
Here is another example: take one unit per right-click, disable the middle-click custom amount, and allow one entry to span several slots
yaml
behavior:
withdraw_amounts:
left: 64
right: 1
shift_left: 2304
shift_right: 576
withdraw_prompt:
enabled: false
multi_slot_stacking: truesearch
yaml
search:
enabled: true
operators:
name: "@"
lore: "#"
id: "$"
exclude: "!"
input:
mode: "auto"
timeout: 30
cancel_keywords: ["取消", "cancel"]
dialog:
title: "dialog.search.title"
can_close_with_escape: true
pause: false
body:
- "dialog.search.body"
inputs:
- type: "text"
key: "query"
label: "dialog.search.label"
max_length: 64
buttons:
- label: "dialog.confirm"| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
enabled | boolean | No | true | true / false | Whether search is available. When false the search button reports that search is disabled |
operators.name | string | No | @ | Any non-empty prefix; an empty string disables this scope | Matches the display name only |
operators.lore | string | No | # | Any non-empty prefix; an empty string disables this scope | Matches lore only |
operators.id | string | No | $ | Any non-empty prefix; an empty string disables this scope | Matches the material key or ItemSource id only |
operators.exclude | string | No | ! | Any non-empty prefix; an empty string disables exclusion | Exclusion: a match rejects the entry |
input.mode | enum | No | auto | auto / dialog / chat | Input method, with the same meanings as behavior.withdraw_prompt.mode |
input.timeout | long | No | 30 | ≥ 0 (negative values are treated as 0) | Seconds to wait for chat input. The middle-click custom withdrawal amount reuses this timeout, see below |
input.cancel_keywords | list<string> | No | ["取消", "cancel"] | Keyword list; an empty list falls back to the default | Keywords that cancel the chat input. They also apply to the custom withdrawal amount chat input |
input.dialog | section | No | see example | CoreLib dialog key set, see Dialog Configuration Block | Dialog configuration. The input with key: query must be kept or the dialog is treated as unusable |
These two keys are shared by two interactions: search.input.timeout and search.input.cancel_keywords both serve the search prompt and the middle-click custom withdrawal amount chat input. So if you shorten the timeout to 5, both inputs get the shorter wait
While a search filter is active the display area cannot accept deposits; right-clicking the search button while searching clears the filter
For example, use English cancel keywords and force chat input
yaml
search:
operators:
name: "n:"
lore: "l:"
input:
mode: "chat"
timeout: 15
cancel_keywords: ["cancel", "quit"]Dialog Configuration Block
behavior.withdraw_prompt.dialog and search.input.dialog use exactly the same key set as the EmakiCoreLib dialog definition files (EmakiCoreLib/dialogs/*.yml), including type, external_title, after_action, columns, exit_button, plus per-input width, initial and label_visible. The only difference is that the id is supplied by the plugin and must not be written in the configuration
How the text resolves: the title, the text of body entries, input labels and button label / tooltip are resolved as language keys first. When you write a key such as dialog.withdraw.title it is read from lang/*.yml, and a missing key renders literally, so MiniMessage text can be written directly instead. The search dialog body supports the four operator placeholders %name%, %lore%, %id% and %exclude%
Prerequisites first: dialogs require client 1.21.6 or newer and EmakiCoreLib's dialog.enabled set to true. If inputs lacks the key the feature needs (amount for withdrawal, query for search) or carries no buttons, the dialog is treated as unavailable: mode: auto falls back to chat input, while mode: dialog disables the interaction and reports dialog.unavailable
You can also change the operator prefixes so they avoid clashing with item names. Search syntax:
- Whitespace separates terms and every term must match (AND)
- A term may carry one scope prefix and one exclusion prefix in either order, so
!@diamondand@!diamondare equivalent - Without a scope prefix a term matches the combined text, which is built from display name plus lore plus identifier, so an unscoped search can also hit a material key
- All matching is substring matching against text that was already lower-cased and stripped of formatting
Note: no regular-expression search is provided, not even behind an admin flag. A hostile pattern can stall the server through catastrophic backtracking, and substring matching covers the actual use case
auto_pickup
yaml
auto_pickup:
enabled: true
mode: on_pickup
radius: 8
max_radius: 16
scan_interval_ticks: 10
default_enabled: false
notify_cooldown_ms: 3000| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
enabled | boolean | No | true | true / false | Master switch for auto-pickup. Once off the feature is entirely unavailable, /estorage autopickup is refused and the scan task never starts |
mode | enum | No | on_pickup | on_pickup (alias pickup) / radius (alias range) | Working mode: on_pickup only collects when the vanilla pickup event fires (no extra cost); radius additionally starts a scheduled scan that collects every dropped item within the configured radius (stronger, but it costs performance). An unknown value falls back to on_pickup |
radius | int | No | 8 | Clamped to 1–max_radius | radius mode only. Scan radius in blocks, server-wide. Ignored in on_pickup mode |
max_radius | int | No | 16 | ≥ 1 (values below 1 are treated as 1) | radius mode only. Clamp ceiling for radius. max_radius is resolved first and then clamps radius, so lowering it tightens radius immediately |
scan_interval_ticks | int | No | 10 | ≥ 1 (values below 1 are treated as 1) | radius mode only. Server-wide scan interval in ticks. 20 ticks = 1 second, so 10 scans twice per second. Lower reacts faster but costs more CPU. Ignored in on_pickup mode |
default_enabled | boolean | No | false | true / false | Default per-player switch state when a player's data is first loaded. true means on by default and players turn it off with /estorage autopickup off; false means off by default. A switch already written to disk is never overwritten by this key |
notify_cooldown_ms | long | No | 3000 | ≥ 0 (negatives are treated as 0) | Notification cooldown for refused pickups, in milliseconds. Further refusals inside the cooldown send no message, avoiding chat spam; 0 notifies every time |
Players manage their personal switch with /estorage autopickup (requires emakistorage.autopickup, default true). There is no per-player radius: radius is a single server-wide value you can only change in the configuration
Watch the cost: mode: radius scans every online player every scan_interval_ticks, so with 100 players online and scan_interval_ticks: 20 that is five all-player scans per second. Tune the interval to the server, or use on_pickup mode
For example, switch to radius scanning and sweep 6 blocks every 2 seconds
yaml
auto_pickup:
mode: "radius"
radius: 6
max_radius: 16
scan_interval_ticks: 40persistence
yaml
persistence:
autosave_interval_seconds: 300
drain_timeout_seconds: 10| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
autosave_interval_seconds | long | No | 300 | ≥ 0 (negatives are treated as 0) | Autosave interval in seconds. 0 disables the timer, leaving saves to GUI close and logout. The legacy key name autosave_interval is still accepted and is migrated automatically at startup |
drain_timeout_seconds | long | No | 10 | ≥ 1 (values below 1 are treated as 1) | Seconds to wait for file writes to drain when the plugin disables. The legacy key name drain_timeout is still accepted and is migrated automatically at startup |
You will see multiple amount changes within one interval merge into a single write. Every save is a full rewrite of storage.dat: write a sibling .tmp, re-read and validate the record count, then replace atomically. A failed write leaves the previous file untouched and deletes the tmp
For example, save every 10 minutes and allow up to 30 seconds on shutdown
yaml
persistence:
autosave_interval_seconds: 600
drain_timeout_seconds: 30logging
yaml
logging:
enabled: true
retention_days: 30
sources: []| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
enabled | boolean | No | true | true / false | Whether ordinary flow records are written. It has no effect on the always-recorded admin operations |
retention_days | int | No | 30 | ≥ 0 (negatives are treated as 0) | Log retention in days. <= 0 disables purging |
sources | list<string> | No | [] | gui / command / api / action / auto_pickup; an empty list means all | Record only these surfaces, matched exactly by source id. The shipped config.yml comment lists only the first four; the fifth source auto_pickup (auto-pickup deposits) also exists |
The log layout is one directory per player and one file per day: logs/<uuid>/<yyyy-MM-dd>.log. Expired files are deleted per day; once every file for a player has expired, their directory is removed as well. Purging runs during reload and shutdown
Remember: the three admin operation kinds ADMIN_SET, ADMIN_GIVE and ADMIN_CLEAR are always recorded, ignoring both enabled and sources. An admin disabling logging to save disk is no reason to lose the audit trail of admin changes
The flow log is write-only: business logic never reads these files, which removes the need for an index, compaction, schema versioning or cross-file transactions. A failed write only logs a warning and never fails the storage transaction that produced it. Records queued for the same player are flushed as one append, so your 36-slot bulk deposit produces one file operation rather than 36
For example, keep 7 days and record only what players do directly in the window
yaml
logging:
enabled: true
retention_days: 7
sources: ["gui"]Related Pages
- GUI and Display: the
guianddisplaysections, thegui/storage_gui.ymltemplate and click semantics - Capacity: the
capacitysection, the three-levelstackLimitand the fouroverflow_policyvalues - Unlock Costs: the tiers, pricing and payment ordering of
unlock_costs.yml - Commands and Permissions: the scope of
/estorage reloadand the permission tiers - CoreLib Actions: changing slots and
stackLimitfrom an action chain - API and Integration: how
StorageCapacityfields map onto configuration keys