Timed Iterator: using coroutines in Mudlet

The Problem

Since it was a day ending in ‘y’ I was hanging out in the Mudlet discord when someone asked what the best way to create a timed iteration through a table is. And there are two main approaches which occurred to me for accomplishing this.

The Journey

The first simple one would be to iterate the table and create a tempTimer for each item, with each new timer set to a longer delay than the last. As a quick example:

local testTable = { this = "that", something = "something else", blah = "blah blah" }
local count = 0
for key, value in pairs(testTable) do
  registerNamedTimer("timed iter", tostring(count), count, function()
    local key = key
    local count = count
    cecho(f"<red>{key}<reset>:<yellow>{value}\n")
  end)
  count = count + 1
end

But that feels clunky and would be difficult to stop once it was kicked off. The other approach is to use Lua’s coroutines, which allow us to temporarily suspend operation of a function, pass control back to the main execution loop, and then pick back up where we left off. A simple example from the Mudlet wiki looks like this:

-- in a script
function ritual()
  send("get wood")
  -- think of coroutine.yield as yielding (giving away) control,
  -- so the function will stop here and resume on making fire 
  -- when called the next time
  coroutine.yield()
  send("make fire")
  coroutine.yield()
  send("jump around")
  coroutine.yield()
  send("sacrifice goat")
end

-- in an alias, which you call each time you want to advance the ritual
-- create a coroutine that'll be running our ritual function
-- or re-use the one we're already using if there is one
ritualcoroutine = ritualcoroutine or coroutine.create(ritual)

-- run the coroutine until a coroutine.yield() and see
-- if there's any more code to run
local moretocome = coroutine.resume(ritualcoroutine)

-- if there's no more code to run - remove the coroutine,
-- so next time you call the alias - a new one gets made
if not moretocome then
  ritualcoroutine = nil
end

In this example, the first time you use the ritual alias, it will send “get wood”, the second time “make fire”, and so on. Once it’s sacrificed the goat it will reset using “ritualcoroutine = nil” so that the next time you use the alias it starts at the beginning again.

The Destination

But that’s pretty hard coded and inflexible, and I like to make things which can be reused. So I made a pair of functions: one to iterate a table on a timer, and the other to stop it. Right now it’s just using pairs to illustrate how it’s done, but you could easily write one that uses ipairs, spairs, or allows you to pick one of the three.

 function createTimedIterator(name, tbl, time, func)
  -- create a wrapper function which iterates tbl
  -- and calls func once for each entry, then yields
  local function doThing()
    for key, value in pairs(tbl) do
      func(key, value)
      coroutine.yield()
    end
  end
  
  -- create the coroutine itself
  local routine = coroutine.create(doThing)
  -- and run the first iteration immediately
  local gotMore = coroutine.resume(routine)
  if not gotMore then
    routine = nil
    return
  end
  -- and a function which the timer will call to resume
  -- the coroutine every time seconds
  local function timerFunc()
    -- execute the next iteration
    local gotMore = coroutine.resume(routine)
    -- if no more after this then stop the timer and clear the coroutine.
    if not gotMore then
      stopNamedTimer("timedIterator", name)
      routine = nil
    end
  end
  -- and now kick the timer off
  registerNamedTimer("timedIterator", name, time, timerFunc , true)
end

function stopTimedIterator(name)
  -- this one's pretty easy, just stop the timer.
  stopNamedTimer("timedIterator", name)
end

Usage

createTimedIterator("testSend", { "This", "That", "The other"}, 1, function(key, value) send(value) end)
-- or
local tbl = {
  test = "passed",
  this = "that",
  something = "something else",
  blah = "blah blah blah"
}
local function func(key, value)
  cecho(f"<red>{key}<reset>:<purple>{value}<reset>\n")
end
createTimedIterator("display items", tbl, 1, func)

Conclusion

Iterating a table on a timer is only one example of how you might use coroutines. Another might be breaking long running actions into smaller pieces and yielding between them. This helps avoid locking up Mudlet for several seconds in heavy crunch situations. They’re also used for the wait and waitLine package on the Mudlet forums. Really, any time you might want to tell your code to “hold that thought” in the middle of a function and be able to come back to it later, a coroutine is the way to go. Hopefully, this short exercise has helped you understand them a little better and you’ll find uses for coroutines in your own scripts.

Happy MU*ing!