A Look at Trigger Gates in Mudlet

The Problem

So once again I found myself sitting in the Mudlet #help channel on Discord and someone came in with a question. And the solution to this question is one that comes up fairly frequently, but can be difficult to explain without examples. I resolved the issue on Discord and asked them if I could use the example to reference back to. And here we are.

The Journey

To start with, someone came in and said they needed help matching a block of text from Lost Wishes. And this is the block they needed to parse

 <===============================(Dominates)===============================>
| Dom 1: Necromancer fellow HP: 18915 SP: 36651 LVL: 195 NEED: 21849 KILLS: 0 
| STR: 168 INT: 213 DEX: 202 CON: 187 WIS: 199 CHA: 235
| 
| Dom 2: Black cat HP: 18915 SP: 38021 LVL: 195 NEED: 21150 KILLS: 0 
| STR: 224 INT: 230 DEX: 201 CON: 171 WIS: 227 CHA: 148
| 
| Dom 3: Mindwitness HP: 16200 SP: 31630 LVL: 180 NEED: 14056 KILLS: 0 
| STR: 168 INT: 157 DEX: 217 CON: 219 WIS: 194 CHA: 152
<=========================================================================>

This is a perfect candidate for gated triggers, as you may not know exactly what comes inside the block but you know exactly how it starts, a general pattern for the pieces in the center, and exactly how it ends.

I’m going to walk through the different pieces of this trigger one at a time, but the complete trigger stack can be downloaded as an xml you can paste into the Trigger editor in Mudlet here.

The Parent Trigger

The first trigger we make is the parent trigger or the trigger gate itself. This is the first line in the block, in this case:

<===============================(Dominates)===============================>

We set the fire length of this trigger to 99, because we’re not sure how many lines will be in the center bit. In the code for the trigger we wipe/recreate a table to hold our dominated mobs (dominates). We also delete the line, because we want this parsing to be invisible in the main display.

dominates = {}
deleteLine()
Screenshot of the trigger head. Also shows the overall layout of the triggers when complete.

The Inner Trigger Gate

For this one in particular, we’re going to use a nested trigger gate. Each block of information on a dominated mob is two lines long, so we’ll use a trigger gate to match these. You could get by without these inner triggers being gated but it’s good practice. This new trigger must be dragged on to the first trigger to establish the parent<->child relationship. The icon for the first trigger will become a funnel when this happens. We set a fire length of 1 because we know the next pattern comes on the very next line. Because we’re matching and parsing information out of this line, we’ll use a regex trigger. The pattern in this case is:

\| Dom (?<number>\d+): (?<name>.+) HP: (?<hp>\d+) SP: (?<sp>\d+) LVL: (?<lvl>\d+) NEED: (?<need>\d+) KILLS: (?<kills>\d+)

And the code in the scripting box looks like this:

dominatenum = tonumber(matches.number)
dominates[dominatenum] = {
  name = matches.name,
  hp = tonumber(matches.hp),
  sp = tonumber(matches.sp),
  lvl = tonumber(matches.lvl),
  need = tonumber(matches.need),
  kills = tonumber(matches.kills)
}
deleteLine()

Because it’s the first line in the block, we create a new entry in the dominates table. We use the number provided by the block as the key, and copy in all the information we have obtained.

Inner trigger head.

Inner Trigger Gate Child

This trigger captures the information out of the second block. Once you create it, you drag it on to the previous trigger, as shown in the pictures. We’ll use another regular expression for this trigger, with the pattern of:

\| STR: (?<str>\d+) INT: (?<int>\d+) DEX: (?<dev>\d+) CON: (?<con>\d+) WIS: (?<wis>\d+) CHA: (?<cha>\d+)

And the script will be:

local info = dominates[dominatenum]
info.str = tonumber(matches.str)
info.int = tonumber(matches.int)
info.dex = tonumber(matches.dex)
info.con = tonumber(matches.con)
info.wis = tonumber(matches.wis)
info.cha = tonumber(matches.cha)
dominates[dominatenum] = info
deleteLine()

First we retrieve the information we stored in the previous trigger, then we add the new information to it, and assign it back to the dominates table.

Inner Trigger Gate Child: the second line of info on a dominated mob

Tidying up

We’ve got two more triggers to go. These sit just below the inner trigger gate parent, but beneath the very first Trigger Parent at the very top. The first is to catch the ’empty’ lines between blocks. I went with a regex here as well, just in case. Pattern is:

^\|\s*$

This one just does deleteLine() as there is no useful information to parse.

Closing the gate

The last trigger matches the line at the very end of the block, and is responsible for closing the entire thing up neat and tidy. I used an exact match pattern, mattching:

<=========================================================================>

And with the script:

setTriggerStayOpen("Dom Gate", 0)
replaceLine("Dominated beings scanned\n")

I used replaceLine so it wasn’t -entirely- silent. The setTriggerStayOpen call is important, it causes “Dom Gate” (the very first trigger we created above) to stop checking its child triggers for matches. It’s resetting it for the next time the first line is caught by the trigger gate.

The final trigger in the stack, which closes the outer trigger gate and stops all these inner triggers from being checked.

The Destination

And here we are, having successfully created not one, but two trigger gates. For some people, it helps to think of them as groups of triggers which Mudlet is enabling and disabling for you based on the fire length. We used setTriggerStayOpen to indicate to Mudlet that we wanted to close the trigger gate before those 99 lines were up. You could emulate this to some extent using enableTrigger and disableTrigger, but letting Mudlet do it for you is handier and allows you to do some filtering of what gets passed to the child triggers, as if you check the “only pass matches” check box then rather than opening the child trigger up for checking future lines, it only sends the part of the line which matched the parent trigger on to the child trigger for evaluation. But that’s a dive for another time.

