Skip to content

GUI and Display ​

What the player sees comes from the gui and display sections of config.yml, the structure of the gui/storage_gui.yml template, and what each click does

For capacity and slot counts see Capacity; for the remaining config.yml keys see Configuration

gui ​

yaml
gui:
  storage_rows: 5
  deposit_feedback: actionbar
  require_empty_cursor_for_withdraw: false
FieldTypeRequiredDefaultAccepted valuesDescription
storage_rowsintNo5Clamped to 1–5 at load time (a warning is logged when out of range)Display row count. The function row always owns the last row, so the real window height is storage_rows + 1 and one page renders storage_rows × 9 slots
deposit_feedbackenumNoactionbaractionbar / message / noneFeedback channel for a successful deposit. An unknown value falls back to actionbar. It only affects success and partial-deposit feedback; failure messages always go to chat
require_empty_cursor_for_withdrawbooleanNofalsetrue / falseWhether withdrawal requires an empty cursor. See the note below

Recommendation: do not set deposit_feedback to none. Under mixed deposit an item lands where the entry order puts it and may end up on another page; with feedback disabled the player will believe the item vanished

require_empty_cursor_for_withdraw is a misclick guard. When you set it to true, clicking a display slot while holding an item on the cursor no longer deposits: the click becomes a no-op and reports gui.deposit.cursor_guard. This closes the "meant to withdraw but still had a leftover cursor" mis-click window, at the cost of losing the "click an entry while holding an item to deposit" shortcut. The default, false, keeps that shortcut deposit; you can still deposit through the fixed deposit port or deposit_all either way

For example, shrink the window to 3 display rows and send deposit feedback to chat

yaml
gui:
  storage_rows: 3
  deposit_feedback: message
  require_empty_cursor_for_withdraw: true

storage_rows Conversion Table ​

The slots values in the template file are only placeholders, so you can ignore them. Real slot numbers are derived from gui.storage_rows at load time:

text
storageRows  = clamp (gui.storage_rows, 1, 5)
totalRows    = storageRows + 1
functionBase = storageRows * 9
display area = 0 .. functionBase - 1
function row = functionBase + offset
storage_rowstotalRowsSlots per pageDisplay slotsFunction slots (offset 0–8)
1290–89–17
23180–1718–26
34270–2627–35
45360–3536–44
56450–4445–53

Out-of-range values are pulled back in: a value >= 6 is clamped to 5 with a warning; a value <= 0 is clamped to 1 with a warning. totalRows peaks at 6, which already sits inside CoreLib's 1–6 range

storage_gui.yml Template Structure ​

yaml
id: storage_gui
gui_type: CHEST
title: "<gradient:#4DA6FF:#FFD166>绘卷仓库</gradient>"

slots:
  storage_slot:
    type: storage_slot
    slots: [0]
    item:
      source: "gray_stained_glass_pane"

  page_prev:
    type: page_prev
    offset: 0
    slots: [0]
    item:
      source: "arrow"
FieldTypeRequiredDefaultAccepted valuesDescription
idstringYesshipped value storage_guiMust be storage_guiTemplate id; the implementation looks the template up by it. Any other value means the template cannot be found, the window fails to open and a warning is logged
gui_typestringNoshipped value CHESTBukkit InventoryType nameContainer type. The layout rebuild always recreates the window as CHEST, so this key does not decide the final window type
rowsintegerNonot declared in the shipped fileignoredThe shared template's row field. The warehouse layout rebuilds the template with gui.storage_rows + 1 rows, so this key has no effect; change gui.storage_rows instead
titlestring / objectNoshipped value "<gradient:#4DA6FF:#FFD166>绘卷仓库</gradient>"MiniMessage string or a TextConfig objectWindow title
slotsmapNoshipped value holds 9 entriesKeys name the slot entriesThe slot entry table. It must contain storage_slot, otherwise the display area cannot be built and the whole template fails to resolve
slots.<key>.typestringNoderived from the lower-cased entry keysee the slot type table belowSlot type label. When omitted it is derived from the key; an unrecognised value leaves the slot present but unresponsive to clicks
slots.<key>.offsetintegerNo—0–8Row offset for function entries. When it is missing, not a number or out of range the entry is rejected with a warning; when two entries claim the same offset the later one is rejected. storage_slot does not use this key
slots.<key>.slotslist<integer>No——The slot indices the shared template parser requires. For function entries this key is overwritten by the offset conversion at load time, so whatever is written there never takes effect
slots.<key>.itemsectionNo—shared item definition key set (source, amount, components, ...), see CoreLib GUIEntry appearance. The item configured on storage_slot only supplies the placeholder appearance of a locked slot

You get exactly these 9 slot types under slots:

Slot typePurpose
storage_slotThe display-area prototype. Required: without it the display area cannot be built and the whole template fails to resolve
deposit_slotThe fixed deposit port. Clicking it with a loaded cursor deposits
page_prevPrevious page
page_infoCurrent page information, display only
page_nextNext page
searchOpens the search prompt; right-click while searching clears the filter
sortCycles the sort; shift-click cycles it in reverse
deposit_allBulk deposit across the 36 main inventory slots
unlockEntry point for paid expansion

An entry may also declare type explicitly; when omitted it is derived from the lower-cased entry key, so naming an entry after its type is enough

How offset Relates to slots ​

