The client’s widgets talk to the server in messages, and these two doors put your addon in the middle. hafen.event():action() is for a message on its way out. hafen.event():message() is for an update on its way in. Stop, rewrite or re-issue what a widget is about to send. Swallow an update before the widget applies it.
-- snap every walk to the centre of the tile you clicked:
hafen.event():action():on("click", function(event)
if event:widget():type() ~= "MapView" then return end
local arguments = event:args()
arguments[2] = hafen.session():current():world():snapPlace(event:position(2))
event:send(arguments)
end)
| Rule | Detail |
|---|---|
| What reaches the server | event:resend() and event:send(table) issue a message in place of the one the widget was about to send. Everything else observes, or cancels the client’s own behaviour. |
| Open key sets | Unlike the bus catalogue, any string is accepted: a message name is protocol the server can introduce. * is reserved for the whole stream. |
| Two handlers on one name | Both run. Either calling preventDefault cancels. |
hafen.event():action():on(msg, fn) fires when a widget is about to send action msg, with the arguments fully resolved. For a move "click" that is the destination world coordinate, which does not exist at input time. It is the door for stopping or rewriting before the server receives it, where a bus event arrives too late.
event:resend()andevent:send(table)need thewidget.sendpermission, the keywidget:sendneeds. They put a message on the same wire, andsendcarries arguments of your choosing.event:preventDefault()needs nothing. The key read is the key of the addon whose handler is running. Aneventhanded to another addon through a shared table is measured against that addon’s manifest, as aWidgetor aSessionis.
| Method | Returns | Permission | Description |
|---|---|---|---|
event:msg() |
string |
Unprotected | The message name. |
event:widget() |
Widget | Unprotected | The widget sending it. A live handle, so :type() reads the class and :parent() navigates. |
event:gob() |
Gob \| nil |
Unprotected | The game object a map click landed on. nil for ground (below). |
event:args() |
table |
Unprotected | A 1-based array snapshot of the raw protocol arguments, in wire units. A coordinate is {x=, y=}. |
event:position(i) |
Position | Unprotected | Argument i as a Position. Throws when that argument is not a coordinate. |
event:pixel(i) |
{x=, y=} |
Unprotected | Argument i as design pixels in the sending widget’s own space. Throws when not a coordinate. |
event:preventDefault() |
— | Unprotected | Cancel the send. |
event:resend() |
— | widget.send |
Re-send the original arguments verbatim. Implies preventDefault. |
event:send(table) |
— | widget.send |
Send a new argument table, a Position where a coordinate goes. Implies preventDefault. |
| Rule | Detail |
|---|---|
| For the handler you are in | A cancel counts only while the client waits for the answer. An event stashed and used later raises, as does one whose widget has left the tree. Hold the widget and use widget:send(msg, ...) outside the moment. |
| Two coordinate spaces | A click carries the press point at 1 and the world destination at 2, both {x=, y=}. Nothing in the shape says which. Name the space at the index you mean. Each verb throws naming the other on an index holding anything else. event:args() stays raw because resend and send round-trip through it. |
click is not the map’s alone |
A portrait, an item box, a party member’s tile send it too, with other arguments at other indices. Check event:widget() before reading an index. |
| Writing a coordinate back | A Position is accepted wherever a coordinate goes and the client encodes the wire form. A {x=, y=} table is taken verbatim, since only a Position says which space it is. A place this session cannot locate throws rather than walking you somewhere else. |
| No loop | resend and send bypass every action handler, so re-issuing an action cannot recurse: intercept, act, event:resend(). |
| Common names | click, itemact, drop, place, sel, act, use, take, transfer. |
hafen.event():action():on("click", function(event)
hafen.log():write("click on " .. tostring(event:gob())) -- implementation here: act before the click goes through
event:resend()
end)
event:gob() is the Gob a click from the MapView resolved to. nil for ground and for any action that is not a map click.
hafen.event():action():on("click", function(event)
local gob = event:gob()
if gob then hafen.log():write("clicked " .. (gob:name() or "?")) end
end)
The id is in event:args() too, and that copy is not to be read. The wire carries it as a sign-truncated 32-bit number while the server’s ids are unsigned. Comparing args[6] against gob:id() is wrong for every id above 2^31.
hafen.event():message():on(msg, fn) fires when a server update msg is about to be applied to a widget.
| Method | Returns | Permission | Description |
|---|---|---|---|
event:msg() |
string |
Unprotected | The message name. |
event:widget() |
Widget | Unprotected | The widget about to receive it. |
event:args() |
table |
Unprotected | A 1-based array snapshot of the raw protocol arguments, in wire units. |
event:position(i) |
Position | Unprotected | Argument i as a Position, as on action. |
event:pixel(i) |
{x=, y=} |
Unprotected | Argument i as design pixels in the receiving widget’s own space. |
event:preventDefault() |
— | Unprotected | Swallow the update, so the widget never applies it. |
event:rewrite(table) |
— | Unprotected | Apply the update with new arguments, a Position included, as on action. |
| Rule | Detail |
|---|---|
preventDefault wins over rewrite |
When both are called. |
| One update carries one rewrite | The last event:rewrite(table) before the widget applies the update is applied. Two addons rewriting one update do not compose and neither is told. One addon swallowing swallows for everybody. |
| Common names | set, add, del. |
local frozen = true -- flip it from a hotkey
-- freeze the HUD meter bars by swallowing their updates:
hafen.event():message():on("set", function(event)
if frozen and event:widget():type() == "IMeter" then event:preventDefault() end
end)
* is the key both streams reserve for every message on the stream. It is the one subscription you cannot write by hand, since the list of names is the server’s to grow. No message is called *.
| Rule | Detail |
|---|---|
The event is whole |
event:msg() says which message fired. Every other verb behaves as under a named key. |
A name and * together |
Both handlers run for that name, the named one first, over one event. A preventDefault from either cancels once. subscription:off() on the wildcard leaves a named one firing. A wildcard is your addon’s own: an addon holding only click is never called for another name. |
| Cost | Your handler runs on every message where a named key runs it on one. Keep the body short and read threading before making one wait. An addon that named its key pays nothing for another’s wildcard. |
hafen.event():action():on("*", function(event)
hafen.log():write(event:widget():type() .. " -> " .. event:msg() .. " (" .. #event:args() .. " args)")
end)
| Not reported | Detail |
|---|---|
A send from inside an action handler into the character whose message it is |
The stream is not re-entered while dispatching that character’s traffic, which stops a handler recursing. The send reaches the server unreported, your own wildcard included. The guard is one character’s: a send into another login’s widget is reported on that login’s stream. event:resend() and event:send(table) bypass the stream for the same reason. |
| A send from outside a character’s UI | An outbound handler runs inside the tree the message leaves, which is every player action and every widget send. A send from anywhere else passes through unreported. |
An outbound handler is inside a tree while an inbound one is not. When your Lua is busy on another thread the inbound one waits its turn and the outbound one is skipped, on the terms threading states.
The one to reach for first: make the thing happen on screen, read what arrived, then subscribe by name.
hafen.event():message():on("*", function(event)
hafen.log():write(event:widget():type() .. " <- " .. event:msg() .. " (" .. #event:args() .. " args)")
end)
event:preventDefault()on an inbound wildcard stops the client. A wildcard swallows every update, so the widget tree stops receiving from the server. The client’s change detection reads applied updates, so the bus stops firing with it. Swallow inside anifonevent:msg(), never at the top of the handler.
| Rule | Detail |
|---|---|
| Where it runs | Where the update arrived, before the widget applies it, which makes preventDefault and rewrite possible and means the handler cannot be deferred. It holds no character’s UI, so it may build a window and write any tree. It is the one row of threading’s table that is neither the step nor inside a tree. |
| No guarding needed | The client lets one entry into your Lua at a time, so this handler and the step’s Update never run together. |
| Order is not promised | The two are moments the client puts in no sequence: anything that must happen in order belongs in one place. |
| Time | Time spent here is time the client is not spending elsewhere, on every update for a wildcard. The CPU budget disables an addon that sustains an overrun. Read a field, count something, append to a table you drain on a timer. Do the work from the step. |
hafen.event() — subscribing, and the handle that ends one.event:widget() hands you.