Actors in Lua¶
The lua plugin has been equipped with a method to utilize the actor subsystem of MQ. This system will allow script writers to communicate between scripts, clients, or even external applications using the same framework that is available to plugins. There are some few specializations unique to lua, and this document aims to provide a reference for them. Please refer to the terminology section in the other actors document for a glossary of terms.
Module¶
The module is loaded by requiring actors
. This module has a method register
for creating actors, and a method send
for sending anonymous messages to actors. Both of these methods provide overloads for various use cases:
local actors = require('actors')
-- create an actor for the script -- the actor is addressed using script
local script_actor = actors.register(function (message) ... end)
-- create an actor with a mailbox name -- the actor is addressed using script and mailbox
local actor = actors.register('myactor', function (message) ... end)
local payload = {test='payload'}
-- send a simple message to script_actor across all clients
actors.send(payload)
-- send a simple message to a specific address
actors.send({mailbox='targetmailbox'}, payload)
-- send a response message to script_actor across all clients (this will fail if any other client is running the same script)
actors.send(payload, function (status, message) ... end)
-- send a response message to a specific address
actors.send({character='charname', mailbox='targetmailbox'}, payload, function (status, message) ... end)
There are some things to notice about these functions that are essential for understanding how actors function. These will be addressed in the following sections: What Goes in a Message, Addressing, Message Handler, and Response Callbacks
The Actor¶
The actor is the central component of this system. It's what you use to send messages, and it's where the message handler lives. The object that the register function returns is technically a dropbox, so it will provide the methods that a dropbox will provide:
send
: the main method for sending from an actor. The overloads are the same as the anonymousactors.send
methods.unregister
: a method to unregister the actor if you want to remove it early.
What Goes in a Message¶
Before we get to the register functions and the message handler, let's look at what a message is. In general, a message can contain any native lua primitive or a lua table. Different types can be added to the serialization in the plugin if they are required, so please open feature requests if there are types that need to be added. MacroQuest datatypes can not be serialized generically because they are localized to the eqgame client. The following datatypes are currently serializable:
- nil
- string
- number
- boolean
- ImVec2
- ImVec4
- table (with the caveat that anything not serializable in the table will be ignored)
In the example above, the payload is the message content, and is simple a table with a single named string entry. This could be arbitrarily complex and nested tables will serialize as well, any type in the table that can't be serialized will simply be ignored.
Addressing¶
Next, we want to see what goes into an address. An address in lua can also be referred to as a header, and is a table that can optionally contain the following entries:
{
mailbox='', -- the target mailbox name for an actor
absolute_mailbox=false, -- if you want to provide the fully-qualified mailbox name (which will look like 'plugin:mailbox' or 'lua:script:mailbox'), set this to true
script='', -- the target script for an actor, which is a qualifier to mailbox names to gurantee uniqueness
pid=0, -- an unsigned integer value for Windows PID. This won't likely be available, but it could be used to direct a message to a specific client
name='', -- a name of a standalone actor client, like 'launcher' -- used to direct messages to external applications
account='', -- the name of the target client's account
server='', -- the name of the target client's server shortname
character='', -- the name of the target client's character
}
All of these entries are completely optional, and can be ambiguous if you want to send messages to multiple clients. If mailbox and script are not specified, then the target is assumed to be the current actor's mailbox (or just script if sent anonymously). Everything else can be used to disambiguate.
Message Handler¶
Now we get to the meat of the actors, the message handler. The function that is specified when you register an actor is what is called a "message handler". The function takes a message and returns nothing, to be used to handle incoming messages as you receive them. Delay will not work in the message handler and will throw a script error. The typical message handler would look something like this:
local function handler(message)
if message.content.id == 'something' then
...
elseif message.content.id == 'somethingelse' then
...
end
end
In this example, id
is something that the script writer provided in the message table, but content
is provided by the plugin.
Message Structure¶
The plugin passes the message into the handler with some specific structure, as follows:
content
: the actual data that was serialized from the sending side, following the rules for what goes in a message.reply
: a method used to reply to messages that have specified a response callback. This has two overloads:function(content)
andfunction(status, content)
, wherecontent
is any message as before, andstatus
is an integer status (defaults to 0 in the first overload).sender
: the Address of the sender, fully qualified.send
: a method similar to reply, but is used for cases where the message doesn't have a response callback and you just want to send a message back to the sender for it to go through their message handler.
Response Callbacks¶
A response callback is similar to a message handler, but has an additional argument for status
, which is just an integer value indicating response status. And important thing to consider is that these response messages will not be routed through your message handler, but will instead be routed directly through the response callback specified. The message is the same as in the message handler, so the only additional information needed for the callback is status. Typically, negative values indicate some kind of failure and should be handled, while positive values indicate something meaningful with 0 being the nominal "acknowledge" status. The follow enums are provided as they are errors that can happen external to the lua plugin:
actors.ResponseStatus.ConnectionClosed = -1
actors.ResponseStatus.NoConnection = -2
actors.ResponseStatus.RoutingFailed = -3
actors.ResponseStatus.AmbiguousRecipient = -4
Of these, only the last two are something that you would have any control over in lua. RoutingFailed
would happen if there is no recipient at the given address specified (like if you tried to address a specific character that was no longer logged in). AmbiguousRecipient
would happen if there was too much ambiguity in the address (like if you addressed an entire server). This is important because these are RPC-style messages, and the requirement is that exactly 1 recipient will receive the message, or we wouldn't be able to provide the guarantee that you will get a response. If you need to send a response to a multi-recipient message, then just use message.send
to do so, and handle the responses in the message handler.
Putting it All Together¶
In this example we demonstrate how to use Actors to communicate across clients to beg for buffs. This script can be found alongside other examples in the MacroQuest Git repository here
local mq = require('mq')
local actors = require('actors')
-- some example buffs, for demonstration purposes
local mybuffs = {
CLR={'Aegolism'},
DRU={'Protection of the Glades', 'Shield of Blades', 'Spirit of Wolf'},
SHM={'Riotous Health', 'Focus of Spirit', 'Spirit of Wolf'},
ENC={'Speed of the Shissar'}
}
mybuffs = mybuffs[mq.TLO.Me.Class.ShortName()]
local buff_queue = {}
local function dobuffs()
for name, buff in pairs(buff_queue) do
printf('Casting %s on %s...', buff, name)
mq.cmdf('/target %s', name)
mq.delay(5000, function() return mq.TLO.Target.CleanName() == name end)
if mq.TLO.Target.CleanName() == name then
mq.cmdf('/cast "%s"', buff)
end
end
buff_queue = {}
end
-- store a list of buffs and who can cast them
local buffers = {}
local function addbuffer(buff, sender)
printf('Received buffer %s casting %s', sender.character, buff)
if buff and sender then
if not buffers[buff] then buffers[buff] = {} end
if not buffers[buff][sender] then
buffers[buff][sender] = true
end
end
end
-- whenever a buffer disconnects, handle that
local function removebuffer(sender)
for buff, _ in pairs(buffers) do
buffers[buff][sender] = nil
end
end
-- this is then message handler, so handle all messages we expect
-- we are guaranteed that the only messages here we receive are
-- ones that we send, so assume the structure of the message
local actor = actors.register(function (message)
if message.content.id == 'buffs' and message.sender and mybuffs then
-- request to send a list of buffs I can cast
for _, buff in ipairs(mybuffs) do
message:send({id='announce', buff=buff })
end
elseif message.content.id == 'beg' then
-- request for a buff, send back a reply to indicate we are a valid buffer
message:reply(0, {})
buff_queue[message.sender.character] = message.content.buff
elseif message.content.id == 'announce' then
-- a buffer has announced themselves, add them to the list
addbuffer(message.content.buff, message.sender)
elseif message.content.id == 'drop' then
-- a buffer has dropped, remove them from the list
removebuffer(message.sender)
end
end)
-- buffer login, notify all beggars of available buffs
local function bufferlogin()
for _, buff in ipairs(mybuffs) do
-- need to specify the actor here because we're sending to beggars
-- from the buffer actor but leave it loose so that _all_ beggars
-- receive this message
printf('Registering %s on beggars', buff)
actor:send({id='announce', buff=buff})
end
end
-- beggar login, request buffer buffs
local function beggarlogin()
actor:send({id='buffs'})
end
-- beggar buff request, choose from local list of buffers
local function checkbuffs()
for buff, senders in pairs(buffers) do
if not mq.TLO.Me.Buff(buff)() then
-- get a random buffer that can cast the buff we want
local candidates = {}
for buffer, _ in pairs(senders) do
table.insert(candidates, buffer)
end
-- once we have the random buffer, ask them to cast the buff
local random_buffer = candidates[math.random(#candidates)]
if random_buffer then
printf('Requesting %s from %s...', buff, random_buffer.character)
actor:send(random_buffer, {id='beg', buff=buff}, function (status, message)
-- we have a reply here so that we can remove any buffers that didn't
-- clean up nicely (by calling /stopbuffbeg)
if status < 0 then removebuffer(random_buffer) end
end)
end
end
end
end
if mybuffs then bufferlogin() end
mq.delay(100)
beggarlogin()
-- we want to cleanup nicely so that all beggars know that we are done buffing
local runscript = true
mq.bind('/stopbuffbeg', function () runscript = false end)
while runscript do
checkbuffs()
dobuffs()
mq.delay(1000)
end
actor:send({id='drop'})