Conclusion

Hopefully you’ve learned a little bit more about Mudlet’s trigger engine and some of the lesser talked about things it can do while following along with this. And hopefully this demonstration has provided you with some idea of when you might want to use a trigger chain. For another example of a trigger chain and when you might want to use one, check out this video from Vadi about capturing data in Mudlet. And in the meantime…

Happy MU*ing!

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!

Targeted Tables

The Problem

While I was hanging out on Discord the other day helping folks with Mudlet, someone asked me a question that comes up with some regularity in one form or another. They wanted to define a table of strings which included information from a variable. The first most obvious form of this would be

myActions = {
  attack = "kill " .. myTarget,
  heal = "cast 'healing word' " .. myTarget
}

The problem with this is that it will use the value of myTarget at the time of definition. But your target might change, and do you really want to redefine the table every time you change targets? I mean sure that would solve the problem, but There Has To Be A Better Way. And there is!

The Journey

There are a few ways to solve this which aren’t just redefining the table every time. One of the first is you could define the elements as functions themselves.

myActions = {
  attack = function() return "kill " .. myTarget end,
  heal = function() return "cast 'healing word' " .. myTarget end,
}

But this is a lot of typing any time you want to set a new value in the table, and it’s also kind of ugly honestly. Another option is to use a function to wrap your use of the items in the table.

myActions = {
  attack = "kill %s",
  heal = "cast 'healing word' %s"
}
function getAction(name)
  local action = myActions[name]
  if action then
    -- in case myTarget is nil, we add the 'or "DEFAULT"'
    -- this keeps string.format from erroring
    return string.format(action, myTarget or "DEFAULT")
  end
end

But then you have to explicitly and consciously use a function call every time you want to get a value from the table. Which is more typing and repeating yourself than I like.

That being said, both of the above solutions are viable and work just fine, and there’s no reason to move away from them if you’re happy with them. But if we spend a little time examining what happens when you get or set a value in a table, there are some tricks which will let us hide these details away.

So, what happens when you look up a value in a table? It looks up the key and returns the value stored there, or nil if no value is stored there, right? Well, mostly right. It does in fact look up the key and if there is something stored there it returns it. But if there isn’t something stored there, it looks to our table’s metatable. Specifically, it will look for the metatable’s __index function, passing in the original table we’re checking a value in, and the key we’re looking for. Similarly, when you set a value in a table it will look for the metatable’s __newindex function and call that to set the values. So using these two facts we can customize how a table behaves when you interact with it to store or retrieve data. So let’s build it piece by piece.

The Table

Now that we’re going to be manipulating all of the entries on entry and lookup, we can’t store the values directly at their associated keys, as it would just return those values and put us right back at the original problem. We can solve this by using a subtable to actually hold all the entries before formatting. I’ll also go ahead and put one entry in it, by way of example. We are largely using the second solution above, we’re just going to use metatables to have Lua do it for us automagically.

-- a top level table to keep everything in, for tidiness in the global space.
demonnic = demonnic or {}
-- and I'll go ahead and define the 'target' variable I'll be using
demonnic.target = demonnic.target or "DEFAULT"

-- and the table which we will use to store and retrieve the targeted actions
demonnic.targetedActions = demonnic.targetedActions or {
  -- the entries table is where the base entry, before manipulation, will be stored.
  -- Here I go ahead and preload it with one item I know I will always want in it
  entries = {
    attack = "kill %s"
  }
}

The Metatable

Now let’s define the metatable which will govern our table’s actions. The code is heavily commented rather than breaking it into multiple blocks.

-- use a local table as we don't really need to
-- access it outside of defining and assigning it
local meta = {}

-- let's go ahead and assign the metatable 
-- to my targeted actions table while we're here
setmetatable(demonnic.targetedActions, meta)

-- now, let's define how an item is looked up.
-- 'which' is the table being looked in
-- 'what' is the key being looked for
function meta.__index(which, what)
  local entry = which.entries[what]
  if entry then
    -- even though I definitely define demonnic.target above, I still tend to leave
    -- safeguards like this in place. Just In Case.
    return string.format(entry, demonnic.target or "DEFAULT")
  end
  -- the below isn't strictly necessary, but I like to be explicit
  return nil
end

-- We will also need to define how we store values for the table
-- so they go to the `entries` sub table for storage
-- 'which' is the table you're trying to assign data to
-- 'what' is the key being assigned to
-- 'becomes' is the value being assigned
function meta.__newindex(which, what, becomes)
  -- we only want to accept strings or nil (to clear an entry) so first we check the type
  local becomesType = type(becomes)
  if becomesType == "nil" then
    -- time to clear the entry out if it exists
    which.entries[what] = nil
    return
  end
  if becomesType ~= "string" then
    -- we don't want non strings, it'll break the string.format call in '__index'
    local msg = "You must provide a string and a string only for values to demonnic.targetedActions"
    -- print the msg, with stack trace, to the error console
    printDebug(msg, true)
    -- and return nil. The msg isn't actually passed along in this case 
    -- but I include it out of habit and it hurts nothing
    return nil, msg
  end
  -- uncomment the following if you want to also ensure 
  -- entries include a '%s' to substitute the target in for.
  -- if '%s' is not included with the string the target won't
  -- get pushed in, but it doesn't error so that's why I commented it
  --[[
  if not becomes:find("%%s") then
    local msg = "You must provide a %s in the string to replace with the target"
    printDebug(msg, true)
    return nil, msg
  end
  --]]

  -- and finally we set the value now that it's passed the checks.
  which.entries[what] = becomes
