Brodgar client

API Conventions: The Shared Vocabulary

The rules that hold across the whole hafen.* API: how it is spelled, what a read gives back, what a write costs. Every reference page assumes this one. The catalogue of things a verb can be handed is references. What a plain table of numbers looks like is shapes.

local session = hafen.session():current()          -- a section is called; everything after is a colon verb
local window = hafen.ui():window():title("Scout")  -- one argument writes, and hands the object back
hafen.log():write(window:title())                  -- no argument reads
for _, gob in ipairs(session:world():gob():list("rabbit")) do   -- :list(filter) is the array
  hafen.log():write(tostring(gob:position()))
end

The grammar

A section is called, everything after it is a colon verb, and arity is the verb. A section groups one subsystem and owns one reference page. One large enough for several pages owns a folder with a hub.

Sections: you call one

Rule Detail
The section object hafen.time is the section, hafen.time() the section object, every verb a colon call on it (hafen.time():clock()). The same object every time: hafen.time() == hafen.time(), and calling a section in a draw callback allocates nothing.
No arguments Where a section holds one thing, the section object is that thing. hafen.timer() is the collection of your timers, hafen.console() your console commands, hafen.session() the logins the client holds.
Reached, not mounted What names one character’s state hangs off the Session that names the character, one call further in: session:world(), session:player(). Everything else about it is the rule above unchanged.

Verbs: arity is the verb

A verb with no argument reads. The same verb with one writes and hands the object back, so writes chain: window:title() reads, window:title("Scout"):size(180, 48) writes twice.

Rule Detail
One name per property No getX, setX or clearX. A boolean is written window:visible(true) and named as a bare adjective, never isX: kin:online(), gob:player().
A boolean argument is true or false In Lua 0 and "no" are true, so a verb taking Lua’s word would read window:visible(0) as show it and video:shadows("no") as shadows on. The type is what is asked, as for a string and a number. What your own callback returns keeps Lua’s answer: a filter ending in return gob:name() is an ordinary filter.
A surplus argument raises kin:online(1) and binding:key("F5", "x") raise naming how many arguments the verb takes and how many it got.
Arguments that are not writes Addressing (collection:get(key), request:header(name), session:world():grid():at(position)). Actions (item:drop(count), session:world():place(position, angle, button, mods), sound:play(volume)). Conversions (position:offset(dx, dy), position:distance(other), grid:tile(cell)).

Collections: the noun is the kind, the verb is how many

A set you can address into is reached by the singular kind name and hands back a collection object, never a bare array. :list() is the enumerating verb wherever it appears (the control named for a list is hafen.ui():listbox()).

Verb Gives you
:list(filter) A plain array of members, empty rather than nil.
:count(filter) How many.
:find(filter) The first member that matches, or nil.
:get(key) One member by its key, where members have keys. A miss is below.
:add(...) A new member, where the collection can create one.
:remove(key_or_member) The collection, so removals chain, where it can destroy one.
Rule Detail
A distinguished member is a verb on its collection :current(), :selected(), :leader(), :pursuing(). The member carries no flag of its own, since objects are interned: session:party():leader() == member is the exact test.
One object, or a view A section’s collection is one object every call (hafen.map():marker()). A collection off a thing (gob:overlay(), meter:segment(), quest:conditions()) or a partition (session:char():skill():buyable(filter)) is re-derived per call. Two calls are not ==. Neither outlives what it came off. Identity lives on members that carry a key: gob:overlay():get("tag") is one overlay.
An object, not a sequence #collection, collection[1], pairs(collection) and ipairs(collection) are refused naming what to write: collection:list() is the array.
A store table A collection whose members are rows and whose filter is a SQL clause: :list(clause, ...), :count(clause, ...), :find(clause, ...) take what follows FROM <table> with a value per ?. :get(key, ...) addresses a row. :put(row) stands where :add would (tables).

get: what a key that names nothing answers

A miss gives you Collections
nil Every collection not named below.
An object, so :exists() is the question hafen.session(), hafen.sound(), session:world():gob(), session:actionbar(), session:fight():action(), keybindings():binding(), hafen.client():addons().
An error naming the keys there are hafen.asset(), hafen.font(), session:char():attr(), hafen.map():display().

A collection whose members have no key has no :get and says what to reach for. Two buffs can share a resource. A marker’s only id is one this client mints. A timer is only the one you were handed. hafen.timer():get(1) raises: hafen.timer() has no verb 'get' — a timer has no key: hafen.timer():after(s, fn) and hafen.timer():every(s, fn) hand you the timer they make, and hafen.timer():list() is every one of yours.

Every index is 1-based

