Posted on January 23, 2026
Table primer: or how I stopped worrying and learned to love Lua tables
So look. I get it. Tables can be scary, especially if you’re not already a coder. But you really shouldn’t keep avoiding them. The dirty secret in Lua (and thus, Mudlet) is that it’s really just tables all the way down. If you set a global variable using target = "Bob" then congrats, you just made an entry in a table whether you knew it or not. So now that you know you’re already a veteran table user, let’s pull back the covers and get a look at what you can do with them.
Congratulations, it’s fraternal twins!
I’m just going to go ahead and spill the beans. One of Lua’s dark secrets is tables are actually multiple data types hiding in a trench coat. In this post we’ll focus on the two basic ones, but for more on some of the advanced usages you might want to take a look at the Targeted Tables post.
The first type of table is often referred to as ‘indexed’ or ‘array style’ tables. These tables have entries that are referenced using a contiguous number sequence. Which is a fancy way of saying the first thing in the myTable is myTable[1], the second thing is myTable[2], and so on with no gaps in the count.
The second type of table is often referred to as ‘keyed’ or ‘map style’ tables. These tables have entries which are retrieved using a specific key. For instance, if you want to know how much gold Bob has you might store and retrieve that information in bobTable[“gold”], their silver in bobTable[“silver”] etc.
If you’re familiar with coding in other languages, these line up pretty well with actual arrays/lists and maps/hashes from those other languages. If you’re not, that’s fine I’m about to go into more detail on each of them.
Indexed / Array tables.
The first type of table we’ll look as is the indexed tabled. They’re similar to arrays and lists in other languages, in that they hold … well, a list. The index you use to reference the items in the list isn’t necessarily related to the items themselves. Think of it like a row of small lockers, numbered 1 through N, where N is the number of items you need to store in the lockers. You put one item in each locker, and then when you need to reference the item you use the number of the locker it was placed in. If you need to add an item, you put another locker at the end of the line and N goes up by 1. If you remove an item, you remove the locker it is in and the number on all the lockers after it goes down by 1. To loop through all the items in such a table, you should use the ipairs function. Let’s break it down by task. If you want to know how many items are in the table, use #myTable
Indexed Tables: Removing items
The simplest way to remove an item from a table (and retrieve it at the same time) is to use the table.remove function. The signature for the function is table.remove(myTable, positionToRemove). If you call it without the positionToRemove argument, then it will remove the last item in myTable. It’s easiest to just demonstrate this.
local tbl = { "orange", "apple", "kiwi", "banana", "strawberry" }
local someFruit = table.remove(tbl)
-- tbl is now { "orange", "apple", "kiwi", "banana" }
-- someFruit is now "strawberry"
local someOtherFruit = table.remove(tbl, 2)
-- tbl is now { "orange", "kiwi", "banana" }
-- someFruit is still "strawberry"
-- someOtherFruit is now "apple"
Indexed Tables: Adding Items
Adding an item to an indexed table is similar to removing one, but you use table.insert rather than table.remove. The signature for this function is table.insert(myTable, [position], item) with position being an optional argument. If you do not provide a position, it will add the item at the end of the table (index myTable[#+1] ). If you do provide a position, it will insert it a new locker with that item in it, and then renumber all the lockers shifted up to be correct. Here, I’ll show you.
local tbl = { "orange", "kiwi", "banana" }
table.insert(tbl, "strawberry")
-- tbl is now { "orange", "kiwi", "banana", "strawberry" }
-- and the new item "strawberry" is accessible as tbl[4]
-- this call could also be rewritten as tbl[#tbl + 1] = "strawberry" and you may see it this way frequently as well for adding items to the end of an indexed table
table.insert(tbl, 2, "apple")
-- tbl is now { "orange", "apple", "kiwi", "banana", "strawberry" }
-- and the new item "apple" is accessible as tbl[2].
-- "strawberry" is now tbl[5], as it got shifted up to make room for "apple" in locker 2
Indexed Tables: How Many Entries?
This is pretty easy for indexed tables. Just use #tbl
local tbl = { "orange", "apple" }
-- #tbl is 2
tbl[#tbl + 1] = "kiwi"
-- tbl is now { "orange", "apple", "kiwi" }
-- #tbl is 3
Indexed Tables: looping
The recommended way for looping indexed tables is to use the ipairs function. Its signature is ipairs(tbl) but you’ll pretty much exclusively use it as the basis for a for loop. Let’s take a look.
local tbl = { "orange", "apple", "kiwi" }
for index, value in ipairs(tbl) do
print(index)
print(": ")
print(value)
print("\n")
-- we could use string.format or the .. concatenation operator here but
-- I didn't want to confuse the issue
end
-- prints the following
-- 1: orange
-- 2: apple
-- 3: kiwi
To break this down, the line for index, value in ipairs(tbl) do initializes the loop. It will go through each item in numerical order (so first 1, then 2, etc) until it runs out of lockers to check in. For each locker it will put the locker number in the local variable index and the item in the locker into value. These names are arbitrary, and you can use whatever you like. So if you’re looping through a list of fruit as I was above you could use for index, fruit in ipairs(tbl) do . If you don’t intend to do anything with the index, the convention is to use _ as the variable name, for example for _, fruit in iapirs(tbl) do is valid, and indicates to anyone reading it that you’re ignoring the index in the loop. This does mean you will run into some loops in the wild like for i,v in ipairs(tbl) do but I do not recommend you do this yourself, it makes the code harder to read. I only mention it to aid you in reading the code of others.
Indexed Tables: Sorting
Since indexed tables hold their order, you might find you want to sort them. The easiest way to sort an indexed table is using the table.sort function. Its signature is table.sort(tbl, [functionToUseForSorting]) . The function to use while sorting is optional and if it isn’t provided it will sort the items in the table using < > sorting (alphanumerically for strings, numerically for numbers, and probably error or give you a surprise with anything else. Supplying your own function allows you to sort things other than just lists of strings or numbers. I’ll provide a couple examples without and one with a function.
local tbl = { "orange", "apple", "kiwi" }
table.sort(tbl)
-- tbl is now { "apple", "kiwi", "orange" }
local tbl2 = { 3, 9, 1 }
table.sort(tbl2)
-- tbl2 is now { 1, 3, 9 }
-- with a function to reverse sort
-- table.sort passes 2 items to the function you give it, and the function should return true if a should come before b, and false if b should come before a.
local function reverseIt(a, b)
return a > b
end
table.sort(tbl, reverseIt)
-- tbl is now { "orange", "kiwi", "apple" }
table.sort(tbl2, reverseIt)
-- tbl2 is now { 9, 3, 1 }
Maps/key-value Tables
These tables are nice for collecting things you want to look up by what they are, but do not need any particular order to be maintained. For instance, if you’re collecting information on other players, you might make a table per player that contains information like their name, class, and citizenship among other things. If you have this information stored in the bob table then you might reference their name as bob["name"] or bob.name . These are both the same thing, and generally speaking it’s easier to use the latter unless there’s a space in the key. For instance, you would have to reference the are they evil key as bob["are they evil"] but in practice you’d probably just stuff it in bob.evil. Let’s break it down like we did the other.
Keyed Tables: Removing Items
To remove an item from a keyed table, you just set it to nil. Super easy.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
bob.citizenship = nil
-- bob is now { name = "Bob the Great", class = "Wizard" }
-- bob has been booted from Bobtopia
Keyed Tables: Adding Items
To add an item to a keyed table you just set it using =.
local bob = { name = "Bob the Great", class = "Wizard" }
bob.citizenship = "Bobtopia"
-- bob is now { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
-- bob has returned to the glorious state of Bobtopia
-- the same thing can be used to changed an item
bob.citizenship = "CliffTown"
-- bob is now { name = "Bob the Great", class = "Wizard", citizenship = "CliffTown" }
-- bob has been moved to CliffTown
Keyed Tables: How Many Entries?
Unfortunately keyed tables do not work using #bob . Thankfully, Mudlet ships with table.size(tbl) which will return the number of items in the table.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
-- table.size(bob) returns 3
bob.gold = 42069
-- table.size(bob) returns 4
Keyed Tables: looping
Unlike with indexed tables, ipairs will not work. Ipairs just goes up from 1 and adds 1 to it each time until your table comes up empty. Since keyed tables aren’t required to have numbers as keys, let alone in contiguous order, you have to reach instead to the pairs function. It will loop through each item in your table much like ipairs does, but it does not guarantee any sort of order. Each time you run a table through pairs it is possible it will display in a different order. In stock Lua, this is where you’d be stuck. But Mudlet ships with another looping function, spairs. I’ll demonstrate both below.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
for key, value in pairs(bob) do
print(key)
print(": ")
print(value)
print("\n")
end
-- might print
-- name: Bob the Great
-- class: Wizard
-- citizenship: Bobtopia
-- but also might print
-- citizenship: Bobtopia
-- name: Bob the Great
-- class: Wizard
--enter spairs! its signature is spairs(tbl, functionToSortWith) but by default it behaves like table.sort.
for key, value in spairs(bob) do
print(key)
print(": ")
print(value)
print("\n")
end
-- will print, every time:
-- citizenship: Bobtopia
-- class: Wizard
-- name: Bob the Great
-- To do a reverse printing like the reverse sorting above we have to do it a little different and pass the table we're sorting as well. For example
local function reverseIt(tbl, a, b)
return tbl[a] > tbl[b]
end
for key, value in spairs(bob, reverseIt) do
print(key)
print(": ")
print(value)
print("\n")
end
-- will print, every time:
-- name: Bob the Great
-- class: Wizard
-- citizenship: Bobtopia
For some more advanced examples, check out the sorting functions from the SortBox in the MDK.
Keyed Tables: Sorting
There is no way to sort a keyed table in place. You can sort it when you go to display it using spairs as I show above, or use another table to store the order you wish to traverse the keys in for arbitrary sorting at loop time. Here’s an example.
local sortOrder = { name, citizenship, class }
-- neither alphabetical or reverse alphabetical
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
for _, keyname in ipairs(sortOrder) do -- loop the sortOrder table in a predictable fashion
print(keyname) -- the keyname we're printing now
print(": ")
print(bob[keyname]) -- we use the [] way of accessing it here since we don't want bob["keyname"] but rather bob["name"] or bob["class"] etc.
print("\n")
end
-- this will reliable print the following:
-- name: Bob the Great
-- citizenship: Bobtopia
-- class: Wizard
Summary
Whew. OK, I know that was kind of a lot, but hopefully it did a good job of showing the differences between the two main table types found in Lua, and how to interact with them. Here’s a quick summary/reference.
Indexed Tables:
- Are good for lists of things, like a grocery, shopping, or packing list
- Have their order preserved
- Have their size reported by #tbl
- Are accessed using numbers from 1 to #tbl. IE tbl[1], tbl[2], etc
- Can be looped in their preserved order using the ipairs function
- Has items added by table.insert and removed by table.remove
- Can be sorted in place (with the order preserved) using table.sort
Keyed Tables:
- Are good for referencing specific information about something. For instance, a table of information about a person, or of configuration items and their values, things like that
- Do not have their order preserved
- Do not have their size reported by #tbl, you must use table.size or count them yourself in a pairs loop instead.
- Are accessed using a key, such as “gold” or “fontSize”. IE bob.gold or bob[“gold”], config.fontSize
- Can be looped without preserving any kind of order using the pairs function
- Can be looped in a sorted manner using the ipairs function
- Has items added by simply assigning them using =, IE
bob.gold = 42069 - Has items removed by assigning
nilto them. IEbob.gold = nil - Cannot be sorted in place at all.
As always I’m happy to answer additional questions on the Mudlet discord, or wherever you might happen to run into me. I hope this has helped at least some of you come to grips with what tables are, why you might want to use one, and how to interact with them when you do.
Happy MU*ing!
Posted on May 15, 2023
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()

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 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.

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 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!
Posted on January 14, 2023
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!
Posted on October 13, 2022
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

Posted on May 20, 2022
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.

Posted on February 14, 2022
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.
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")

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!
Posted on December 28, 2021
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.
Posted on October 31, 2021
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)

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
Posted on September 20, 2021
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.
Posted on July 21, 2021
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

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


# now we build
make
# and install it
sudo make install


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

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


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.

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.

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

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

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.
