Screens
screen
| Function | Does |
|---|---|
screen.current() |
Returns {type, title, handled}, or nil when no screen is open |
screen.close() |
Closes the open screen the way Escape does |
screen.open([title]) |
Opens an empty screen that frees the cursor without pausing, for UIs drawn in render_gui |
screen.widgets() |
Every widget of the open screen, in layout order |
screen.widget(label) |
First widget whose text contains label, or nil |
screen.click_text(text) |
Runs the click event of a text component on the open screen |
screen.dialog() |
The server dialog on screen, or nil |
screen.dialog_input(key, value) |
Sets a dialog input by its key |
screen.sign() / screen.set_sign(lines) |
Reads or replaces the four lines of a sign editor |
screen.book() / screen.set_book_page(page) |
Reads or turns an open book |
type is a stable name for the screens the client recognizes ("inventory",
"generic_container", "anvil", "furnace", "merchant", "chat", "game_menu",
"death", "sign", "book", "lectern", "dialog", "waiting_for_response", …), "script"
for screen.open, and "other" for the rest. title is a text component. handled
marks container screens, whose slots interaction.click_slot can click and
container can read.
screen.close() does what Escape does on that screen: a container tells the server, a dialog
runs its cancel action, a sign editor sends its lines. With no screen shown but a server menu
still open (its screen_open was cancelled) it closes that menu.
local s = screen.current()
if s and s.type == "generic_container" then
print("looking at", s.title:string())
end
screen_open fires before a screen appears and screen_close before it
goes. A screen builds its widgets as it opens, so act on them from the next tick.
Widgets
screen.widgets() works on any screen: dialogs, confirmations, the anvil's name field, merchant
trade buttons, book page arrows. A handle reads the widget live and acts through the vanilla
widget code, so the server sees exactly what a player's click would send.
| Method | Returns |
|---|---|
w:kind() |
"button", "checkbox", "cycle", "slider", "text_field", "text_area", "label", "item", "other" |
w:text() |
The message as a text component: a button's label, a field's label, a label's text |
w:value() |
Field text, checkbox state, cycle option (its id in a dialog), slider position 0..1; nil otherwise |
w:set_value(v) |
Sets that value; false when the widget is inactive, stale, or a cycle has no such option |
w:press([button]) |
Clicks the middle of the widget; false when inactive, hidden or stale |
w:active() / w:visible() / w:focused() |
Widget state |
w:valid() |
Whether the widget's screen is still the open one |
w:bounds() |
x, y, width, height in GUI-scaled pixels |
A handle belongs to the screen it came from. Once another screen opens, press and
set_value do nothing and return false.
-- answer an anvil prompt: the rename reaches the server, then take the result slot
for _, w in ipairs(screen.widgets()) do
if w:kind() == "text_field" then w:set_value("my answer") end
end
interaction.click_slot(interaction.sync_id(), 2, 0, "pickup")
Dialogs
Servers show dialogs: screens made of text, items, inputs and buttons.
screen.dialog() returns what the server sent:
| Field | Holds |
|---|---|
kind |
"minecraft:notice", "minecraft:confirmation", "minecraft:multi_action", "minecraft:server_links", "minecraft:dialog_list" |
title, external_title |
Text |
escape |
Whether Escape closes it |
pause |
Whether it pauses singleplayer |
after_action |
"close", "none" or "wait_for_response" |
body |
List of {kind, text?, item?}: "minecraft:plain_message" with text, "minecraft:item" with an item and maybe text |
inputs |
List of {key, kind, label, value, widget} |
Each input also carries what its kind needs: max_length for "minecraft:text", options
({id, text} list) for "minecraft:single_option", min, max, step for
"minecraft:number_range". value is the value the server would get now: a string, a
boolean, an option id, or a number.
screen.dialog_input(key, value) sets an input the same way: a string, a boolean, an option
id, or a number within min..max. The dialog's buttons are plain widgets: press them by label.
Clickable body text runs through screen.click_text.
module:event("tick", function()
local d = screen.dialog()
if not d or d.title:string() ~= "Register" then return end
screen.dialog_input("password", "hunter2")
screen.dialog_input("remember", true)
local submit = screen.widget("Submit")
if submit then submit:press() end
end)
Clicking text
screen.click_text(text) runs a component's click event, its own or the first one in its
siblings, the way clicking it would on the open screen. A dialog runs it as a dialog action,
a book turns pages or runs its command, anything else (chat included) takes the vanilla path.
Commands, page changes, dialogs, custom server actions, suggestions and clipboard copies go
through. Links and files are refused: read them from text:style().click instead.
local book = screen.book()
if book then
for _, part in ipairs(book.pages[book.page]:siblings()) do
if part:string():find("Accept") then screen.click_text(part) end
end
end
Signs
A server that asks for text with a sign editor gets its lines when the editor closes.
screen.sign() returns the four lines, screen.set_sign(lines) replaces them (missing ones
become empty).
if screen.sign() then
screen.set_sign({ "1000", "", "", "" })
screen.close()
end
Books
screen.book() returns {pages, page} for a written book or a lectern: pages is a list of
text components, page is 1-based. screen.set_book_page(page) turns to a page;
on a lectern the server turns it.
container
The slots of the open screen handler: chest, shulker, villager, a server's custom menu. These
are handler indices, the ones interaction.click_slot takes, not the player-screen slots
inventory uses.
| Function | Returns |
|---|---|
container.size() |
Slot count of the handler, container and player rows together |
container.storage() |
How many leading slots belong to the container itself |
container.revision() |
Server revision of the handler, 0 until the first content sync |
container.kind() |
Menu type ("minecraft:generic_9x6", "minecraft:anvil", …), nil with only your inventory open |
container.carried() |
Item handle of the stack on the cursor |
container.item(slot) |
Live item handle, or nil |
container.find(id[, from[, to]]) |
First slot holding that item, or nil |
container.count(id[, from[, to]]) |
Total count of that item, optionally within a range |
container.empty([from[, to]]) |
First free slot, or nil when the range is full |
A container screen opens before the server has sent what's inside: the slots read as empty
for the first ticks. revision() flips from 0 once the full content sync arrives, so an
automation that opens chests itself should wait for it before reading slots (the
container_content packet carries the same signal as an event).
Merchants
container.trades() lists the offers of an open merchant, villager or server shop alike, and
returns nil for any other handler. Each trade is {cost_a, cost_b?, result, uses, max_uses, out_of_stock}, the costs and result as item handles with discounts applied.
container.select_trade(index) selects one (1-based, as listed): it moves the price into the
payment slots and tells the server, like clicking it in the list. The result then waits in
slot 2.
for i, trade in ipairs(container.trades() or {}) do
if trade.result:id() == "minecraft:emerald" and not trade.out_of_stock then
container.select_trade(i)
interaction.click_slot(interaction.sync_id(), 2, 0, "quick_move")
break
end
end
Slot layout
The container's own slots come first, your inventory after:
| Range | Holds |
|---|---|
0 … storage() - 1 |
The container, so 54 slots for a six-row chest |
storage() … storage() + 26 |
Your main inventory rows |
storage() + 27 … size() - 1 |
Your hotbar |
With no container open this is your own inventory handler, where storage() covers the
crafting result and grid, and the armor slots count as inventory.
Moving a whole stack
click_slot with "quick_move" is a shift-click: one packet moves the entire stack to the
other side of the handler.
module:event("tick", function()
local open = screen.current()
if not open or not open.handled or open.title:string() ~= "Sell" then return end
local sync, storage = interaction.sync_id(), container.storage()
for slot = storage, container.size() - 1 do
local item = container.item(slot)
if item and item:id() == "minecraft:cobblestone" then
interaction.click_slot(sync, slot, 0, "quick_move")
end
end
end)