end

Usage

That’s about it. Now you would use the table as normal for getting and receiving values, but it’ll have the target swapped for you. For example:

demonnic.target = "Bob"
-- the following will send "kill Bob"
send(demonnic.targetedActions.attack)

-- adding a new action
demonnic.targetedActions.heal = "cast 'healing word' %s"

-- and now it will send "cast 'healing word' Bob"
send(demonnic.targetedActions.heal)

demonnic.target = "Linda"
-- and now it will send "case 'healing word' Linda"
send(demonnic.targetedActions.heal)

The Destination

That’s all well and good and works if you only have one such table, but what if you wanted more than one? Maybe you want one table of targeted cures and one of targeted attack moves. Maybe they should track two separate targets. I’m not your dad, I don’t tell you how to live your life.

But I will tell you how to make a function which can return a ‘targeted’ table for you to use, so you can reuse the code without repeating it. With the hopes that you will be able to apply it in future scripts, obviously =)

The following code can also be viewed as a Gist

-- this function allows us to take a name of a variable as a string for checking 
-- For instance, if you use getValue("demonnic.target") it will return 
-- the value at demonnic.target, or nil and an error.
local function getValue(location)
  local ok, err = pcall(loadstring("return " .. tostring(location)))
  if ok then return err end
  return nil, err
end

local meta = {}
-- return an entry from the table with the target swapped out for %s token
-- @param which the table being looked in
-- @param what the key being looked for
function meta.__index(which, what)
  local entry = which.entries[what]
  if not entry then
    return nil
  end
  local target = getValue(which.target)
  return string.format(entry, target or "DEFAULT")
end

--- create an entry in the targeted table
-- @param which the table to add the entry to
-- @param what the key to use for the entry
-- @param becomes the value to set the entry to
function meta.__newindex(which, what, becomes)
  local becomesType = type(becomes)
  if becomesType == "nil" then
    which.entries[what] = becomes
    return
  end
  if becomesType ~= "string" then
    local msg = "You must provide a string and a string only for values to a targeted table"
    printDebug(msg, true)
    return nil, msg
  end
  -- uncomment the following if you want to also ensure
  -- entries include a '%s' to substitute the target in for.
  -- if '%s' is not included with the string the target won't
  -- get pushed in, but it doesn't error so that's why I commented it
  --[[
  if not becomes:find("%%s") then
    local msg = "You must provide a %s in the string to replace with the target"
    printDebug(msg, true)
    return nil, msg
  end
  --]]
  which.entries[what] = becomes
end

function createTargetedTable(target, entries)
  -- use an empty table if no entries provided
  entries = entries or {}
  -- do some type checking
  entriesType = type(entries)
  targetType = type(target)
  if entriesType ~= "table" then
    return nil, "createTargetedTable(target, entries): optional parameter entries as nil or table expected, got " .. entriesType
  end
  if targetType ~= "string" then
    return nil, "createTargetedTable(target, entries): target variable name as string expected, got " .. targetType
  end
  local new = {
    entries = entries,
    target = target
  }
  setmetatable(new, meta)
  return new
end

In action

And here’s a photo of it in action, captured from REPLet

New test-in-mudlet GitHub Action

As part of eliminating my remaining excuses for not properly testing all my Mudlet packages, I repackaged Busted for use in Mudlet on Windows. In the process, I realized I really ought to make it so GitHub could do the automated running for me, even if I had made it so I could test easily when booted into Windows and even get nice color output.

So I set out to make a GitHub action which would mimic the steps already in use in Mudlet’s build tasks to run our Busted tests for Mudlet itself. And now I have the MDK building and testing automatically when I make a pull request. You can view the action on the marketplace. Here’s the workflow I use for the MDK now.

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      
      - name: Muddle
        uses: demonnic/build-with-muddler@v1.3
      
      - name: Upload MPackage
        uses: actions/upload-artifact@v2
        with:
          name: MDK-package
          path: build/tmp/
      
      - name: Run Busted tests
        uses: demonnic/test-in-mudlet@v1.1
        with:
          pretestPackage: ${{ github.workspace }}/build/MDK.mpackage

And an action shot.

Mudlet 4.15 Feature Highlight

In Short

This release of Mudlet can perhaps be summarized with two words; Sound and Labels. The Mudlet sound system saw an overhaul with this release opening up a lot of possibilities for ambient and background sounds. And there were several improvements to and around Labels; ability to display animated gif files, ability to resize themselves based on their content, a ScrollBox to contain them in a scrollable area for lists of labels or larger zoomed images and the like, c/d/hecho support for easily setting text formatting and color in Labels, and the new Geyser.StyleSheet for managing the stylesheets of your Labels for a more cohesive look.

Sound Overhaul

I don’t use the sound system in Mudlet much myself, so I reached out to the author of the overhaul to give me a couple paragraphs on it, so in the words of Tamarindo from the Mudlet discord:

New sound and music features were introduced with the Mudlet 4.15 release. The Mud Client Media Protocol (MCMP) uses Generic Mud Client Protocol (GMCP) to trigger playing of media files. Added are new optional parameters, “start”, “fadein” and “fadeout”.

Client.Media.Play {
  "name": "monty_python.mp3", -- the track
  "url": "https://yourdomainhere.com/music/", -- download and cache from here
  "type": "music", -- music has difference features than sound in MCMP
  "volume": 100, -- max volume
  "start": 1000, -- start 1 second into the track
  "fadein": 5000, -- raise the volume from the minimum to 100 over 5 seconds
  "fadeout": 7000 -- begin to lower the volume 7 seconds before the track finishes from 100 to minimum volume
}

