KKAWAKIDOCSBack to site
Documentation menu

KAWAKI DOCS / LUA · CLIENT

Markdown

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)