Brodgar client

session:actionbar: The Action Bar

One character’s action bar (the F-key and number-key hotbar), read, activated and written through its session. session:actionbar() is that character’s bar, and a slot can be held for one of your own menu entries or for one of the player’s macros.

local session = hafen.session():current()                      -- the character on screen
for _, slot in ipairs(session:actionbar():list()) do           -- every slot, occupied or not
  if not slot:empty() then
    hafen.log():write(slot:index() .. ": " .. (slot:name() or slot:res()))
  end
end
session:actionbar():get(1):use()                               -- protected: activate the first slot

Method Returns Permission Description
session:actionbar():get(n) Slot Unprotected The slot at position n, 1..144, the number slot:index() answers.
session:actionbar():list(filter) Slot[] Unprotected Every slot, a 1-based array in game-index order.
session:actionbar():count(filter) number Unprotected How many match.
session:actionbar():find(filter) Slot \| nil Unprotected The first that matches.
session:actionbar():page() number Unprotected Which of the twelve pages that character’s bar is showing, 1..12.
session:actionbar():page(n) the collection Unprotected Turn to that page.
Rule Detail
Whose bar Every character has its own hotbar and the server fills each on its own: slot 11 on two characters is two buttons. hafen.session():get("alt"):actionbar():count() answers for that character, drawn or not.
One object session:actionbar() is the same object every call, minted once per session. A session the client no longer holds reads as 144 empty slots.
One number addresses a slot :get(n) takes the number slot:index() answers: session:actionbar():list()[n] == session:actionbar():get(n). :get(0) raises naming the change. An index outside 1..144 raises: the bar is a fixed array. slot:wire() is the server’s raw 0-based number, for comparing against something the server said.
Always 144, never sparse An empty slot is a Slot answering :empty(). A string filter matches a slot’s resource name, so :list("act/") is the occupied ability slots. No :add, no :remove: what changes is a slot’s content.
Interned per addon On character and index: session:actionbar():get(1) == session:actionbar():get(1) and seen[slot] = true work. The same number through two sessions is two objects. A Slot re-reads the bar on every call, so a stashed one tracks the slot being set, cleared or dragged (snapshots vs handles).
Before the hotbar exists Before the character is in the world, and briefly after a reload, every slot reads as empty. At login the occupied slots stream in shortly after, as a burst of ActionbarChanged.

The page

The client draws twelve buttons at a time and pages through the 144. Page p is slots (p-1)*12+1 .. p*12. It is turned by the client’s Go to page N keys or :page(n), both 1-based.

local actionbar = hafen.session():current():actionbar()
local first_shown = ((actionbar:page() - 1) * 12) + 1      -- the slot the leftmost button is showing
actionbar:page(3)                                          -- ...and now it is 25
Rule Detail
Changes nothing about the bar A slot’s address is absolute: :get(1) is slot 1 whatever page is up, :list() is all 144, contents untouched. The page says which twelve the player sees and the client’s button keys reach.
Unprotected both ways The page is a field of one widget: turning it sends nothing, so the client’s own page keys need no permission either.
No HUD yet Reads page 1. A write is a silent no-op. A page outside 1..12 raises, so does a non-number.
No event Read it where you draw.

Read

Method Returns Permission Description
slot:index() number Unprotected Its 1-based position, the number :get(n) takes. Always answers.
slot:wire() number Unprotected The raw 0-based game index the server’s message carries.
slot:empty() boolean Unprotected Whether the slot has no content. true before the hotbar exists.
slot:res() string \| nil Unprotected The resource name of the slot’s action or item. On a held slot, the entry’s identity. On a macro’s, the macro’s token, macro/<token>.
slot:name() string \| nil Unprotected The display name, once the action’s data has resolved. nil on a macro’s slot.
slot:cooldown() number \| nil Unprotected The meter fraction, a 0..1 fraction. Present only for an ability with a meter.
slot:info() ActionbarSlot \| nil Unprotected A plain-table snapshot, for logging and serialising.
Rule Detail
An empty slot Every reader except :index() and :empty() answers nil. None throws. None is protected.
Event ActionbarChanged, payload the changed Slot: set, cleared or changed. Not on a cooldown ticking. Read :cooldown() live.
No slot:widget() The bar is one widget painting all 144 slots, so there is nothing per slot to hand back. Reach the bar by role, session:ui():match("hud.belt"), and place against widget:rootPos().

