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
- Session — the address every read here goes through.
session:menugrid — where the names the write takes come from, and where your entries live.
- Permissions — the keys the protected writes share.
ActionbarSlot — the snapshot shape :info() returns.
- Events —
ActionbarChanged.