Repository navigation
LuaUI Files
Two script trees, one on each side. Neither knows the other's paths; they meet only at the numbers in the protocol files.
The two sides do not run the same Lua:
| Client | Server | |
|---|---|---|
| Runtime | LuaJIT - Lua 5.1 | Lua 5.3 |
| Integers | none; every number is a double | real 64-bit integers |
Bitwise and // operators |
not available | available |
string.pack / string.unpack
|
not available | available |
| Unpacking a table | unpack(t) |
table.unpack(t) |
A helper written for a server window will not necessarily run in the client script. Write each side in its own dialect rather than copying code between them.
Two consequences:
-
Bit twiddling. On the server,
flags & 0xFFis ordinary code. On the client, write the arithmetic out (flags % 256) or use LuaJIT'sbitlibrary. - Big numbers. The client cannot hold an exact integer beyond 2^53, so a value that is exact on the server can come back rounded. See Protocol for what to do about item serials.
The payload reader and writer already hide this: each side has its own
implementation with the same method names, so w:dword(v) means the same thing in
both halves regardless of dialect.
Everything lives under Data\Custom\Scripts.
| File | What it is |
|---|---|
UI\UI.lua |
The window library. ui.Window{...}, layout columns and rows, and the helpers every window script uses. You call it, you do not edit it. |
UI\Registry.lua |
The one place window ids and texture slots are named. Every script reads the same table, and taking an id from it is what stops two scripts picking the same number. |
UI\Protocol.lua |
Your wire contract: per system, its actions, result codes and limits. Mirrored on the server. |
UI\Enums.lua |
Named client values, grouped Enums.Group.VALUE the way the server's Defines\Enums.lua is. |
UI\Net.lua |
Routes incoming window packets to the handler that registered for them, and builds outgoing ones. |
UI\Windows\*.lua |
One file per window. This is where you work. |
UI\Windows\Demo\ |
The example windows that ship with the update, including the placement ruler below. Off with one flag, see Turning the examples off. |
UI\Windows\DemoWindows.lua |
The one file that pulls that folder in. Delete it and none of it loads. |
SimpleXml.lua |
Small XML reader, for windows that load a catalogue from a file. |
Take window ids from Registry.lua, not from your head. Reading a new name
out of that table mints the next free number, so two scripts can never land on the
same one. A number written straight into a script sits outside that count, and if
it collides the second window is never created: the call hands back the first
script's window, so your controls quietly appear on someone else's panel. In dev
mode the debug console says so; in a live client the only symptom
is a layout that makes no sense.
UI\Windows\Demo\UIPlacement.lua is a small HUD panel that answers the
question you have while writing a window: what number do I type here? Press
F11 and it reports, live:
| It shows | Use it for |
|---|---|
cursor |
the mouse position - paste it straight into x/y
|
over |
which Lua window is under the cursor, and its id |
pos |
that window's top-left corner |
size |
its declared size, and in brackets what it covers on screen |
anchor |
what the anchor you gave the helper itself resolved to |
canvas |
the coordinate space all of the above is in |
Placing a control: open your window and the helper, put the cursor where the
control's top-left corner belongs, read cursor, use those two numbers.
Sizing a panel: read cursor at one corner, then the other, and subtract.
Trying an anchor is the same trick applied to the helper itself. Edit its ANCHOR
and MARGIN, press F9, and anchor prints where that combination lands - which
beats guessing whether bottomright with margin = { 6, 140 } clears the panel
already sitting there.
The bracketed pair in size is worth understanding: 280x380 (140x190) means the
window declares 280x380 and covers 140x190 on screen at a render scale of 0.50.
Sizes scale, x/y do not - the same asymmetry the anchors exist for.
Delete the file to drop the helper; nothing depends on it.
Everything in UI\Windows\ is loaded automatically, in alphabetical order, once
the client has connected to the GameServer and received its settings - so the
window scripts only exist from that point on, never on the login screen. There is
no list to register in: dropping a file in the folder is all it takes, and deleting
it removes the window.
Encrypted .usc files are always loaded. Plain .lua files are loaded only in
dev mode (see Lua UI). If a script of yours silently does nothing on
a live server, that setting is the first thing to check.
The scan reads UI\Windows\ itself and does not go into subfolders, so
anything in a subfolder is off unless something asks for it. That is how the
windows shipped as examples are kept out of the way:
UI\Windows\DemoWindows.lua loaded by the scan, and asks for the rest
UI\Windows\Demo\ CoinExchange, Demo, FxShowcase, UIPlacement, ...
One flag in Registry.lua decides:
R.LOAD_DEMO = 1 -- 0 runs the client without any of themDeleting DemoWindows.lua does the same thing, and is the option to reach for
when you would rather not re-encrypt Registry.usc to flip a number.
Your own windows go in UI\Windows\ as usual and are unaffected by the flag.
The same trick works for anything of yours you want loaded conditionally: put it
in a subfolder and require("UI.Windows.<Folder>.<Name>") it from a file the scan
does read.
With dev mode off, the client removes every .lua under its script folder at
start-up, subfolders included, before any script runs. Nothing there was ever
going to load: a live client takes .usc for windows and .lsc for core
scripts, and require has no plain path. A .lua sitting in a live
client is readable source and nothing else.
It is deleted whether or not an encrypted twin sits next to it. The rule is the extension, not what else is in the folder.
Two things follow:
- Never keep your only copy inside the client. Work in a folder of your own and encrypt a copy into the client - see Encrypting scripts.
-
Shipping a
.luaby accident costs you nothing. It is gone on the first launch instead of sitting in the install for players to read.
In dev mode nothing is deleted.
In dev mode, F9 reloads the whole client-side Lua without restarting the client. Edit a window script, press F9, and the change is on screen.
It is a full reset, not a per-file refresh:
- every window is destroyed, then rebuilt by the scripts that run again
- every
Event.onandnet.onhandler is dropped and re-registered - anything a script kept in a local - a cached list, a counter - starts over
-
loadfires again, andingameright after it if you are already in the world
So a window that shows itself from ingame reopens on its own, and because opening
is a request, the server sees a fresh open and answers with fresh state. Nothing
special is needed to get the data back.
The console reports each pass, and a script with a syntax error says so there rather than reloading silently.
F9 is the client only. The GameServer's Lua reloads through its own console command; a client reload never touches the server half of a window.
The libraries in UI\ are not auto-loaded - they arrive through require:
local ui = require("UI")
local R = require("Registry")
local net = require("Net")
local P = require("Protocol")require follows the same rule as the window scan: it loads the encrypted .usc
of a library whenever one is present, and falls back to a plain .lua only in
dev mode. On a live server a library is loaded from its .usc and never from
source, so the UI\ libraries ship encrypted just like the window scripts do.
Bind them to lowercase locals. UI and Net are global tables the client
already provides, and a local with the same name would hide them.
Everything lives under Data\Plugins\LuaAPI.
| File | What it is |
|---|---|
Includes\UIWindow.lua |
Holds what each window registered and routes calls to it. |
Includes\UIPacket.lua |
Reader and writer for payloads - the server's half of the wire format. |
Includes\Net.lua |
The same, for the general (non-window) channel. |
Defines\UIProtocol.lua |
Mirror of the client's Protocol.lua, plus the window ids. |
CallbacksUI.lua |
The entry points the server calls. You do not normally touch it. |
Windows\*.lua |
One file per window - the server half of what you wrote on the client. |
Unlike the client, the server scans nothing. It loads Main.lua, and everything
else is pulled in from there:
LoadScript(BASE .. "Windows\\CoinExchange.lua")Forget that line and the window will open but never answer - the handlers were never registered. It is the single most common mistake when adding a window.
| You want to | Edit |
|---|---|
| add a window | a new file in client UI\Windows\ + a new file in server Windows\ + one LoadScript line |
| name a new window id or texture | client Registry.lua
|
| add actions or result codes | client Protocol.lua and server Defines\UIProtocol.lua
|
| change how a window looks | only its own client script |
The libraries (UI.lua, Net.lua, UIWindow.lua, UIPacket.lua) are shared by
every window - patching them to suit one window is how the next update overwrites
your work.
MuLua Scripting Plugin | Home
🖼️ Lua UI
- File map
- Creating windows
- Client and server
- Designing a window
- Protocol
- Scrolling and scrollbars
- Controls
- Custom text
- Text input
- Debug console
- Bitmap Slicer
- Encrypting scripts
Maps
Monsters
Shared
Version 1.14 | 2026-09-09