Keep this rule in mind: function-row entries only declare offset (valid 0–8). The slots key in the template exists only to satisfy the shared template parser and is overwritten by the conversion at load time, so it must never be read as a real position

  • When offset is missing, not a number, or outside 0–8, the entry is rejected with a warning
  • When two entries claim the same offset, the later one is rejected with a warning
  • When every function entry is rejected, a warning is logged and the paging and deposit buttons become unavailable

The display area's storage_slot is a prototype: at load time it expands across the whole display area, 0 .. functionBase - 1. The item you configure on it only supplies the placeholder appearance of a locked slot and does not affect how unlocked slots render

Once you change gui.storage_rows, the function row moves automatically; no slot number needs to be edited by hand

For example, give the window a custom title and move the search button from its default offset 4 to offset 2, the only gap in the default layout

yaml
title: "<gold>My Warehouse</gold>"

slots:
  search:
    type: search
    offset: 2
    item:
      source: "compass"

Check before you move an entry: the default function row occupies offsets 0, 1, 3, 4, 5, 6, 7 and 8, leaving offset 2 free. Make sure the target offset is not claimed by another entry, or the moved entry is rejected with a warning

GUI Click Semantics ​

LocationActionEffect
Display slotLoaded cursor + left-clickDeposits the whole cursor
Display slotLoaded cursor + right-clickDeposits 1 unit
Display slotEmpty cursor + left / right / shift-left / shift-rightWithdraws per withdraw_amounts
Display slotEmpty cursor + middle-clickOpens custom-amount input (governed by behavior.withdraw_prompt.enabled; behavior.withdraw_prompt.mode decides dialog versus chat input)
deposit_slotLoaded cursorDeposits the whole cursor
deposit_slotEmpty cursorReports that the port is empty
deposit_allClickBulk-deposits across the 36 main inventory slots
sortClick / shift-clickCycles the sort / cycles it in reverse
searchClickOpens the search prompt; right-click while searching clears the filter
page_prev / page_nextClickPages; an empty page cannot be paged into
Player inventory itemShift-clickRedirected into the deposit port logic

These click kinds are rejected outright: double-click, number keys, offhand swap, the drop key and Ctrl + drop. They respectively aggregate across slots, bypass cursor semantics or discard the item outright, so none of them can be given a safe meaning; such clicks report gui.click.unsupported

Note: while a search filter is active, deposits into the display area are refused (paging follows the filtered result set, so a display slot no longer identifies a real slot number), but the fixed deposit port keeps working

Withdrawal amounts, the default sort and the deposit filter are documented in the behavior section of Configuration

display ​

yaml
display:
  amount_mode: percent
  percent_scale: 99
  compact_units: ["K", "M", "B", "T", "P", "E"]
  compact_decimals: 2
  show_exact_amount: true
  lore_position: bottom
FieldTypeRequiredDefaultAccepted valuesDescription
amount_modeenumNopercentpercent / oneHow the slot item's amount is expressed: percent shows occupancy as 1–99, one is always 1. An unknown value falls back to percent
percent_scaleintNo99Clamped to 1–99 before use (no error when out of range)Scale ceiling for percent mode; it is also the value written to the slot item's max_stack_size component
compact_unitslist<string>No["K", "M", "B", "T", "P", "E"]At most 6 elements; an empty list falls back to the defaultCompact amount units, in order for 10³... 10¹⁸. An empty list falls back to the six defaults rather than degrading to exact numbers
compact_decimalsintNo2≥ 0 (negatives are treated as 0)Decimal places kept in compact format, rounded down with trailing zeroes trimmed
show_exact_amountbooleanNotruetrue / falseWhether to also show the grouped exact amount in lore
lore_positionenumNobottomtop / bottomWhere generated lines go relative to the template lore. An unknown value falls back to bottom

For example, turn off occupancy rendering (slots always show 1), keep one decimal, and put the generated lines above the template lore

yaml
display:
  amount_mode: "one"
  compact_decimals: 1
  show_exact_amount: false
  lore_position: top

1–99 Scale Rules ​

The valid range of the max_stack_size component is exactly 1–99, so the implementation never uses 100 or above and no packet trickery is involved. The scale boundaries are set deliberately:

CaseDisplayed amount
amount_mode: oneAlways 1
stackLimit unlimited (Long.MAX_VALUE) or <= 0Degrades to 1; a percentage is meaningless for an unlimited ceiling
Stored amount <= 01
Stored amount >= stackLimit (genuinely full)percent_scale (default 99)
Partially filledclamp(round(amount / stackLimit * scale), 1, scale - 1), so at most 98 by default

Why these clamps exist: without them a nearly-empty slot would render as 0 (invisible) and a nearly-full one would be indistinguishable from full

The slot percentage and the lore text are computed differently: the percentage text in lore is computed independently on the real 0–100 range and does not inherit the 1–99 clamping

Compact Units ​

compact_units maps in order onto the six thresholds 10³, 10⁶, 10⁹, 10¹², 10¹⁵ and 10¹⁸, taking at most 6 elements. Long.MAX_VALUE is roughly 9.22E, so the E unit covers the entire long range. Amounts below 1000 print the plain number. If you write an empty list in the configuration, the parser falls back to the six defaults, so the "empty list means no compact units" branch is unreachable from configuration

  • Configuration: the remaining config.yml sections, including behavior and search
  • Capacity: slots per page, total pages and the three-level stackLimit
  • Unlock Costs: the tiers and batch options behind the unlock button
  • Commands and Permissions: /estorage reload also reloads the GUI template