Posted by
| Nick Gammon
Australia (23,122 posts) Bio
Forum Administrator |
Message
| Now that the release of MUSHclient 3.80 is becoming imminent, with Lua 5.1, I want to present a simplified version of the waiting script. First, the script, which is intended to be saved to disk as the file "wait.lua".
require "check"
module ("wait", package.seeall)
-- ----------------------------------------------------------
-- table of outstanding threads that are waiting
-- ----------------------------------------------------------
local threads = {}
-- ----------------------------------------------------------
-- wait.timer_resume: called by a timer to resume a thread
-- ----------------------------------------------------------
function timer_resume (name)
local thread = threads [name]
if thread then
threads [name] = nil
assert (coroutine.resume (thread))
end -- if
end -- function timer_resume
-- ----------------------------------------------------------
-- wait.trigger_resume: called by a trigger to resume a thread
-- ----------------------------------------------------------
function trigger_resume (name, line, wildcards, styles)
EnableTrigger (name, false) -- don't want it to fire again
DoAfterSpecial (1, "DeleteTrigger ('" .. name .. "')", 12) -- delete it
local thread = threads [name]
if thread then
threads [name] = nil
assert (coroutine.resume (thread, line, wildcards, styles))
end -- if
end -- function trigger_resume
-- ----------------------------------------------------------
-- convert x seconds to hours, minutes, seconds (for AddTimer)
-- ----------------------------------------------------------
local function convert_seconds (seconds)
local hours = math.floor (seconds / 3600)
seconds = seconds - (hours * 3600)
local minutes = math.floor (seconds / 60)
seconds = seconds - (minutes * 60)
return hours, minutes, seconds
end -- function convert_seconds
-- ----------------------------------------------------------
-- wait.time: we call this to wait in a script
-- ----------------------------------------------------------
function time (seconds)
local id = "wait_timer_" .. GetUniqueNumber ()
threads [id] = assert (coroutine.running (), "Must be in coroutine")
local hours, minutes, seconds = convert_seconds (seconds)
check (AddTimer (id, hours, minutes, seconds, "",
timer_flag.Enabled + timer_flag.OneShot +
timer_flag.Temporary + timer_flag.Replace,
"wait.timer_resume"))
return coroutine.yield ()
end -- function time
-- ----------------------------------------------------------
-- wait.regexp: we call this to wait for a trigger with a regexp
-- ----------------------------------------------------------
function regexp (regexp, timeout)
local id = "wait_trigger_" .. GetUniqueNumber ()
threads [id] = assert (coroutine.running (), "Must be in coroutine")
check (AddTriggerEx (id, regexp,
"-- added by wait.regexp",
trigger_flag.Enabled + trigger_flag.RegularExpression +
trigger_flag.Temporary + trigger_flag.Replace,
custom_colour.NoChange,
0, "", -- wildcard number, sound file name
"wait.trigger_resume",
12, 100)) -- send to script (in case we have to delete the timer)
-- if timeout specified, also add a timer
if timeout and timeout > 0 then
local hours, minutes, seconds = convert_seconds (timeout)
-- if timer fires, it deletes this trigger
check (AddTimer (id, hours, minutes, seconds,
"DeleteTrigger ('" .. id .. "')",
timer_flag.Enabled + timer_flag.OneShot +
timer_flag.Temporary + timer_flag.Replace,
"wait.timer_resume"))
check (SetTimerOption (id, "send_to", "12")) -- send to script
-- if trigger fires, it should delete the timer we just added
check (SetTriggerOption (id, "send", "DeleteTimer ('" .. id .. "')"))
end -- if having a timeout
return coroutine.yield () -- return line, wildcards
end -- function regexp
-- ----------------------------------------------------------
-- wait.match: we call this to wait for a trigger (not a regexp)
-- ----------------------------------------------------------
function match (match, timeout)
return regexp (MakeRegularExpression (match), timeout)
end -- function waitfor
-- ----------------------------------------------------------
-- wait.make: makes a coroutine and resumes it
-- ----------------------------------------------------------
function make (f)
assert (type (f) == "function", "wait.make requires a function")
assert (not (GetInfo (106) or GetInfo (107)), "Not connected to MUD")
assert (GetOption ("enable_timers") == 1, "Timers not enabled")
assert (GetOption ("enable_triggers") == 1, "Triggers not enabled")
coroutine.wrap (f) () -- make coroutine, resume it
end -- make
An important enhancement in Lua 5.1 is the ability for a coroutine to find its own coroutine address, thus simplifying the script over the previous ones, which had to pass down the coroutine address when needing to wait. For example, in the previous version:
-- wait a second for luck
wait (t, 1)
The new version looks like this:
-- wait a second for luck
wait.time (1)
A small change, but a helpful one I think. The other important consideration is the fact that it is using the new "module" function, which is in Lua 5.1.
What this effectively does is move all of the waiting functions (wait for time, wait for regexp etc.) into a single table - the "wait" table. Then the individual functions are accessed from inside that table.
Also, its own data (a table of active threads) is now private to the wait table, and cannot be accidentally changed by other scripts.
The exposed functions are:
- wait.time (n) - wait for 'n' seconds
- wait.match (text, n) - wait for 'text' to arrive, time out after 'n' seconds.
If the match is made it returns the matching line. If it times out, it returns nil.
- wait.regexp (re, n) - wait for regular expression 're' to arrive, time out after 'n' seconds
If the match is made it returns the matching line. If it times out, it returns nil.
- wait.make (f) - makes a coroutine thread and starts it up. The above functions must appear inside such a function (see example below).
- wait.timer_resume - function called by a timer when the requested time is up.
The wait.time, wait.match and wait.regexp functions automatically creates the appropriate timers.
- wait.trigger_resume - function called by a trigger when the requested match text arrives from the MUD.
The wait.match and wait.regexp functions automatically creates the appropriate triggers.
Here is an example of using it (which might go inside an alias):
require "wait"
wait.make (function () --- coroutine below here
repeat
Send "cast heal"
line, wildcards =
wait.regexp ("^(You heal .*|You lose your concentration)$")
until string.find (line, "heal")
-- wait a second for luck
wait.time (1)
Note ("heal done!")
end) -- end of coroutine
An alternative way of writing it, which might look simpler perhaps, is this:
require "wait"
function cr ()
repeat
Send "cast heal"
line, wildcards =
wait.regexp ("^(You heal .*|You lose your concentration)$")
until string.find (line, "heal")
-- wait a second for luck
wait.time (1)
Note ("heal done!")
end -- of function cr
wait.make (cr) -- start coroutine up
This makes it clearer that there are really two steps. One is to have a function which runs as a coroutine (cr in this case), and the second step is to start the coroutine up (wait.make).
The nice thing about this is that the specialised code to handle the waiting (wait.lua) is now in its own file, where its purpose is clearly defined.
The "require" function only loads wait.lua from disk if it hasn't already been loaded, so you could put it into every alias that needed it without much overhead. A quick test on my PC shows that you can call:
... 2 million times in a single second, so the overhead is really minimal.
The wait.lua also has a require statement in it, which indicates that it has a dependency on check.lua, which implements the "check" function. The code for that is:
--
-- check.lua
--
-- ----------------------------------------------------------
-- return-code checker for MUSHclient functions that return error codes
-- ----------------------------------------------------------
function check (result)
if result ~= error_code.eOK then
error (error_desc [result] or
string.format ("Unknown error code: %i", result),
2) -- error level - whoever called this function
end -- if
end -- function check
|
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|