Write (protected)

The client sends only shapes a player could compose.

Method Returns Permission Description
slot:use(mods) the Slot actionbar.use Activate the slot, as a left-click on that button does. On a macro’s slot, only inside a press on your addon.
slot:res(name) the Slot actionbar.res Assign an action by resource name, as dragging it off the menu grid does.
slot:clear() the Slot actionbar.clear Empty the slot, as a right-click on that button does.
Rule Detail
Permission Each key declared in your manifest, or the group actionbar.*. An undeclared key raises naming it (the permission model). One key covers every character (a key names the action, not the target). Each acts on the character whose bar the slot is on, watched or not.
Kept by the server slot:res(name) and slot:clear() outlive your addon being disabled, reloaded or uninstalled, and the session ending. To draw over a slot and give it back untouched, hold it.
mods Optional bitfield: Shift = 1, Ctrl = 2, Alt = 4. A value that is not a number raises naming the verb and the parameter. A numeric string is still a string. The refusal comes before the slot is looked at.
use Raises on an empty slot: check :empty() first. A ground-targeted ability enters targeting mode. Supply the target with session:world():click or place.
slot:res() is one name for the pair No argument reads, one assigns. The name it takes is the string it reads back, so put the action on the bar by hand once and read it. Works on any slot, overwriting an occupied one. A non-string or empty name raises. An unknown resource name is silently ignored by the server, as dragging something that does not exist would be.
The name is enough for every action The server addresses its pushed abilities by an id of its own and a drag sends that id. So does this verb, looking the name up in that character’s menu and sending whichever message the client’s own drop would. The id is never a thing an addon holds.
slot:clear() takes no arguments An argument raises naming both the assignment and slot:hold(nil). Clearing an empty slot is inert. It clears what the server has, where slot:hold(nil) releases your entry and hands the server’s content back. On a held slot it is legal, and a beat later the server’s write ends the hold.
Asynchronous res and clear send to the server, which echoes before the slot changes. The next line reads the old content. :res(name):use() in one chain activates what was there before. React to ActionbarChanged on that slot, or wait a beat.
local session = hafen.session():current()
session:actionbar():get(1):res("gfx/hud/act/mine")             -- protected: put "Mine" on the first slot
hafen.timer():after(0.5, function()
  session:actionbar():get(1):use()
end)

Hold a slot (unprotected)

A menu entry an addon added sits on the bar too. It draws its icon in the slot and runs its Lua when the slot’s key is pressed. The server owns the bar and knows nothing of the entry, so the client holds the slot. It draws over the server’s content and hands it back untouched when the hold ends.

local session = hafen.session():current()
local dig_entry = session:menugrid():add("dig"):name("Auto-dig"):icon(hafen.asset():get("dig.png"))
dig_entry:on("Pressed", function() hafen.log():write("dug") end)
session:actionbar():get(11):hold(dig_entry)            -- the entry now draws in that slot, and fires from it
Method Returns Permission Description
slot:hold() Pagina \| nil Unprotected The entry this slot is held for. nil for every slot the server owns, a slot holding one of the game’s own actions included (slot:res() names that, session:menugrid():get(name) is its Pagina). nil on a slot held for a macro too.
slot:hold(pagina) the Slot Unprotected Hold this slot for an entry an addon added to that character’s menu, yours or another’s.
slot:hold(macro_res) the Slot Unprotected Hold this slot for the player’s macro whose token macro_res is, the res a Drop handed you, as the drag from the Macros page does.
slot:hold(nil) the Slot Unprotected End the hold, whoever took it, a macro’s included. Inert on a slot nobody holds.
Rule Detail
Immediate, no permission Nothing reaches the server, where slot:res(name) is a round trip. ActionbarChanged fires on both edges.
One character’s pair A slot is held for an entry in that same character’s menu. An entry not in it (added on another login, or removed) is refused naming which addon’s entry it is and why it is not there.
Whose entry does not matter The bar is one shared surface: session:menugrid():get(res) hands you any entry’s Pagina and slot:hold(pagina) places it, as the player’s drag does. A hold places an entry and writes nothing on it. Renaming, re-iconing and handlers stay the entry’s owner’s.
While held slot:res() is the entry’s addon/… identity, slot:name() the name you gave it, slot:empty() is false. Pressing the slot (key, click, slot:use()) runs your pagina:on("Pressed", fn) handlers: one button in two places through the same code.
Dragging does the same Drag one of your entries off the action menu onto a slot and the client holds it, sending nothing.
Stacked holds Holding a slot another addon holds is allowed and the last write wins. What is carried through is the server’s content, so one release puts the game’s action back. A slot held for another addon’s entry follows that entry: it goes back when that addon removes the entry, reloads or is disabled.
Refusal Message names
slot:hold(pagina) for one of the game’s own entries That it is the client’s own entry, pointing at slot:res(name).
slot:hold(pagina) for an entry you removed That the entry is no longer in the menu.
slot:hold(pagina) for an entry another addon removed Not in that character’s menu, naming that addon.
slot:hold(7), slot:hold("dig") Expected the Pagina object, the res a Drop handed you for a macro, or nil.
slot:hold("macro/1"), or any macro/ string that is no live macro’s token That there is no such macro, naming the string. A deleted macro’s token reads the same.
slot:res("addon/myaddon/dig") That it is an entry an addon added, pointing back at slot:hold.
slot:res("macro/1"), or any macro/ string That it is a macro’s identity, which goes on the bar by the drag from the Macros page.