As you might expect, “start” instructs the client to begin playing the identified track at a specified number of milliseconds (1000 msec = 1 second). If one wants to progressively increase the volume from the start of the track over a period of time, they may now use “fadein”, which will use a linear algorithm to raise to volume until the number of milliseconds. Similarly, “fadeout” will reduce volume using a linear algorithm, but using the number of milleseconds specified from the end of the track as the starting point.

Additionally, the 4.15 release opens up all of the benefits of MCMP direct to the Lua API for Mudlet. Similar to the example above, one could define a table {} and do the following in a Mudlet script:

playMusicFile( {
  name = "monty_python.mp3",
  url = "https://yourdomainhere.com/music/",
  volume = 100,
  start = 1000,
  fadein = 5000,
  fadeout = 7000
} )

The API also offers these additional functions, loadMusicFile(), loadSoundFile(), playSoundFile(), stopMusic(), stopSounds() which also take a table as an argument. These take parameters as well, so playSoundFile(“”, 100) works as it has in earlier releases. If you have scripts that use playSoundFile today, you could now enhance them to specifically use the stopSounds(“”) function to have more control over your media. Please reference the Mudlet wiki for more information: https://wiki.mudlet.org/w/Manual:Miscellaneous_Functions or contact Tamarindo on the Mudlet Discord for more help.

Labels

Labels got a lot of improvements this release, but I’ll start with the additional support items added in this release

Geyser.StyleSheet

If you’re familiar with CSSMan then Geyser.StyleSheet should be immediately useful to you, as it has all the functionality of CSSMan and then some. Namely, you can assign a parent Geyser.StyleSheet to inherit properties from. This means you can set the border properties you know will be shared by a bunch of Labels in one Geyser.StyleSheet, and then use it as the parent for stylesheets which define the background color for related groups of Labels, and then use this second stylesheet as the parent for individual Labels which might each have their own font color, or some other individual trait. And then if you change the first label to make the border thicker, it will be inherited by all of the stylesheets all the way down. Check out the manual page for more details.

ScrollBox

Have a fancy who list with labels for the other players online, and not enough screen real estate to display it all? The ScrollBox gives you a container you can place Labels into which allows you to scroll either vertically or horizontally to view everything, but still keep it contained to a certain size on your actual screen. Example can be seen in the right side of this screenshot here from Tamarindo on the pull request which added the feature. Another good example from Edru for a zoomable map label can be seen here (mp4 file from discord).

Label auto resizing to fit contents

Another valuable addition from Edru, Labels can now tell you what size they should be, base on their contents. Using getLabelSizeHint Geyser.Labels can now auto adjust their width, height, or both based on these hints. Settings autoWidth or autoHeight to true will have Geyser automatically resize the label whenever you use its functions to change the text/images it displays. This is part of what allows for the zoomable map I linked to in the last section as well.

Get style information from a Label

This version of Mudlet also comes with the ability to query a Label for its style information. You can use getLabelStyleSheet and the Geyser equivalent to get the text stylesheet applied to the Label, if any. Or you can use getLabelFormat to get a format table in the same format as getTextFormat does for MiniConsoles. You can also use getBackgroundColor to get the background color set via setBgColor and the like for both Labels and MiniConsoles.

Can now play animated gif files

As shown in the following demo video by Vadi, you can now play animated gif movies in Mudlet! The package that drives this is available here. You could also potentially use it for stacking multiple state images (say, 28 different levels of fill for a highly stylized gauge) and setting the frame of the animation to the desired image.

video demonstrating animated gif files.

cecho support

Once Vadi added support for Mudlet to tell us all about a Label’s styling, I set to work using that information in order to make cecho, decho, and hecho work with Labels, as one of the biggest drawbacks to Labels until now is that they would behave differently if you tried to cecho to them with the same text you sent to a MiniConsole. Sending mixed format text to a Label in particular required knowledge of html tags. Now you can use the same functions you would use with a MiniConsole in a Label and get text formatted in a familiar way.

The test I used while developing looked like this.

tl = Geyser.Label:new({name = "tl"})
tl:setStyleSheet([[
  color: rgb(0,0,180);
  border-width: 1px;
  border-style: solid;
  border-color: gold;
  border-radius: 10px;
  font-size: 12.0pt;
  background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #98f041, stop: 0.1 #8cf029, stop: 0.49 #66cc00, stop: 0.5 #52a300, stop: 1 #66cc00);
]])
decho("tl", "<128,0,128:0,128,128>This <b>is</b><i> a<r> tester <b>and so far<r> it seems to <128,128,128:0,0,0>pass")
decho being used on a test label

Other improvements

  • bold, overline, underline, italics, and strikethrough support in ansi2decho and c/d/hecho2ansi functions.
  • getProfileStats returns a table listing the active, temp, and total number of triggers, aliases, etc for use in profiling your packages and optimizing things.
  • windowType will tell you if it’s a MiniConsole, the main window, etc
  • string.patternEscape will escape your string for use with Lua patterns

And more

As usual this is by no means an exhaustive list, but a selection of the things I want to highlight for one reason or another, but Mudlet is constantly evolving and changing and even I have a hard time keeping all the changes in mind.

Thanks for sticking with me through a longer than usual feature highlight, and thanks go out once again to Tamarindo for writing the sound section for me.

Happy MU*ing!

Help Wanted: Adventurers Inquire Within

Hello, Adventurers!

