Brodgar client

The Runtime

What the client does with your addon once it has read the manifest. When your code runs. What your Lua may touch. What it costs if it misbehaves. The commands you drive it with.

hafen.event():on("Load", function() hafen.log():write("loaded once, on the login screen") end)
hafen.event():on("SessionEnteredWorld", function(session)
  hafen.log():write(session:user() .. " can be read from here on")
end)
hafen.event():on("Disable", function() hafen.store():flush() end)   -- the last write before teardown

When your code runs

The client loads the enabled addons once when it starts, on the login screen, and again at every :reload. The files named in files run in order, top to bottom, once. Then Load fires. From there your addon does what its event handlers and timers do.

Moment What is ready
Your file bodies The whole hafen API. Your addon’s own vars, each when named. No character.
Load The same, once every file has run. Once for the client.
SessionEnteredWorld The HUD, the map view, the player, that character’s own vars. Once per character reaching the world: again for the same session when it picks another, and for every login in the world at a :reload.
Disable Your last chance to write before the client flushes and tears down: on a reload, on being disabled, on the way out of the client. Once for the client.
Rule Detail
Before a session reaches the world Everything a character owns (HUD, world, map, its vars) is absent. Each read verb’s page states what it gives back then.
Errors An error while a file runs stops that addon’s file and marks it errored in the AddOns manager. An error inside a handler, a timer or a draw callback is logged with your addon’s id. It is isolated from your other handlers, other addons and the client. A callback failing in a way that is not an error (off the end of the stack, out of memory) is contained too. It costs you the addon.

Your addon outlives the character

The addon system is its own layer above the sessions. It is loaded once for the client. It draws above whichever character is on screen, and reaches the drawn one through the API.

Rule Detail
Switching character changes nothing about your addon The same Lua environment, every value held, windows keeping their place, focus and any drag, timers counting. Load fired once. Disable has not fired.
What changes is underneath The session events: SessionAdded when one connects, SessionEnteredWorld when its character can be read, SessionSelected when the screen moves to it, SessionRemoved when it ends. Tabbing between two characters in the world fires SessionSelected only.
Keeping your state valid is yours A widget handle taken under one character names a widget of that character’s tree, which dies with them. Nothing clears what you cached. Read the widget you want when you want it (session:ui():match).

The sandbox