When a hold ends

What happened The slot goes back to The record is
slot:hold(nil) The server’s own content. Forgotten.
A right-click on the slot The server’s own content. The right-click is not sent, so nothing is cleared. Forgotten.
session:menugrid():remove(pagina) The server’s own content. Remembered.
Your addon reloads, or you log out The server’s own content. Remembered.
Your addon is disabled The server’s own content. Remembered.
The server writes that slot What the server wrote: that is the slot’s content now. Forgotten.

Every row but the last puts back what the server has in the slot, unchanged. In the last row the message being handled is the server assigning or clearing that slot, so what it wrote stands and the hold ends. slot:res(name) on a slot you hold is that row, once the server’s echo arrives.

A hold is remembered

The client keeps, per character, which entry belongs in which slot, and puts it back the moment that entry exists again. The call that re-applies it is session:menugrid():add(id), the one your addon already makes.

hafen.event():on("SessionEnteredWorld", function(session)
  local dig_entry = session:menugrid():add("dig"):name("Auto-dig"):icon(hafen.asset():get("dig.png"))
  dig_entry:on("Pressed", function() hafen.log():write("dug") end)
end)                                         -- if it was on that bar, it is on that bar again
Rule Detail
Inside the add The entry lands in its slot before add returns, so the next line reads slot:hold(). Your addon stores nothing: a row of the client’s own file, keyed by character, written when the hold is taken or ended. slot:hold(pagina) is remembered as a drag is.
One entry per slot The live hold chains, the record does not. Two addons holding one slot in turn are one remembered entry, the last written. The bar comes back with that one.
Two endings, remembered differently Ending by hand (slot:hold(nil), a right-click, the server taking the slot) says the entry no longer belongs there and the record goes. The entry going away (:remove, a reload, a logout) says nothing about the slot, so it waits. A :reload puts every button back. A player who right-clicked one off keeps it off.
Disabling keeps the slots They go back to the server’s content as the addon is torn down and the rows are kept, with its options and hotkeys. Enabling puts every button back as your :add runs. A right-click on a kept slot forgets it.
Per character The same entry can stand in a different slot on another character. A slot held on one is not held on the next.

A macro on a slot

The player’s macros stand on the bar as an entry does: the player drags one from the Macros page onto a slot, and the client holds the slot for it. A macro’s slot reads as a token: your addon never learns which macros exist, nor their names or bodies. It reads such a slot, runs it on the player’s press and can release it, and places one with slot:hold(token), the token a bar you draw receives from the player’s drag.

for _, slot in ipairs(hafen.session():current():actionbar():list("macro/")) do
  hafen.log():write(slot:index() .. ": a macro, token " .. slot:res())