I am a curmudgeonly hermit atop a lonely mountain in search of an apprentice. I find that I have more good ideas for things than I have time to create them. I am willing to trade my knowledge of the more obscure corners of Mudlet and ask in exchange that you spend some of your time putting this dark knowledge to use for my purposes.

Or in corpo-speak: Hey there, have I got an opportunity for you!! I’m talking about on-the-job training in a fast-paced environment at my startup! We’re accepting applications for intern positions**, it’s a chance to get in on the ground floor of something special!

** All intern positions unpaid

Jokes aside, I need some code written that I just can’t find the time to write myself, and I’m willing to spend some personal 1-on-1 instruction time with someone in exchange for code crafted using the things I’ve taught you. 

The Ideal Applicant will have:

  • Some basic Lua and Mudlet knowledge. This is a tricky one. Ideally, for this to be of maximum benefit to us both you would be an intermediate Mudlet user. Know what a table is, have maybe made a Geyser widget or two, but not necessarily already be doing your own OOP Geyser object extensions or algorithm optimizations and the like yet. You’d still have a fair bit to learn but not be so green that I have to teach you what a trigger is. But I’m more likely to pick someone ‘under’ qualified than ‘over’ qualified because otherwise I’ll feel like I’m ripping you off.
  • A willingness to learn. If you already know how to code and feel like you’ve got a great grasp on Mudlet, this likely isn’t for you. Ditto if you’re not really interested in listening to me ramble on about the whys and wherefores. Cuz I’m gonna. If I’m not teaching you I’m just gonna feel like I’m using you for code, and I don’t think anybody wants that.
  • A willingness to do it my way. At least in the code you’re doing for me, I’m going to ask for certain code formatting, and for things to be PRs in github repositories I either already have or will set up. I’m 100% willing to explain why I do things the way I do, and even consider other ways of doing them, but at the end of the day I’ll have the final say for stuff in my own projects. I will give you the same in your own repos for anything I help you with.
  • The understanding that this is meant to be beneficial for both of us. If at any point either of us feels like they’re not getting anything out of the relationship, or things become more irritating than fun, or it starts to seem like a proper job, or anything like that, we can just agree to call it and part ways, hopefully before any animosity builds. I’m not looking to just steal free labor, and if you don’t feel like I’m teaching you enough for what I’m asking you to help with, fair enough and I’d rather we talk about it sooner rather than waiting for feelings to get hurt.
  • Your own goals! Teaching is a lot easier with a direction in mind. Plus then I can be helping you achieve them!
  • Empathy. I won’t tolerate racism, sexism, homophobia, or really just bigotry in general. This is a hard line for me.

I do not expect to find the Ideal Applicant per se, almost everything on the list (except that last one) is a suggestion/negotiable. Honestly, I’m not sure anyone will be interested at all. But I figure if I don’t ask, I’ll never find out!

If by some fluke you’re still interested after all that then fill out the form and I’ll be in touch.

Mudlet 4.14 Preview

Mudlet 4.14 is on the horizon, and I thought I might do a brief preview of a few of the features I’m looking forward to in the next release of Mudlet.

Dark theme

There has been an excellent dark theme package available on the Mudlet forums for a couple years now; however, a dark mode for Mudlet remains a frequently requested feature. I’m happy to report that this version marks the first one with an included dark theme. It’s using the Dark Fusion theme, if I remember correctly. Have some preview pics!

On the left above is the ‘light’ mode most Mudlet users are presented upon first install, followed by a picture highlighting where the option is in the preferences dialog, and finally a picture which shows a similar picture to the first, but in dark mode. The editor theme in the left picture is the default ‘Mudlet’ theme, on the right is ‘Monokai Midnight’ which is the one I usually use.

Named tempTimers and not-so-anonymous event handlers

Mudlet 4.14 will ship with a new internal IDManager which will manage the work of killing and recreating temp timers and event handlers for you, rather than forcing you to capture return IDs and handle it yourself. One of the more frequent issues I hear from people while sitting in the help channel is that they’ve got multiple tempTimers stacking up for the same thing, or their old anonymous event handlers aren’t cleaned up and fires four times for each raiseEvent call. Or in more practical terms

-- before 4.14
-- tempTimer
myTable = myTable or {}
if myTable.timerID then
  killTimer(myTable.timerID)
  myTable.timerID = nil
end
myTable.timerID = tempTimer(4, function() doSuperCoolThingInFourSeconds() end)

-- anonymous event handler
if myTable.vitalsID then
  killAnonymousEventHandler(myTable.vitalsID)
  myTable.vitalsID = nil
end
myTable.vitalsID = registerAnonymousEventHandler("gmcp.Char.Vitals", "myTable.handleVitals")

-- after 4.14
-- tempTimer
registerNamedTimer("Demonnic", "super cool thing", 4, function() doSuperCoolThingInFourSeconds() end)

-- event handler
registerNamedEventHandler("Demonnic", "vitals", "gmcp.Char.Vitals", "myTable.handleVitals")

-- for those who prefer to work with Lua 'objects'
myTable.idmgr = myTable.idmgr or getNewIDManager()
myTable.idmgr:registerTimer("super cool thing", 4, function() doSuperCoolThingInFourSeconds() end)
myTable.idmgr:registerEvent("vitals", "gmcp.Char.Vitals", "myTable.handleVitals")