:list() hands back a 1-based array, and the position a member reports is its position in that array: session:actionbar():get(1) == session:actionbar():list()[1], session:speed():get(1) is crawl. Where the wire’s number differs it is a verb of its own: slot:index() is the position and slot:wire() the number the server’s setbelt carries. card:index()/card:wire(), speed:index()/speed:wire() and action:index()/action:wire() are the same pair. Nothing in this API takes a wire number.

Endings: the receiver’s kind picks the word

Nothing has to be ended: what your addon takes is given back on reload or disable. An ending is what you write to end it now.

Verb Ends Called on
:off() A subscription. The Sub any :on handed you.
:remove(key_or_member) A member of a collection. The collection, never the member.
:release() A layer or a hold over what the client owns. A rule, a sheet, a grab, a map-overlay hold.
:destroy() A thing your addon built. A widget.
:cancel() Something in flight. A timer, a request, the open radial menu.
:stop() A sound in the air. The sound.
:close(code, reason) A connection to a server. The connection.
:close() A voice link. The link.
:finish() A profiling scope. The scope.
Rule Detail
The collection ends its members hafen.asset():remove(asset) frees a file, hafen.session():remove(session) ends a login. What is a member of nothing ends on itself.
Endings chain Every ending hands the receiver back. Where a page says one is idempotent, a second call answers the receiver again.
Not endings item:drop(count) is a game action. session:close() is the login’s own act. widget:revert(), widget:replace(nil) and widget:size(nil) undo your layer.

Events: a subject and an edge

An event key is a subject and an edge. A new key takes the first edge below true of the moment it names. Where none is (Load, Disable, ManeuverUsed) the word is the moment.

Edge Word Examples
It appeared Added GobAdded, BuffAdded, SessionAdded, FlowerMenuAdded, Added on a selector watch.
It went Removed GobRemoved, MeterRemoved, SessionRemoved, Removed on a widget.
It changed Changed MeterChanged, KinChanged, MarkerChanged.
It crossed a threshold EnteredWorld SessionEnteredWorld.
It was picked Selected SessionSelected, ChannelSelected, OpponentSelected.
It was clicked Clicked GhostClicked, SpriteClicked, ObjectClicked, PatchClicked.
Rule Detail
The subject is singular MarkerChanged, though the handler is given the whole collection.
An outcome is a key QuestCompleted and QuestFailed, not one event and a field to check.
One word per edge at every level Update on the bus and on a surface of yours hand the same dt.
Open sets are lower case A console command, a hotkey, a wdgmsg or uimsg name is a name you or the protocol chose. Every key the client fires is PascalCase and closed: an unknown one raises.

Objects, and the snapshot hatch

A read hands back a live object: it re-resolves on every call, answers nil once what it names is gone, and reports :exists(). Objects are interned per addon, so == is the identity test and one works as a table key. A point-in-time copy is :info() and nothing else. Every shape is in data types.

nil is an error unless it means something

An explicit nil argument raises: a value that arrived as nil would otherwise turn the write into a read.

nil means Where
Undo your layer, back to the rule beneath, else to the stock: the client’s own, or your builder’s default widget:position(nil), widget:size(nil), widget:text(nil), widget:title(nil), widget:replace(nil). A font variant’s handle:size(nil) and handle:aa(nil).
End the hold slot:hold(nil).
Withdraw the page options:panel(nil).
Unbind the control widget:bind(nil).
None A virtual entity’s :tint(nil), gob:tint(nil), gob:outline(nil).
The top of the AddOns category pagina:parent(nil).
The login screen hafen.session():current(nil).
The pointer the client would have drawn mouse:cursor(nil).
Everything A filter: collection:list(nil), :count(nil), :find(nil).
NULL A value bound to a ? of a statement or a store table’s clause. A column a :put row leaves out.
Rule Detail
Counted, not guessed The bridge separates the cases by counting arguments, exact for a value passed directly, a table field included: widget:size(config.width, config.height) with a missing key raises. The one gap: f(g()) where g returns nothing arrives as f(). A g returning an explicit nil is refused.
Optional arguments Leaving one out takes the default. nil in a slot you did pass raises: session:world():place(position, angle, nil, 0) passes a third argument.

A number is not a string, and a numeric string is not a number

The type is checked, never what Lua would convert to. session:kin():add(1234) is refused, since a hearth secret is a string. entry:value("42") is taken. A radio row may be labelled "061.8". The refusal names the verb, the parameter and the conversion you meant (tostring(n), tonumber(s)).

A number is finite, and an index is whole

Rule Detail
Finite 0/0 and math.huge are numbers by type, and every range test is false for 0/0 on both sides. So a number argument is checked for being finite.
Whole An index, an id, a count or a number of design pixels is a whole number. session:speed():get(2.7) raises naming the verb, the parameter and the number.
The exception x, y and the lengths the draw verbs take are finite without being whole: sub-pixel on purpose, rounded once where they meet the screen.