Library Available Absent
The base functions The safe ones: pairs, ipairs, next, select, type, tostring, tonumber, pcall, xpcall, error, assert and their neighbours. require, package, load, loadfile, dofile, loadstring.
string, table, math Whole. —
os The clock half: time, clock, date, difftime. The process and filesystem half: execute, exit, getenv, remove, rename, tmpname, setlocale.
io, debug, coroutine, the Java bridge — Never installed, so type(io) is "nil".
The globals hafen. ADDON (ADDON.id, ADDON.dir, informational). —
Another addon’s code Through hafen.client():addons(): its export, as a copy, and its functions through the client. require.
Rule Detail
LuaJ is 5.2 unpack is table.unpack, with no base-level alias.
os.setlocale Process-wide C state that would change every other addon’s string.format and os.date. A translation is hafen.locale, one catalogue per addon, scoped and dropped with it, touching no number, date or sort order.
The string metatable is the client’s getmetatable("") answers a sentinel rather than the table, so the vocabulary ("x"):upper() resolves through is out of reach and no addon can move another’s strings. Your own string table is yours: writing to it moves string.upper(text) in your addon alone, and leaves ("x"):upper() on the client’s.
string.format is LuaJ’s %d, %i, %u, %x, %X, %o and %c honour flags and width (%5d, %-5d, %05d) and take a fraction whole, towards zero (("%d"):format(3.7) is 3). %f, %e, %g and %s honour neither width nor precision. ("%.1f"):format(2.25) is 2.25, ("%.0f"):format(12) is 12.0, ("%-8s\|"):format("ab") is ab\|. %f writes the double whole: ("%.2f"):format(1/3) is 0.3333333333333333.
Round before you print %d for a whole number, math.floor(x * 10 + 0.5) / 10 under %s for one decimal. Pad with (" "):rep(math.max(0, width - #text)), since a negative count raises rather than answering "".
A number prints with float precision tostring, .. and %s write a whole number whole (12, never 12.0) and a fraction to eight significant digits (1/3 is 0.33333334). They use exponent form from ten million up (12345678.9 is 1.2345679E7) and below a thousandth (0.0001 is 1.0E-4). %d is the way to print an epoch time or an id whole.
Reading a file hafen.asset’s job.
Where it runs Almost everything on the client’s step, one callback after another in the frame. A draw, a press and the two message streams run where the thing they answer is (threading).

Budgets and the watchdog

An addon that never returned would freeze the client. The limits below make that impossible, none reachable by ordinary code.

Limit Detail
Per entry: ten million instructions Every entry into your Lua (a handler, a timer, a draw, a file body) gets a fresh budget. Exhausting it aborts the call with an error naming the runaway. It counts instructions, not time, so a loop over a few hundred hafen.* calls burns a frame without nearing it.
An entry inside an entry Two callbacks running at once on the two threads that can be inside your Lua each spend their own. A callback your callback calls into is an entry too and hands the budget back on the way out. A call into another addon’s exported function, or a callback it calls back, is an entry into that addon.
Per tick: 10 ms, sustained An addon whose total Lua time in one tick exceeds the budget for thirty consecutive ticks is auto-disabled. The reason is on its AddOns manager row and in the console. One heavy load or a stalled frame resets the count.
On the way out: one budget for every Disable together The client leaves when it is spent, whatever a handler is still doing. Stated with what quitting writes.

An auto-disable lasts until the next load: fix it, then :reload. A library’s auto-disable takes its loaded hard dependants with it, each reading auto-disabled (needs <id>). The enable state is untouched. hafen.client():profiling() reports what each addon spends per frame.

When a failure is fatal

A handler that recurses without bound overflows the client’s stack. One that builds a table without bound exhausts its memory. Both are raised by the JVM, below Lua, and pcall never catches them.

Rule Detail
Contained, paid for with your addon The failure is logged with your addon’s id and the stack goes to the terminal. At the end of that tick your addon is torn down as the CPU budget tears one down. Disable fires, vars are flushed, everything owned is given back. Its row reads auto-disabled (…), naming what was raised, until the next load.
The whole addon stops, not the one callback A client that has run out of stack under your handler holds nothing you could read. Every other addon, the client and the frame keep running.

The console commands

Press : to open the client’s command line.

Command Does
:reload Rebuild the addon layer from disk: the developer loop.
:addons List every discovered addon with its status.
:addons enable <id> Enable an addon, applied on the next :reload.
:addons disable <id> Disable one, applied on the next :reload.
:lua <expression> Evaluate Lua against the live API and print the result.
:store Print where the client keeps its recorded map and its resource cache: the folder under %APPDATA%, or with -Dhaven.store=sqlite the two files under savedata/ with their entry counts and sizes, and the last sweep’s examined and dropped counts.
:store sweep In sqlite mode, drop every cached resource the client’s resource pack holds at the same or a newer version, then print the report; it also runs once at start. In files mode nothing is swept.
:lua hafen.session():current():world():gob():count("terobjs/tree")
:lua for _, session in ipairs(hafen.session():list()) do print(session:user(), session:chat():count()) end
Rule Detail
:lua Prints its result as JSON prefixed lua=, in the console and in full on the terminal. Your own console, not sandboxed: every protected verb answers, the fastest way to try a call and to break something. The instruction watchdog applies.
A macro is a kept :lua line A body on the Macros page runs in this same Lua: the same globals, the same permissions, the same print, the same watchdog. It is statements only, and its value is never shown.
:reload undoes anything typed there The console owns what it puts up, from a typed line or a macro, as an addon does: windows, gob labels, overlays, hotkeys, clips. A reload takes it back by the same teardown.
print inside :lua Answers in the System channel too, one line per print, tab-separated, where lua= and :threads land. The terminal keeps its copy whole. A line shows once finished, so an io.write with no newline waits. Before a HUD is up the terminal is the whole of it. In an addon, print goes to the terminal alone. hafen.log():write reaches the player.
Reserved lua, addons and reload cannot be taken over, and neither can store or any other command the client itself owns. Addons add commands with hafen.console.
Saying a line session:console():run(line) runs any of these and any client command at the character you address, without the colon (run("reload")). run("lo") logs out the character it was said at. Needs console.run, the widest key, since :lua is among the commands it reaches.

What a reload keeps, and what it drops

:reload rebuilds the addon layer only: every session stays connected, the world loaded, the client’s own windows as they are.

Step Detail
Teardown Each addon is torn down. Every folder the AddOns manager marked for removal is deleted and every staged install moved into addons/, with nothing loaded.
Rebuild The folder and the enabled set are re-read. The enabled addons run from disk and Load fires. SessionEnteredWorld fires for every session in the world, the one on screen first, each once. A session not yet in the world is not announced.
Dropped and re-created Event subscriptions, timers, hotkeys, console commands and input hooks. Your windows and overlays. World ghosts, sprites and objects. Loaded assets, your stylesheet, sounds you started. The client’s widgets you hid, moved or replaced, handed back as the user saw them. Your vars are written first, at Disable.
Released on its own Each holding is released separately: one that fails is a console line naming it, and everything else is released, so a teardown finishes. A disable frees what that addon had and touches no other’s.
Two things stay A font family you load goes into the one namespace the client shares. $font[…] resolves it. A registered name draws the face that took it first. Your handle draws your file. A sound of several clips leaves an entry in the client’s audio cache.
Kept Everything outside the addon layer: a :reload never costs you your login.

What quitting writes

Rule Detail
Vars first Closing the window or :q writes every logged-in character’s own vars and your addon’s own to your file, whatever the timer or flush() did. A row a table or statement wrote is in the file already, and the quit closes it.
Disable fires first An addon computing its state at teardown has that write picked up by the flush that follows.
Disable cannot hold the exit open The addons share one wall-clock budget of two seconds on the way out, spent in reverse load order. A handler still running when it is spent is abandoned, with a terminal line; an addon whose turn comes after that gets no Disable, with a terminal line naming it. The flush runs no code of yours and happens either way.
A crash or a kill The client runs nothing. The automatic save every thirty seconds is the whole of what covers it.

See Also