The first parameter identifies what ‘name’ to file the handlers under, so that you can keep the timer/handler names themselves simpler without colliding with the names used by others. The second parameter is the name of the timer or event itself. These are both picked by you, and I would recommend using your own handle or character name for the first one if it’s just scripts you’re writing for yourself, or your package name for scripts you’re writing to give to others. In the latter case this helps to keep all of a particular package’s events and timer’s logically bundled together in the same manager. This is similar to the API for managing GMCP modules. No matter how many times you call registerNamedTimer or registerNamedEventHandler with the same names it will replace and restart them rather than allowing them to stack. These two functions on their own would cut way down on my own boiler plate code, but there are also stopNamed*, resumeNamed*, deleteNamed*, and getAllNamed* functions which should make working with tempTimers and named event handlers even easier and more convenient.

More error/debug message options

One of the things which Lua is not so great at in my opinion is error handling. Its error and assert functions do not print nearly enough stack trace information, and options for printing error messages without halting execution were nonexistent. Mudlet added debugc which allows for non-error messages to be printed to the error console, but it was not much different than a normal echo to a hidden miniconsole for debugging, and in my experience goes largely unused by most. I aim to replace all of these with printError and printDebug.



-- prints calm green debug message.
printDebug("This is a debug message, will only show in the error console in the script editor")
-- prints calm green debug message, with stacktrace
printDebug("This is a debug message, will only show in the error console in the script editor", true)

-- prints big red error. Does -not- halt execution
printError("This is an error and shows up anywhere using error() would")
-- prints big red error, including stack trace. Does not halt execution
printError("This is an error and shows up anywhere using error() would", true)
-- prints big red error, including stack trace, and halts execution
printError("This is an error and shows up anywhere using error() would", true, true)
show new printDebug and printError function outputs

Adjustable wait time for non-GA/EOR games

One common complaint with Mudlet on games which cannot be configured to send the telnet GA (Go Ahead) or EOR (End of Record) signals with their prompt is that it can feel a bit sluggish. This is because by default there is a 300ms wait to make sure the game has sent everything it needs to send and there aren’t more packets waiting to come in which may contain part of the line. Without a delay of some sort you can wind up with line breaks in unanticipated places and triggers which won’t fire consistently, and that’s no good either. 300ms can feel like a looooong time sometimes though, and if you have a fast connection with low latency it’s unlikely you need to wait that long. And starting with 4.14 Mudlet will allow you to adjust this wait time to suit your specific situation.

And more…

This post has only highlighted a few of the things coming up in Mudlet 4.14 that I’m personally excited for. There have been a lot of other tweaks and improvements made since 4.13 dropped; from minor bugs to new features there will be lots to explore and enjoy in the next version of Mudlet and I hope you’re all as excited as I am about the upcoming release.

Happy MU*ing,

demonnic

Introducing: Mudlet Mondays

I’ve been kicking around the idea of doing a weekly Q&A session for a while now, and after the impromptu presentation on Muddler I did (available on YouTube) was relatively successful I started kicking at it a little harder.

Now with the upcoming release of Mudlet 4.13 I’ve decided to just start it up and see how it goes. So starting on Monday, September 27th I will be doing a weekly discussion on the Mudlet Discord voice chat at 12pm(noon) Eastern/4pm UTC.

After the first one or two, we may adjust the time a little bit to try and accommodate as many people as possible but for now I plan to do it essentially on my Monday lunch break.

For the first one, I will be going over Mudlet 4.13 highlights, as it’s currently planned to be released by then. My intent is for it to be an informal question and answer format, with the occasional targeted lesson or lecture.

I’m looking forward to trying this out and I hope to see you there; however, you don’t need to wait for any presentation to ask your questions, you can always pop by the discord #help channel and we’ll do our best to help you.

Dev Environment – Windows

What we will try to accomplish

We will walk step by step through setting up your Windows machine to have a development environment similar to the one I use.

For windows, we will be leaning heavily on WSL (Windows Subsystem for Linux) as it is much much easier to get the toolchain setup that way than attempting to compile everything natively in Windows. As such the instructions past a certain point are very similar to the linux ones.

What are we installing, actually?

  • WSL
  • Ubuntu 20.04 for WSL (the rest is inside WSL unless specified)
  • basic build tools
  • Lua 5.1
  • LuaRocks
  • Luaformatter
  • Visual Studio Code (native and WSL)
  • Docker
  • muddler

Let’s get started!

I recommend doing this while your OS is up to date, though it isn’t required necessarily it’s just a good idea generally speaking. I will reuse screenshots from the Linux install instructions where they apply to both, in part because it’s essentially the same and in part because I forgot to get them while setting it up on my windows boot partition.

Install WSL

I am not going to detail how to install WSL for your windows machine here, rather I will link you to the official instructions from Microsoft. I recommend you install WSL2 if you can, but if you do not have the proper KVM/SVM cpu support then WSL1 is your only option. The only thing WSL1 changes is the docker and muddler installs at the end, and I will highlight those differences then.

So, follow the instructions at THIS LINK and preferably go for the full WSL 2 install. I would get the Ubuntu 20.04 image.

Build tools and dependencies

From here the instructions very closely mirror the Linux dev environment, but I will repeat them here with the minor differences. You will want to open the Ubuntu 20.04 terminal by going to Start->Ubuntu 20.04 LTS or opening it in Windows Terminal if you installed that.

# Make sure you have the latest package information
sudo apt-get update
# Install dependencies. Up to cmake are for luarocks/luaformatter. After that will be needed by docker's install later. Unzip was not needed for linux but is in wsl
sudo apt-get install build-essential gcc git libreadline-dev lua5.1 liblua5.1-0-dev unzip cmake apt-transport-https ca-certificates curl gnupg-agent software-properties-common
Installing system dependencies (pic from linux instructions)

LuaRocks