A table is a value, never named arguments

A table you pass is data: a colour, a coordinate, a document. A thing you build is constructed bare and configured by chained setters, so configuration reads in order and a setter can refuse. A request carries request:header(name, value) rather than an options table. What a verb returns (a :list() array, an :info() table) is an ordinary Lua table. A document names a file by path and a Lua call takes the handle. A stylesheet is a document, so {asset = "img/panel.png"} and {asset = "fonts/Inter.ttf", size = 12} load through hafen.asset() and intern to the object :get(path) hands you. Everywhere else (a sprite, an object, a draw verb) a path is refused.

A name that moved says where it went, and an unknown one says what exists

Rule Detail
A moved name raises At the line that wrote it, naming what to write instead.
An unknown name on an object raises gob:pozition() and gob.pozition alike (a field read and a colon call are one lookup): gob has no verb 'pozition' — a gob is one thing in the world: it answers :id() :exists() :sessions() :info() :position() :facing() :name() … and :distance().
On hafen and on a section a miss reads nil Where a feature probe asks: if hafen.something then keeps working.

Several logins, one screen

The client holds more than one account and draws one. What belongs to one character is reached through the Session that names it (session:world(), session:player(), session:kin()). A read says which character it is about. What belongs to the screen stays where it was: one pointer, one scene. Your addon is the client’s, loaded once beside every session, and nothing of yours is torn down when the screen moves.

Snapshots vs handles

Kind Detail
Snapshots Plain Lua tables, point-in-time copies from :info() readers (gob:info(), item:info()). They do not update: re-read rather than caching across ticks. Shapes in data types.
Handles Live, bridge-owned proxies with methods (hafen.ui():window(), hafen.timer():every, hafen.event():on), released on disable or reload. So is every object a read hands you, re-resolving rather than holding a value. Userdata with a closed vocabulary. An unknown name raises naming what it answers. Nothing can be written onto it (nothing deletes a handle’s own :cancel()). tostring(handle) names the thing (Timer(every 5s), Options(video), Sub(GobAdded)).
A table the bridge owns and you write into hafen.store():var(name): neither copy nor proxy but the table that goes to disk. Assigning into it is saving. It is the one place a typo on a key is silent and persisted.

The filter argument

Every enumerating verb takes one optional filter in the same form.

filter Keeps
nil (omitted) Everything.
A string Entries whose name contains the string (substring match).
A function Entries for which filter(entry) returns truthy. entry is the object, never a snapshot.
local gobs = hafen.session():current():world():gob()
local rabbits = gobs:list("rabbit")                                          -- name contains "rabbit"
local injured = gobs:list(function(gob) return (gob:health() or 1) < 1 end)  -- a Gob object
local pins = hafen.map():marker():list(function(marker) return marker:type() == "player" end)
Rule Detail
Members with no name A party member, a segment, a timer: a string is refused naming the forms that work. Use a function.
A store table The filter is the SQL clause. A function is refused naming SQL.
A name not arrived yet Does not match and does not spoil the call. A mistake inside your predicate raises out of the verb.

Missing data returns nil

A read returns nil, or an empty table for a list verb, when the data is not available yet. That is before the world loads, before a HUD widget streams in, while a resource resolves. Reads never throw a loading error. Much character-sheet data (meters, food, skills, quests, wounds) streams in shortly after SessionEnteredWorld: read it on a timer or subscribe to its event.

Threading

Your Lua runs either on the step or answering something. On the step it is inside no character’s UI and reaches every one. Answering something (a draw, a press, a drop, a console line) it is inside the one character’s UI that dispatched it. That is the only one it may reach. Taking a second is refused at the line that tries. Nothing blocks. Threading says which handler is which.

The permission model

Rule Detail
Protected A verb that starts an action the player could have performed runs only with its permission key. Your addon declares the key in the manifest and the user enables it. Such an addon is disabled the first time the client sees it and enabling it raises a consent dialog, unless the client installed it from its own release, which grants what it declares. One that never declared the key gets an error naming the verb and the key.
A key names the action, not the character One grant covers every character the client holds.
Named <section>.<verb> gob.click, item.transfer. <prefix>.* asks for the family in one line. No key grants the tier whole.
Outside the client, the same words http.get, http.post, websocket.connect, voice.connect, with the network host allowlist as the key’s argument. The key says whether, the hosts where, read as one consent line. The approved list gates the call.
A protected verb lives with the thing it changes Walking is on the character, clicking on the gob: the page you look a verb up on holds its key under Write (protected).
The line is what a verb does map.marker deletes a pin no server restores, client.settings rewrites every hotkey. Both reach nothing outside the client and are protected. Everything else observes or writes client-local and undoable, (unprotected).

See Also