UI
UI is the high-level client API for building interactive interface trees from Lua.
Typical use:
- server ships a resource with
client.lua - client script creates UI with
UI.* - gameplay actions go back to server through
Event/Command/service APIs
Error Handling Pattern​
- Success:
err == nil - Failure:
erris an error code string
Scope Model​
UIis client-only.- Server does not directly render UI.
- Server influences UI by shipping client resources and by sending events/data to clients.
- Runtime may render engine-owned overlay UI that is outside Lua ownership (for example bottom-right
mtadm vX.Y.Zwatermark). - Pointer visibility is managed separately through
Cursor. - World-space overlay lines/wireframes are handled by
Visual, not byUI.
This page documents the currently implemented UI runtime.
If a control or event is not listed here, scripts should not depend on it.
Root Model (LuaUI vs NodeUI)​
LuaUIis the engine/internal CanvasLayer root (implementation detail).NodeUIis the logical Lua API domain for UI graph ownership.- Public scripts should use
UI.*(not raw internal engine paths).
Resource ownership:
- each resource writes only to its own UI subtree.
- canonical logical root:
/NodeUI/resources/{resourceName}/... - resource stop/restart automatically removes owned UI subtree.
resourceNameis resolved from the executing Lua script resource context.
Runtime Model​
UI is a retained-tree system.
That means:
UI.Create(...)creates persistent controls owned by the resource.UI.SetProps(...)updates existing controls in place.UI.ClearResource(...)destroys the full resource subtree.
Production guidance:
- do not
UI.ClearResource(...)on every small state change - prefer creating a stable root once, then patch child props in place
- for overlays/panels, prefer
visible = true/falseover destroy/recreate - rebuild only when structure changes materially (for example a different tab set or a different row count that cannot be patched)
This matters for performance:
- creating hundreds of controls in one frame is expensive
- clearing and recreating a large tree on every open/close or every metric update is not a good production pattern
- the intended fast path is retained controls plus in-place updates
Window/Dialog API​
The runtime now exposes built-in window and dialog kinds.
Goals:
- reduce repeated Lua boilerplate for top-level shells
- keep title/close/drag behavior consistent across resources
- keep child composition on the existing
UIparent model
Current first-version scope:
- no automatic focus/capture behavior here yet
- no modal-stack redesign yet
- no built-in window persistence/state saving
Semantics:
window
- movable editor/tool window shell
- intended for inspectors, browser panes, settings, hierarchy panes
- children parented to the
windowid are inserted into the window content area automatically
dialog
- centered dialog shell
- intended for confirm/open/save/picker dialogs
- children parented to the
dialogid are inserted into the dialog content area automatically
Close behavior:
closeMode = "hide":- built-in close button hides the existing window/dialog instance
- moved position is preserved because the same shell stays alive
close_requestedis still emitted
closeMode = "destroy":- built-in close button only emits
close_requested - the resource is responsible for removing/recreating the UI element if it wants destroy-on-close behavior
- built-in close button only emits
Function List​
| Function | Description | Scope |
|---|---|---|
UI.GetViewportSize | Read the current client viewport size for layout. | C |
UI.GetCountryInfo | Resolve a country code into UI-ready country display data. | C |
UI.GetMetrics | Inspect resolved runtime metrics for an existing UI element. | C |
UI.Create | Create one UI element. | C |
UI.SetParent | Reparent one UI element. | C |
UI.SetProps | Patch UI element properties. | C |
UI.TreeSelect | Apply native selection to a tree_view. | C |
UI.TreeClearSelection | Clear native selection on a tree_view. | C |
UI.Get | Read one UI element. | C |
UI.List | List child UI elements. | C |
UI.Remove | Remove one UI element. | C |
UI.Bind | Bind UI interaction callback. | C |
UI.Unbind | Remove one UI callback binding. | C |
UI.ClearResource | Remove all UI owned by a resource. | C |
Event List​
| Event | Description | Scope |
|---|---|---|
ui:created | Fired after a UI element is created. | C |
ui:updated | Fired after a UI element is updated. | C |
ui:removed | Fired after a UI element is removed. | C |
ui:action | Fired after a bound UI interaction callback executes. | C |
Element Kinds (v1)​
containerwindowdialoglabellinecirclerich_textimagebuttonmenu_buttoncheck_buttoncolor_buttoninputsliderstackscroll_containertab_containeritem_listtable_viewtree_view
tab_container includes Godot's built-in tab header bar.
There is currently no separate tab_bar kind in Lua UI API.
Current runtime note:
stack,scroll_container,input, andsliderare implemented.table_viewis intended for high-density lists such as scoreboards or admin lists.tree_viewis intended for hierarchy editing (parent/child UI).- While a native
inputhas focus, Lua input binds/hotkeys and the core chat-open action are suppressed. - If a Lua UI should be mouse- or pointer-interactive, the resource should also acquire a cursor handle through
Cursor.Acquire(...).
Common Properties​
name(string)visible(boolean)x,y,width,height(number, pixels)mouseFilter(ignore|pass|stop, forcontainer)draggable(boolean, forcontainer)title(string, forwindow/dialog)closable(boolean, forwindow/dialog)closeMode(hide|destroy, forwindow/dialog; defaulthide)centered(boolean, fordialog)showHeader(boolean, forwindow)draggable(boolean, forwindow; defaulttrue)cornerRadius(number, forcontainer/window/dialog/button/menu_button)contentPadding(number, forbutton/menu_button; applies equal inner padding on all sides)text(string, forlabel/button/menu_button/check_button)disabled(boolean, forbutton/menu_button/check_button/color_button)backgroundColor(color table, forbutton/menu_button)iconSource(string, forbutton/menu_button; same source rules asimage.source)tooltip(string, forbutton/menu_button/color_button)focusable(boolean, forbutton/menu_button/tree_view; defaulttrue)x1,y1,x2,y2(number, forline)thickness(number, forline/circle)x,y,radius(number, forcircle; center-positioned)filled(boolean, forcircle)text(string, forrich_text; BBCode-enabled)source(string, forimage; supportsres://...,user://..., or distributed resource assets viaresourceName:path/to/file.svg)stretchMode(scale|tile|keep_aspect|keep_aspect_centered|keep_aspect_covered, forimage)placeholder(string, forinput)checked(boolean, forcheck_button)value(color table{ r, g, b, a }, forcolor_button)editable(boolean, forinput)secret(boolean, forinput)value(number, forslider)min(number, forslider)max(number, forslider)step(number, forslider)editable(boolean, forslider)wrap(boolean, forlabel)clipText(boolean, forlabel)autoresizeHeight(boolean, forlabel)horizontalAlignment(left|center|right|fill, forlabel)verticalAlignment(top|center|bottom|fill, forlabel)direction(vertical|horizontal, forstack)gap(number, forstack)contentWidth(number, forscroll_container)contentHeight(number, forscroll_container)scrollX(number, forscroll_container)scrollY(number, forscroll_container)value(string/number/bool, kind-specific)tabs(table<string>, fortab_container)selectedIndex(number, 0-based, fortab_container)items(table<string>, foritem_list)selectedIndex(number, 0-based, foritem_list)items(table<table>, formenu_button)columns(table<string>, fortable_view)showHeaders(boolean, optional, defaulttrue, fortable_view)columnWidths(table<number>, optional, fortable_view)columnExpandRatios(table<number>, optional, fortable_view)columnAlignments(table<string>, optional, fortable_view)rowHeight(number, optional, fortable_view)rows(table<table>, fortable_view)selectedIndex(number, 0-based, fortable_view)nodes(table<table>, fortree_view)showHeaders(boolean, optional, defaulttrue, fortree_view)hideRoot(boolean, optional, defaulttrue, fortree_view)allowReparent(boolean, optional, defaultfalse, fortree_view)focusable(boolean, optional, defaulttrue, fortree_view)selectedIndex(number, 0-based, fortree_view)selectedNodeId(string, optional, fortree_view)
Layout semantics:
- when
options.parentis set,x/y/width/heightare interpreted in the parent control's local space. - when no parent is set, layout is relative to the resource UI root.
- children under
stackare laid out automatically by the stack direction/gap rules. - children under
scroll_containerrender inside a clipped scrollable content area. scroll_containercan declare virtual content size throughcontentWidth/contentHeight.- if
contentWidth/contentHeightare omitted or<= 0, the runtime auto-measures content bounds from the actual child controls. scroll_containercan be positioned programmatically throughscrollX/scrollY.
Current layout limitations:
- layout is absolute-position based
- there is no padding/margin API on
stackyet - there is no public ellipsis mode yet
autoresizeHeighthelps labels grow to fit wrapped content, but only for labels
Performance guidance:
- prefer a persistent root panel that is shown/hidden
- prefer updating row text/images in place over rebuilding full lists
- prefer lazy creation of expensive subtrees such as tabs or diagnostics panes
- avoid large full-tree rebuilds in high-frequency event paths
- for very large lists, prefer virtualization or a bounded row pool instead of one control per logical row
- prefer scroll virtualization for long tables instead of one control per logical row
- prefer
table_viewover many generic controls when the UI is fundamentally a live table - prefer
tree_viewwhen the UI needs hierarchy + drag/drop reparent semantics
Drag guidance:
draggable = trueis intended for retained containers such as windows/panels- dragging updates the live control position immediately
- if a resource wants drag position to survive a later rebuild, it should store the last
drag_endcoordinates and reuse them on the next render - a visible top-level draggable
containeris treated as an interactive foreground window and automatically suspends gameplay input while it is open
Resource asset note:
- if a Lua resource wants to use its own images, the files must be declared in the resource
meta.jsonassets[]list - once the resource is started, those assets are distributed to clients like other resource packets
image.source = "resourceName:path/to/file.svg"resolves against the distributed asset payload for that resourcebutton.iconSource = "resourceName:path/to/file.png"resolves the same waymenu_button.iconSource = "resourceName:path/to/file.svg"resolves the same way
menu_button.items entries support:
text(string)id(string)disabled(boolean)separator(boolean)
menu_button uses Godot's built-in popup menu behavior. When an item is picked, item_selected dispatches a payload table with:
index(number)text(string)id(string)
color_button uses Godot's native ColorPickerButton. When the user changes the color, color_changed dispatches a payload table with:
r(number)g(number)b(number)a(number)
Example:
local pickerId, err = UI.Create("color_button", {
name = "marker_color",
x = 16,
y = 40,
width = 220,
height = 32,
value = { r = 1.0, g = 0.5, b = 0.0, a = 0.85 }
})
if not err then
UI.Bind(pickerId, "color_changed", function(ctx)
local value = type(ctx.value) == "table" and ctx.value or ctx
Logger.Info(string.format(
"picked color r=%.2f g=%.2f b=%.2f a=%.2f",
tonumber(value.r) or 0,
tonumber(value.g) or 0,
tonumber(value.b) or 0,
tonumber(value.a) or 0
))
end)
end
Functions​
UI.GetViewportSize​
C Client Only
local viewport, err = UI.GetViewportSize()
Returns:
viewport(table | nil)err(nil| string)
Viewport table fields:
width(number)height(number)
Example:
local viewport, err = UI.GetViewportSize()
if err then
Logger.Error("UI.GetViewportSize failed: " .. tostring(err))
return
end
local maxPanelBottom = math.floor(viewport.height * 0.80)
local panelY = 120
local panelHeight = math.max(120, maxPanelBottom - panelY)
UI.GetCountryInfo​
C Client Only
local info, err = UI.GetCountryInfo(countryCode)
Parameters:
countryCode(string): two-letter country code such as"de"or"us". Unknown values resolve to the fallback country entry.
Returns:
info(table | nil)err(nil| string)
Country info table fields:
code(string)name(string)flagIcon(string): UI-ready texture resource path for use withimage.source
Example:
local info, err = UI.GetCountryInfo(player.countryCode or "xx")
if not err and info then
UI.Create("image", {
name = "flag",
x = 8,
y = 8,
width = 20,
height = 14,
source = info.flagIcon
}, { parent = rowId })
end
UI.GetMetrics​
C Client Only
local metrics, err = UI.GetMetrics(uiId)
Returns resolved runtime metrics for an existing UI element.
Returns:
metrics(table | nil)err(nil| string)
Common fields:
widthheightvisiblevisibleInTree
table_view currently adds:
contentHeightcontentWidthheaderHeighttotalContentHeightheadersVisiblescrollBarVisiblescrollBarWidth
UI.Create​
C Client Only
local uiId, err = UI.Create(kind, props, options)
Parameters:
kind(string): element kind.props(table): initial element props.options(table, optional):parent(string): parentuiId(default resource root)name(string)tabIndex(number, optional): whenparentis atab_container, choose which tab page receives this element (0-based).
If omitted, current tab is used.
Default parent resolution:
- when
options.parentis omitted, parent defaults to/NodeUI/resources/{currentResource}root.
Returns:
uiId(string | nil)err(nil| string)
Label example:
local labelId, err = UI.Create("label", {
name = "server_status",
x = 16,
y = 16,
width = 320,
height = 48,
text = "This text stays inside the assigned rectangle.",
wrap = true,
clipText = true,
autoresizeHeight = true,
horizontalAlignment = "left",
verticalAlignment = "top",
visible = true
})
Rich text example:
local nameId, err = UI.Create("rich_text", {
name = "colored_name",
x = 16,
y = 72,
width = 320,
height = 24,
text = "Don[color=#ff0000]iel<3[/color]",
wrap = false,
visible = true
})
Image example:
local info, err = UI.GetCountryInfo("de")
if not err and info then
UI.Create("image", {
name = "flag_de",
x = 16,
y = 104,
width = 24,
height = 16,
source = info.flagIcon,
stretchMode = "keep_aspect_centered",
visible = true
})
end
Stack example:
local actionsId, err = UI.Create("stack", {
name = "actions",
x = 16,
y = 72,
width = 260,
height = 160,
direction = "vertical",
gap = 8,
visible = true
})
UI.Create("button", {
name = "kick_btn",
width = 260,
height = 36,
text = "Kick"
}, { parent = actionsId })
UI.Create("button", {
name = "ban_btn",
width = 260,
height = 36,
text = "Ban"
}, { parent = actionsId })
Scroll container example:
local scrollId, err = UI.Create("scroll_container", {
name = "details_scroll",
x = 320,
y = 16,
width = 420,
height = 300,
visible = true,
contentHeight = 1200,
scrollY = 0
})
UI.Create("label", {
name = "details_text",
x = 0,
y = 0,
width = 400,
height = 240,
text = "Long content...",
wrap = true,
clipText = true,
visible = true
}, { parent = scrollId })
UI.Bind(scrollId, "scroll_changed", function(ctx)
local offsetY = ctx.value and ctx.value.y or 0
Logger.Info("Scroll offset Y: " .. tostring(offsetY))
end)
Window example:
local windowId, err = UI.Create("window", {
name = "inspector_window",
title = "Inspector",
x = 1320,
y = 120,
width = 360,
height = 640,
visible = true,
closable = true,
draggable = true,
cornerRadius = 8
})
UI.Create("label", {
name = "inspector_title",
x = 16,
y = 16,
width = 220,
height = 24,
text = "Object Inspector",
visible = true
}, { parent = windowId })
UI.Bind(windowId, "close_requested", function()
UI.SetProps(windowId, { visible = false })
end)
Dialog example:
local dialogId, err = UI.Create("dialog", {
name = "confirm_delete",
title = "Delete Map",
width = 420,
height = 220,
visible = true,
closable = true,
centered = true,
cornerRadius = 8
})
UI.Create("label", {
name = "confirm_text",
x = 16,
y = 16,
width = 260,
height = 24,
text = "Delete this map?",
visible = true
}, { parent = dialogId })
Table view example:
local tableId, err = UI.Create("table_view", {
name = "scoreboard_table",
x = 24,
y = 120,
width = 952,
height = 520,
visible = true,
columns = { "ID", "Name", "Country", "Ping", "FPS" },
showHeaders = true,
columnWidths = { 64, 0, 120, 80, 80 },
columnExpandRatios = { 0, 5, 2, 0, 0 },
columnAlignments = { "left", "left", "right", "right", "right" },
rowHeight = 26,
rows = {
{
cells = {
{ text = "1" },
{ text = "Don#ff0000iel<3", parseColorCodes = true },
{ text = "AT", icon = "res://Game/Assets/Icons/flag-icons-main/flags/4x3/at.svg", iconMaxWidth = 20, horizontalAlignment = "right", iconAlignment = "right" },
{ text = "21", horizontalAlignment = "right" },
{ text = "144", horizontalAlignment = "right" }
}
}
},
selectedIndex = 0
})
UI.Bind(tableId, "item_selected", function(ctx)
local rowIndex = ctx.value or -1
Logger.Info("Selected row: " .. tostring(rowIndex))
end)
table_view row model:
- each row is a table with
cells - each
cells[i]entry may contain: text(string)icon(string texture path, optional)iconMaxWidth(number, optional)horizontalAlignment(left|center|right|fill, optional)iconAlignment(left|center|right, optional)parseColorCodes(boolean, optional; renders inline#RRGGBBsegments like chat)
Current table_view limits:
- rendered by one native table control for predictable sizing and low per-row overhead
- built for dense tabular data, not rich text paragraphs
- cells support plain text plus optional icon
- inline color-code rendering is supported for short label-style cells via
parseColorCodes = true
Tree view example:
local treeId, err = UI.Create("tree_view", {
name = "elements_tree",
x = 20,
y = 90,
width = 520,
height = 360,
columns = { "Name", "Type", "ID" },
showHeaders = true,
hideRoot = true,
allowReparent = true,
nodes = {
{ id = "root_empty", parentId = "", cells = { "[E] Root", "EMPTY", "root_empty" } },
{ id = "cube_1", parentId = "root_empty", cells = { "[C] Cube 1", "CUBE", "cube_1" } },
{ id = "sphere_1", parentId = "root_empty", cells = { "[S] Sphere 1", "SPHERE", "sphere_1" } }
},
selectedNodeId = "cube_1"
})
UI.Bind(treeId, "item_selected", function(ctx)
local rowIndex = tonumber(ctx.value) or -1
Logger.Info("Tree selected row index: " .. tostring(rowIndex))
end)
UI.Bind(treeId, "item_activated", function(ctx)
local rowIndex = tonumber(ctx.value) or -1
Logger.Info("Tree activated row index: " .. tostring(rowIndex))
end)
UI.Bind(treeId, "item_reparented", function(ctx)
local payload = ctx.value or {}
-- payload.sourceId
-- payload.targetId
-- payload.parentId
-- payload.dropSection (-1,0,1)
end)
Input example:
local searchId, err = UI.Create("input", {
name = "search_box",
x = 16,
y = 16,
width = 280,
height = 34,
text = "",
placeholder = "Filter players...",
editable = true,
secret = false,
visible = true
})
UI.SetParent​
C Client Only
local err = UI.SetParent(uiId, parentId, options)
Parameters:
uiId(string)parentId(string)options(table, optional)
Returns:
err(nil| string)
UI.SetProps​
C Client Only
local err = UI.SetProps(uiId, patch, options)
Parameters:
uiId(string)patch(table): partial property update.options(table, optional)
Returns:
err(nil| string)
UI.Get​
C Client Only
local element, err = UI.Get(uiId)
Returns:
element(table | nil)err(nil| string)
UI.TreeSelect​
C Client Only
local err = UI.TreeSelect(uiId, nodeIds, selectedNodeId, options)
Parameters:
uiId(string): targettree_viewidnodeIds(table<string>): selected node idsselectedNodeId(string, optional): focused/current node idoptions(table, optional)resource(string, optional): explicit resource scope override. default is current script resource.
Returns:
err(nil| string)
Notes:
- uses native tree selection directly
- does not rebuild the tree structure
UI.TreeClearSelection​
C Client Only
local err = UI.TreeClearSelection(uiId, options)
Parameters:
uiId(string): targettree_viewidoptions(table, optional)resource(string, optional): explicit resource scope override. default is current script resource.
Returns:
err(nil| string)
Notes:
- uses native tree deselection directly
- does not rebuild the tree structure
UI.List​
C Client Only
local elements, err = UI.List(parentId, options)
Parameters:
parentId(string, optional): default resource root.options(table, optional):recursive(boolean, defaultfalse)kind(string or table)limit(number)
Returns:
elements(table | nil)err(nil| string)
UI.Remove​
C Client Only
local err = UI.Remove(uiId, options)
Parameters:
uiId(string)options(table, optional):recursive(boolean, defaulttrue)
Returns:
err(nil| string)
UI.Bind​
C Client Only
local bindId, err = UI.Bind(uiId, eventName, handler, options)
Parameters:
uiId(string)eventName(string):clicked,pressed,toggled,hover_enter,hover_exit,drag_start,drag,drag_end,close_requested,text_changed,focus,blur,tab_changed,item_selected,item_activated,item_reparented,scroll_changedhandler(function)options(table, optional):resource(string, optional): explicit resource scope override. default is current script resource.
Handler signature:
function handler(ctx)
-- ctx.uiId
-- ctx.eventName
-- ctx.value (for tab_changed/item_selected/item_activated: selectedIndex as number)
-- (for menu_button item_selected: { index, text, id, ... })
-- (for item_reparented: { sourceId, targetId, parentId, dropSection })
-- (for drag_*: { x, y, mouseX, mouseY, deltaX, deltaY })
-- (for scroll_changed: { x, y })
-- ctx.timestamp
end
Current event support:
clicked/pressed/hover_enter/hover_exit/drag_start/drag/drag_end:containerclose_requested:window,dialogdrag_end:windowclicked/pressed:buttonitem_selected:menu_buttontoggled:check_buttoncolor_changed:color_buttonfocusblurtext_changed:inputvalue_changed:slidertab_changed:tab_containeritem_selected:item_list,table_view,tree_viewitem_activated:item_list,table_view,tree_viewitem_reparented:tree_viewscroll_changed:scroll_container
text_changed dispatches in both cases:
- when the user edits a native
input - when script code changes
textorvaluethroughUI.SetProps
Not implemented yet:
released
Returns:
bindId(string | nil)err(nil| string)
UI.Unbind​
C Client Only
local err = UI.Unbind(bindId)
Returns:
err(nil| string)
UI.ClearResource​
C Client Only
local err = UI.ClearResource(resourceName)
Parameters:
resourceName(string, optional): default current resource.
Default resource resolution:
- when
resourceNameis omitted, current script resource is used.
Returns:
err(nil| string)
Events​
All events use the standard event envelope:
event.nameevent.payloadevent.sourceevent.targetevent.timestampevent.correlationIdevent.version
ui:created (Event Bus)​
Payload:
uiId,kind,parentId,resource,props
ui:updated (Event Bus)​
Payload:
uiId,patch,resource
ui:removed (Event Bus)​
Payload:
uiId,resource
ui:action (Event Bus)​
Payload:
uiId,eventName,value,resource
Security and Authority Notes​
- UI is client-local and non-authoritative.
- UI callbacks must not be treated as trusted gameplay authority.
- To change server-authoritative state, call server-validated APIs (
Command,Event,Chat,Node.RequestSim, etc.).
Error Codes​
ERR_NOT_FOUNDERR_INVALIDERR_INVALID_KINDERR_INVALID_PARENTERR_INVALID_HANDLERERR_PERMISSION_DENIEDERR_RESOURCE_SCOPEERR_BUSY
Quick Examples​
Create container + label + button:
local panelId, err = UI.Create("container", {
name = "mainPanel",
x = 32, y = 32, width = 420, height = 180
})
local labelId = UI.Create("label", {
text = "Race Control",
x = 16, y = 16, width = 200, height = 24
}, { parent = panelId })
local btnId = UI.Create("button", {
text = "Start Race",
x = 16, y = 64, width = 140, height = 36
}, { parent = panelId })
Bind button click and request server action:
UI.Bind(btnId, "clicked", function(ctx)
Command.Execute("/race start", { sourceType = "chat" })
end)
Use default Godot-style tab headers with tab_container:
local tabsId = UI.Create("tab_container", {
x = 32, y = 240, width = 520, height = 46,
tabs = { "Players", "Resources", "Server", "Bans" },
selectedIndex = 0
})
UI.Bind(tabsId, "tab_changed", function(ctx)
local selectedIndex = tonumber(ctx.value) or 0
print("Selected tab index: " .. tostring(selectedIndex))
end)
Place content under specific tabs:
local playersPanel = UI.Create("container", {
x = 0, y = 0, width = 900, height = 500
}, {
parent = tabsId,
tabIndex = 0 -- Players
})
local resourcesPanel = UI.Create("container", {
x = 0, y = 0, width = 900, height = 500
}, {
parent = tabsId,
tabIndex = 1 -- Resources
})
Use a scrollable player list with item_list:
local listId = UI.Create("item_list", {
x = 32, y = 300, width = 360, height = 260,
items = { "#1 PlayerOne", "#2 PlayerTwo", "#3 PlayerThree" },
selectedIndex = 0
})
UI.Bind(listId, "item_selected", function(ctx)
local rowIndex = tonumber(ctx.value) or 0
print("Selected player row: " .. tostring(rowIndex))
end)