We will install from LuaRocks itself and not the system, as the system version is too old for our purposes.

*Note: when I wrote this, 3.5.0 was latest. I’ve bumped it to 3.9.0 to deal with GitHub turning off the git:// protocol on their servers, which causes LuaFormatter not to install.

cd ~ # move to the homedir.
# the wget is to download the archive file
wget https://luarocks.github.io/luarocks/releases/luarocks-3.9.0.tar.gz
# this extracts the file into its directory
tar zxvf luarocks-3.9.0.tar.gz
# Then we move into the directory
cd luarocks-3.9.0
# The provided configure script will determine where all the pieces luarock needs to build are kept on your system
./configure
download
extract and configure
# now we build
make
# and install it
sudo make install
build luarocks
install luarocks

You will now need to edit your .bashrc file. If you are using a shell other than bash, this filename will be different, use the file appropriate to your shell. Add the following line to the end of the file, save, and quit.

PATH=$PATH:/usr/local/bin:$HOME/luarocks-3.5.0/lua_modules/bin

LuaFormatter

Open a new terminal to pick up the pathing change above, and then run the following to download and install LuaFormatter

sudo luarocks install --server=https://luarocks.org/dev luaformatter

It will take a moment or two while it goes through everything. Don’t worry about any warnings it shows during the build, it’s not a problem unless it’s an error. The pictures below show the process in bare metal Ubuntu 20.04

I recommend downloading and using my .luaformatter file with the commands below, but you can obviously tune it to your own liking.

cd $HOME
wget https://raw.githubusercontent.com/demonnic/MDK/master/.luaformatter
getting .luaformatter file

VSCode

You can download VSCode from https://code.visualstudio.com/Download and do the basic install. Then open VSCode and install the wsl remote extension

first open
installed WSL remote extension

You will then want to open VSCode from within WSL by opening the WSL terminal and typing “code .”

Then we want to install the Lua language server.

install lua language server (note it says ‘install in WSL…’)

Then install the koihik.vscode-lua-format (picture not shown, same as above though.)

To make it use the optional .luaformatter file you downloaded earlier, you should hit ctrl+shift+p and type ‘settings’, then selecting ‘Preferences: Open Remote Settings (WSL: <your image>)’ as this opens the settings for inside your WSL setup. You’ll want to do this for all preferences except for things like your color themes, which get installed on the Windows side.

WSL remote settings