end
On a macro’s slot Answers
slot:res() The macro’s token: macro/ and 32 lowercase hex characters.
slot:name() nil: the name is the player’s.
slot:empty() false.
slot:hold() nil: a macro is no Pagina of yours or any addon’s.
slot:hold(nil) Releases it, as a right-click does.
slot:use(mods) Runs the macro, as its key does (actionbar.use), only inside the player’s press on your addon, one macro a press (the rule). Elsewhere it raises before anything runs.
slot:info() ActionbarSlot with that res and no name.
Rule Detail
The token Random, and the same for the macro’s whole life: across a rename, an edit and a restart. It says nothing of the macro’s place in the list. Every addon reads the same token for one macro, so keep it as you keep a resource name.
Event ActionbarChanged fires as the macro takes the slot and as it leaves it. A rename or an edit fires nothing, and changes nothing you read.
No addon’s A :reload, or your addon disabled, leaves a macro’s slot standing.
Endings When a hold ends holds for a macro. Deleting the macro on its page is one more ending: every slot it stands in goes back to the server’s content, and the record is forgotten on every character.
Remembered Per character, in the same record as the entries: the macro is back in its slot, under the same token, at the next world entry and after a :reload. Renaming it or editing its body keeps every slot.
Refused slot:res(token) raises naming the drag from the Macros page, before the permission is asked, for every macro/ string. slot:hold(string) raises that there is no such macro, naming the string, for every macro/ string that is no live macro’s token. A deleted macro’s token, macro/1 and a guess read the same, so a refusal tells nothing about which macros exist.

Running one: a press on your addon

A macro runs in the console’s Lua, outside every addon’s sandbox. So slot:use() on a macro’s slot runs it only while your addon is handling the player’s own press on it: call it in that handler, before it returns.

Rule Detail
A press on your addon A MouseDown on a widget your addon built, a Pressed of a control it built, its hotkey going down, its console command typed by the player, its menu entry pressed by the player from the grid or from its slot on the bar.
Not a press A subscription on a widget the client built (a MouseDown on the map included), a mouse grab, a timer (one set inside a press included), an Update, a bus event, and a press made by code: hafen.console():run, pag:use(), slot:use().
One press, one macro The first slot:use() on a macro’s slot spends the press. Every handler of that one press shares it, so a second macro raises.
Every other slot Unchanged: an action’s, an item’s or an entry’s slot runs from anywhere, under actionbar.use.
The player’s own key and click On the client’s bar they run the macro as always.
The console A :lua line, and a macro, run any macro’s slot at any moment: they are the console.
Refusal Message names
slot:use() on a macro’s slot outside a press on your addon That it runs a macro only inside the player’s press on your addon, and the doors above.
A second slot:use() on a macro’s slot in the same press That one press runs one macro.

Both come after the permission, mods and the slot are checked, and before anything runs.

Placing one from a Drop

A bar your addon draws in place of the client’s receives the player’s drag as a widget’s Drop, res the macro’s token. Placing it, drawing it and releasing it are yours: slot:hold(res) holds the slot, g:resource(res, ...) draws the macro’s icon, slot:hold(nil) takes it off.

local session = hafen.session():current()
local slot = session:actionbar():get(13)
local button = hafen.ui():widget():parent(session:ui():match("@GameUI")):size(34, 34)
button:on("Drop", function(drop_event)
  local dropped = drop_event:thing().res
  if dropped and dropped:sub(1, 6) == "macro/" then
    drop_event:preventDefault()
    slot:hold(dropped)                                     -- the macro now stands in slot 13
  end
end)
button:on("Draw", function(draw_event)
  if slot:res() then draw_event:g():resource(slot:res(), 1, 1, 32, 32) end   -- a macro's res draws its icon
end)
button:on("MouseDown", function(mouse_event)
  if mouse_event:button() == 1 and slot:res() then slot:use() end   -- the player's press runs it
end)
Rule Detail
As the drag The slot is held exactly as the drag onto the client’s own bar holds it: unprotected, nothing sent, ActionbarChanged on both edges, remembered per character.
A right-click on your bar Is yours to handle. End a macro’s slot with slot:hold(nil): slot:clear() clears the server’s content under it. slot:hold() is nil there, so test slot:res() for macro/.
A token kept past its macro Once the player deletes the macro, slot:hold(res) with its token raises that there is no such macro, naming it, and g:resource(res, ...) draws nothing.
Pressing slot:use() runs the macro, as its key does (actionbar.use), in the handler of the player’s press on your bar: a MouseDown on the button you drew, or your hotkey for it. One macro a press. From a timer, an Update or any other moment it raises that it runs only inside the player’s press (the rule).

See Also