You want to add the following line to it at the top, just below the first {, with <user> replaced by your username.

"vscode-lua-format.configPath": "/home/<user>/.luaformatter",

The order of the entries doesn’t really matter, but this way it should definitely be in the right place.

Docker

If you were only able to install WSL1, you can skip this section as Docker only works in WSL2.

First, we add the GPG key for their repo and the repo itself to our sources.

# Download and install the GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# add Docker repository to repository list
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# update package information
sudo apt-get update

And then we install docker itself, setup the docker group if necessary, and add ourselves to it

# install docker
sudo apt-get install docker-ce docker-ce-cli containerd.io
# add docker group - this failed for me because the group
# already existed, but it doesn't hurt to run it if the group
# already exists, so I included it.
sudo groupadd docker
# and add yourself to it
sudo usermod -aG docker $USER

You can verify it’s working by running their hello world image. You may need to use a new WSL terminal so it sees your new group membership.

docker run hello-world
docker’s hello-world (from the linux instructions)

There are some reports that the service may not start, so if the docker run command asks if the daemon is running, try “sudo service docker start” and then try the docker run again.

If this still doesn’t work, I have had someone tell me they were able to succeed by installing Docker Desktop for windows and enabling the WSL2 options in there. And if that still fails, you can follow the java instructions below.

Muddler

With Docker

If you did the WSL2 install with docker, then this becomes quite simple really.

# pull the image
docker pull demonnic/muddler
# get the convenience wrapper (this step optional but recommended)
wget https://raw.githubusercontent.com/demonnic/muddler/master/muddle
chmod +x muddle
sudo mv muddle /usr/local/bin

To upgrage, you can just run “docker pull demonnic/muddler” again.

Without Docker

If you were only able to install WSL1, then you’ll need to install java8 and download the muddler distribution yourself. It’s still not difficult though.

# update the repo info
sudo apt-get update
# install java jre 8
sudo apt-get install openjdk-8-jre
installing jre-8 in WSL

And now we download muddler itself. At the time of writing 0.8 is the latest version, but you should check https://github.com/demonnic/muddler/releases for the latest version and replace the 0.8 with the current latest version

cd $HOME
wget https://github.com/demonnic/muddler/releases/download/0.8/muddle-shadow-0.8.tar
tar xvf muddle-shadow-0.8.tar
# next line only necessary if you are upgrading muddler. If it says no such file directory this is ok.
rm muddler
ln -s muddle-shadow-0.8 muddler

And then add the following line to the end of your .bashrc file (or appropriate file for your shell)

PATH=$PATH:$HOME/muddler/bin

Now you can open a new terminal and use muddler by simply typing ‘muddle’

To upgrade, just follow the instructions above again, replacing 0.8 with the new version you’re downloading. You can clean up the old directory using ‘rm -rf muddle-shadow-0.8’ or whatever version you’re moving from, but they’re only ~20m each so this is entirely optional and you’re unlikely to fill your hdd up with muddler versions.

Fin

Congratulations, you’ve got the basic setup done! As with the Linux setup, if you want to improve your experience even more you should look into adding Delwing’s Mudlet Docs to VSCode, and you may also get some use out of the snippets for VSCode HERE. You will want to open VSCode from the WSL terminal to ensure the remote WSL extension is used, at least the first time for any given directory. It claims it will remember this decision in future but I have not tested this myself in all honesty. I would run muddler from within the VSCode terminal, which can be accessed using ctrl+` , as this will ensure it is being run in WSL.

The RecogINATOR lives!

“Wait, what? The RecogINATOR? But… I thought you said the Questinator was going to be the first project?”, I hear none of you asking.

Well, the RecogINATOR! <thundercrash> is actually kind of a step in that direction, so I figured it sort of counts, and I wanted to write it, so… here we go.

So what is the recoginator?

I’ll just refer to it in the lowercase from here on out, as to invoke its full majestic glory too often could cause a rift in spacetime. The recoginator is essentially a tool for taking notes on the people who impressed you with their roleplay, so that you can recognize them for it. In Lusternia there is a command for mechanically doing so, “RECOGNISE <name> <reason>”; however, there is a limit on the number of times you can use it in a 24 hour period which resets at midnight UTC.

The problem of course being: what if you just had a really great RP session with 7 other people? How can you possibly choose only three?! Now you don’t have to. You can add all of them to the recoginator, and have them printed to a nice table you can interact with using the mouse or provided aliases. It will keep track of how many times you’ve recognized that day and stop after three. It will parse the output of “RECOGNISELOG” to see how many you’ve used for the day, adjusting for UTC.

Let’s go over some of the pieces and how I put them together. The full code is available on GitHub for browsing.

First, figure out what you need

I know, easier said than done. But it’s always best to at least sit and think about the various things you’re going to need in order to do what you want to do. In this case at a very high level I decided that I needed:

  • Something that lets me take notes on who to recognize
  • and can do the recognizing

But that’s not much to go on when it comes time to implement things. Let’s try breaking those down into smaller, individual items.

  • The ability to add a person to a queue of recognitions
  • The ability to keep a timestamp so we know when it happened
    • use UTC, since that is what the server uses
  • The ability to remove someone from the list without recognizing them
  • The ability to recognize someone from the list for the proper reason, and remove them.
  • The ability to track how many recognizes you have done that day
    • so you don’t pop more off the list than you can mechanically handle
  • The ability to undo the last thing you did
    • if you removed one or more items, put them back
    • if you added an item, pull it back off
  • The ability to save/load the files from the disk
    • a backup file, just in case, would probably be a good idea too.
  • The ability to display it all nicely
    • preferably interactively
  • The ability to do it all via alias as well

You may have noticed that they all start with ‘the ability to’ and that’s not by accident. By framing each one as an action, we define what the package is actually doing which gives us individual goals which are, well, actionable. It also gives you some idea of what aliases and interactions you’ll need to provide for the user, and starts you thinking about the steps needed to get there.

I suggest you note down what the items are whether you write them as actions or just items, so that later you can refer to it when you inevitably forget something, or think of something else to add. One or both of these is almost certain to happen.

Then make it

You don’t need to go down the list in order, but you should refer to it frequently as you proceed. I find it’s easiest to start with the data; once I know what I need to do, then I can figure out what information I need in order to do it. In this case for each entry I need the name, the reason, and a timestamp. The first two I’ll get from the user, the last I can generate myself.

In order to make the timestamps UTC I need to figure out the UTC offset of the user. I decided to do this by querying a website for the current UTC time and comparing it to the output of getTime() in Mudlet, then recording the offset. I go ahead and redo it every profile launch in case it changes (due to daylight savings or some such). This is the recoginator.setUTCoffset function.

Once I have this information, I’ll need to be able to get an adjusted time. I use recoginator.timeywimey() to adjust the output of getTime into a UTC time which I then use in the recoginator.add function, etc.

I won’t go over all the individual pieces, but hopefully that gives you an idea of how one piece can lead to the next. Keep working your way through the list and add the things you think of rather than just noting how cool they are and then forgetting them. If you have a piece and you can’t figure out how it fits with the rest, either that piece needs to go, or you need another piece to bridge the gap. Eventually, you’ll have all the pieces you laid out on the list made and you can package them together.

I did want to highlight the use of TableMaker to create the clickable table of entries however. The following five lines create the table and setup its formatting

local printTable = ft.TableMaker:new({allowPopups = true, autoEchoConsole = recoginator.window})
printTable:addColumn({name = "#", width = 3, textColor = "<orange>"})
printTable:addColumn({name = "Server Time", width = 20, textColor = "<orange>", alignment = "left"})
printTable:addColumn({name = " Name", width = 16, textColor = "<turquoise>", alignment = "left"})
printTable:addColumn({name = " Reason", width = (recoginator.width - 44), textColor = "<green>", alignment = "left"})

recoginator.makeRow is then responsible for creating each data row for the table.

-- just the return
  return {
    {index, commands, hints},
    entry.timeStamp,
    " " .. entry.name,
    " " .. entry.reason
  }

The first column in the row is clickable, so it returns a table of arguments for cechoPopup. The next three columns are just text, so it returns the text I want displayed. Makes it very easy to create clickable sheets. Each entry’s index entry is clickable to recognize, with additional options to remove or remove all of that user’s entries in the right click menu. Check here for more information on the TableMaker, but the result in this case looks like this

image

Relation to the Questinator

Lessons learned in making the interface and aliases for this will help when moving on to the Questinator, as they are both at their hearts note taking and action performing interfaces. It will also be of benefit in some other projects I have on my plate which I’m sure will come up again here once I make some actual progress on them.

Thanks for stopping by, and as always feel free to join me on my Discord or comment here if you have any questions and I’ll answer as quickly as I can.