Manual:Scripting

From Mudlet
Jump to navigation Jump to search

Scripting with Mudlet

Lua tables can basically be considered multidimensional arrays and dictionaries at the same time. If we have the table matches, matches[2] is the first element, matches[n+1] the n-th element.

<lua>

a = "Tom"
matches[2] = "Betty"
b = matches[2]
c = a .. b and e will equal "TomBetty"

</lua>

To output a table you can use a convenience function - display(mytable), which is built into Mudlet.

Lua interface functions to Mudlet - or how do I access triggers, timers etc. from Lua scripts

How to get data from regex capture groups? Regular expression capture groups (e.g. "(\d+)" ) are passed on to Lua scripts as a Lua table matches. To make use of this information inside Lua scripts, you need to specify the number of the capture group within the regex.

Example: You have (\d+) weapons and they are (?:(\b\w+\W+)+)

This regex contains 3 capture groups, but only the 2 green colored ones contain data as the red capture group is a non-capturing group. Consequently, your Lua script gets passed only 2 instead of 3 capture groups and matches[4] is undefined.

In your Lua script you may write following program in order to print the number and status of your weapons on the screen:

You have (\d+) weapons and they are (?:(\b\w+\W+)+)

<lua>

number_of_weapons = matches[2]
status_of_weapons = matches[3]
notice = number_of_weapons .. status_of_weapons
echo( notice )
send( "put weapons in backpack" )
-- the following 2 lines color the first capture
-- group red and the second group blue
-- see below for details
selectCaptureGroup( 2 )
setFgColor( 255,0,0 )
selectCaptureGroup( 3 )
setFgColor( 0,0,255 )</lua>

The best way is to use selectCaptureGroup( number ) to select the proper capture group and then perform your actions on it e.g. replace(), highlight etc. Note: Both selectCaptureGroup() and matches[n] start with group 1 (which is the whole match. The defined capture groups start with 2).

How to select all occurrences of "Tom" and highlight them?

You add a function like this to a script containing you main function definitions. Note that script items differ from all other "scripts" in triggers, timers, actions etc. because they require you to put your code in proper functions that can be called by your other trigger-scripts, timer-scripts etc. via normal function calls. Trigger-scripts, timer-scripts etc. cannot contain any function definitions because they are automatically generated functions themselves because this makes usage a lot easier.

To come back to our question how to select all occurrences of "Tom" and highlight them:

<lua>

 function setBgColorAll( word, r, g, b )
   i = 0
   word_count = 1
   while i > -1 do
       i = selectString(word, word_count)
       if i == -1 then
            return
       end
       word_count = word_count +1
       setBgColor( r, g, b )
   end
 end

</lua>

Then you simply define a substring matching trigger on the word "Tom" and in the trigger script you call above function:

<lua>setBgColorAll("Tom", 255,50,50)</lua>

Sending commands to the MUD or printing information messages on the screen

To print information messages on the session screen you can use the echo( message ) function, or insertText( text). Currently, it only takes one string as argument.

To send a command to the MUD, you can use the send( command ) function. In Alias scripts the command that is being sent to the MUD is contained in the variable command that you can change in the context of Alias scripts. Alias take regular expressions, as well. As a result, you can use following regex and script to talk like Yoda: Perl regex:

say (\w+).*(\w*).*(.*)

script:

<lua> send( "say " .. matches[4] .." " .. matches[2] .." ".. matches[3] ) </lua>

Note: The variable "command" contains what was entered in the command line or issued via the expandAlias( ) function. If you use expandAlias( command ) inside an alias script the command would be doubled. You have to use send( ) inside an alias script to prevent recursion. This will send the data directly and bypass the alias expansion.

Changing text from the MUD or reformatting text (highlight, make bold etc.)

When sending commands to the MUD - from now on referred to as output stream - alias scripts find the command that was issued by the user stored in the variable "command".

By manipulating the value, the command can easily be changed before it is being sent to the MUD.

However, things get much more complicated with the data received from the MUD – from now on referred to as input stream. Before triggers can be run on the MUD data, Mudlet has to strip all format codes from the text and store it in data structures associated with the text. Consequently, the text that is being passed on to the trigger processing unit is a small subset of the data received from the MUD. If you want to edit, replace, delete or reformat text from within your trigger scripts you have to keep this in mind if you don’t want to lose all text format information such as colors etc.

As the text is linked with data structures containing the format of the text, the cursor position inside the line is important if data is being changed. You select a word or a sequence of characters from the line and then issue commands to do actions on the selected data.

Replacing the word "Tom" with "Betty" in the line: Jim, Tom and Lucy are learning a new spell. This could be done with following script:

<lua> selectString("Tom",1) replace("Betty") </lua>

Things get more complicated if there are two or more occurrences of "Tom" in the line e.g. Jim and Tom like magic. Jim, Tom and Lucy are learning a new spell.

The above example code would select the first occurrence of "Tom" in this line and ignore the second. If you want to work on the the second occurrence of "Tom" you have to specify the occurrence number in the call to select().

<lua> selectString( "Tom", 2 ) replace( "Betty" ) </lua>

This code would change the second "Tom" and leave the first "Tom" alone. The function call

<lua>replaceAll( "Betty" )</lua>

will replace all occurrences of "Tom" with "Betty" in the line if "Tom" has been selected before. replaceAll() is a convenience function defined in LuaGlobal.lua.

Colorization example: You want to change to color of the words "ugly monster" to red on a white background.

You add a new trigger and define the regex: ugly monster In the script you write:

<lua> selectString("ugly monster", 1 ) setFgColor(255,0,0) setBgColor(255,255,255) resetFormat() </lua>

Another means to select text is to select a range of characters by specifying cursor positions. If we have following line: Jim and Tom like magic. Jim, Tom and Lucy are learning a new spell.

<lua>selectSection( 28, 3 )</lua>

This example would select the second Tom. The first argument to selectSection is the cursor position within the line and the second argument is the length of the selection.

<lua>selectCaptureGroup( number )</lua>

This function selects the captureGroup number if you use Perl regular expressions containing capture groups. The first capture group starts with index 1.

Deleting Text - Gagging

<lua>deleteLine()</lua>

This function deletes the current line - or any line where the cursor is currently placed. You can use repeated calls to this function to effectively erase the entire text buffer. If you want to delete or gag certain words only, you can select the text that you want to delete and then replace it with an empty string e.g:

If you get this line form the MUD: "Mary and Tom walk to the diner."

<lua>selectString( "Tom", 1 ) replace( "" )</lua>

Then the output will be changed to: "Mary and walk to the diner."

Cursor Movement and Cursor Placement

moveCursor( windowName, x, y ) This will move the user cursor of window windowName to the absolute (x/y) coordinates in the text.

moveCursor( "main", 20, 3950 ) will move the cursor on the 20th character from the left on line number 3950. To determine the current cursor position you can use getLineNumber() and getColumnNumber() as well as getLastLineNumber() to get the number of the last line in the text buffer. moveCursorEnd("main") will move the cursor of the main display to end of the buffer. This is always a new line of size 1 containing the character \n.

number_of_last_line_in_text = getLineCount() returns the number of the last line of the text in the console buffer. This number will change as soon as a new \n is printed either by the user or when a new line arrives from the MUD. All lines from the MUD are terminated with \n which is called line feed or the new line character. This control character ends the current line and move the cursor to the beginning of the next line, thus creating a new, empty line below the line that contains the \n.

line_number_of_the_current_cursor_position = getLineNumber()

column_number_of_the_current_cursor_position = getColumnNumber()

luaTable_containing_textlines = getLines( absolute_line_number_from, absolute_line_number_to ) this will return a Lua table containing all lines between the absolute positions from and to. NOTE: This function uses absolute line numbers, not relative ones like in moveCursor(). This little demo script shows you how to use cursor functions:

moveCursor() return true or false depending on whether the move was possible.

User defined dockable windows

You may want to use dock windows to display information you gathered in your scripts, or you may want to use them as chat windows etc. Adding a user defined window:

openUserWindow( string window_name )

echoUserWindow( string window_name, string text )

setWindowSize( int x, int y )

clearWindow( string window_name )

Dynamic Timers

tempTimer( double timeout, string/function lua code_to_execute, string/float/int timer_name )

disableTimer( name )

enableTimer( name )

Dynamic Triggers

triggerID = tempTrigger( substring pattern, code ) creates a fast substring matching trigger

triggerID = tempRegexTrigger( regex, code ) creates a regular expression matching trigger

triggerID = tempBeginOfLineTrigger( begin of line pattern, code ) creates a fast begin of line substring trigger

Advanced Lua

Lua version

Mudlet uses Lua 5.1.

Lua tables

A good overview of tables is available on Lua's wiki in the TablesTutorial. Nick Gammon has also written a nice overview on how to deal with Lua tables.

Creating Tables

Tables can be created manually, by scripting or by a return value from a function (e.g. getRoomExits())

-- create a simple table
local myTable = { value1, value2, value3 }
local exits = { "north", "south", "west" }

-- create a table with keys in uses the following format
local myTable = { key1 = value1, key2 = value2, key3 = value3 }
local stats = { hp = 1234, mana = 321, moves = 678 }

Tables can also be a mixture of the above!

Accessing Tables

There are a number of different ways to access tables and it depends on how they are created.

Here are a few examples from the tables created above.

  print(exits[1])     -- will display north, tables start indexing at position 1
  print(stats["hp"])  -- will display 1234
  print(stats.hp)     -- will display 1234, a shorthand method
  print(stats[1])     -- will return nothing (or nil), there is no key called 1

Assigning Values to Table Elements

Now you know how to access table elements, addressing and assigning values to them works in the same way as normal variables.

  
stats["hp"] = 2000
-- or -- stats.hp = 2000
exits[1] = "down"

-- create a new entry in the stats table with the key called gold and assign it the value of 35
stats["gold"] = 35

Deleting a Table Element

Assigning an element the value of nil will remove it entirely.

stats["gold"] = nil
-- the stats table now only contains hp, mana and moves

See also: Table Functions API

How to use multimatches[n][m]

multimatches[n][m] is the complement of matches[n] when matching multi-line triggers. multimatches[n][m] stores its matches by lines, inside each line are the relevant matches to it. The following example can be tested on the game batmud.bat.org:

In the case of a multiline trigger with these 2 Perl regex as conditions:

^You have (\w+) (\w+) (\w+) (\w+)

^You are (\w+).*(\w+).*

The command "score" generates the following output on batMUD:

 You have an almost non-existent ability for avoiding hits.
 You are irreproachably kind.
 You have not completed any quests.
 You are refreshed, hungry, very young and brave.
 Conquer leads the human race.
 Hp:295/295 Sp:132/132 Ep:182/181 Exp:269 >

If you add this script to the trigger:

showMultimatches()

The script, i.e. the call to the function showMultimatches() generates this output:

 -------------------------------------------------------
 The table multimatches[n][m] contains:
 -------------------------------------------------------
 regex 1 captured: (multimatches[1][1-n])
           key=1 value=You have not completed any quests
           key=2 value=not
           key=3 value=completed
           key=4 value=any
           key=5 value=quests
 regex 2 captured: (multimatches[2][1-n])
           key=1 value=You are refreshed, hungry, very young and brave
           key=2 value=refreshed
           key=3 value=young
           key=4 value=and
           key=5 value=brave
 -------------------------------------------------------

The function showMultimatches() prints out the content of the table multimatches[n][m]. You can now see what the table multimatches[][] contains in this case. The first trigger condition (=regex 1) got as the first full match "You have not completed any quests". This is stored in multimatches[1][1] as the value of key=1 in the sub-table matches[1] which, in turn, is the value of key=1 of the table multimatches[n][m].

The structure of the table multimatches:

multimatches {
                1 = {
                       matches[1] of regex 1
                       matches[2] of regex 1
                       matches[3] of regex 1
                              ...
                       matches[m] of regex 1 },
                2 = {
                       matches[1] of regex 2
                       matches[2] of regex 2
                             ...
                       matches[m] of regex 2 },
                 ...         ...
                n = {
                       matches[1] of regex n
                       matches[2] of regex n
                             ...
                       matches[m] of regex n }
}

The sub-table matches[n] is the same table matches[n] you get when you have a standard non-multiline trigger. The value of the first key, i. e. matches[1], holds the first complete match of the regex. Subsequent keys hold the respective capture groups. For example: Let regex = "You have (\d+) gold and (\d+) silver" and the text from the game = "You have 5 gold and 7 silver coins in your bag." Then matches[1] contains "You have 5 gold and 7 silver", matches[2] = "5" and matches[3] = "7". In your script you could do:

myGold = myGold + tonumber( matches[2] )
mySilver = mySilver + tonumber( matches[3] )

However, if you’d like to use this script in the context of a multiline trigger, matches[] would not be defined as there are more than one regex. You need to use multimatches[n][m] in multiline triggers. Above script would look like this if above regex would be the first regex in the multiline trigger:

myGold = myGold + tonumber( multimatches[1][2] )
mySilver = mySilver + tonumber( multimatches[1][3] )

What makes multiline triggers really shine is the ability to react to game output that is spread over multiple lines and only fire the action (=run the script) if all conditions have been fulfilled in the specified amount of lines.

Regex in Lua

Lua has its own, fast and lightweight pattern matching built in - see 20.2 – Patterns. Should you need proper regex however, Mudlet has lrexlib available - which works as a drop-in replacement; replace string. with rex. - for example string.gsub to rex.gsub. See manual for documentation.

-- example: strip out trailing .0's from text using a regex
local stripped = rex.gsub("1.0.0", [[(\.0+)+$]], '')
print(stripped)

The lrexlib manual can be hard to read, so the instructions in this section should provide most of what you need.

lrexlib comes preinstalled in Mudlet and it is automatically bounded to the rex variable, as the rex_pcre version. If you want to test your lua code outside Mudlet, you have to install it yourself. The easiest installation method uses luarocks. There are a few flavors that you can specify, but in this section we will focus on the POSIX and PCRE flavors.

luarocks install --local lrexlib-POSIX
luarocks install --local lrexlib-PCRE

After installation, you can use the library:

> local rex = require("rex_posix")

If you set the flavor to rex_pcre, your code should work afterwards in Mudlet.

Keep in mind that precomputing a pattern with the new function has the advantage that objects resulted will be garbage collected and that regexes are greedy by default.

Another thing that may surprise some users is that, when escaping, you need to backslashes:

> rex.match("The answer is 42", "(\\d+)")

You can also achieve this using long string literal syntax, where you don't need the double escape:

> rex.match("The answer is 42", [[(\d+)]])

There are a few functions that you need to know: gsub, count, split, find, match.

count

The count function counts the matches:

> print(rex.count("ab", "."))
2

A single dot here has the same meaning it has in POSIX regexes.

You can also precompute the pattern, using the new function:

> print(rex.count("ab", rex.new"."))
2

Remember that regexes are greedy by default:

> print(rex.count("aaa", ".*"))
1

split

The split function splits a string according to a regex pattern:

> for a in rex.split("aba", "b") do
print(a)
end
a
a
> for a in rex.split("aba", ",") do
print(a)
end
aba

Be careful though, because while the split consumes all the characters with the same value, if they are adjacent, you will also have the borders between them resulting in an empty string:

> for a in rex.split("abba", "b") do
print(a)
end
a

a

find

The find function searches for the first match, and returns the offsets where the match starts and ends.

> print(rex.find("aaa", "b"))
nil

> print(rex.find("aba", "b"))
2	2

find also takes an additional parameter, which is the starting position:

> print(rex.find("abab", "b"))
2	2
> print(rex.find("abab", "b", 3))
4	4

A negative starting position will circle back from the end:

> print(rex.find("ababab", "b", -1))
6	6
> print(rex.find("ababab", "b", -3))
4	4

match

The match function works similarly to the find function, but returns the matches instead of the indices:

> print(rex.match("abcdef", "ab"))
ab
> print(rex.match("abcdef", ".*"))
abcdef
> print(rex.match("abcdef", "ab.."))
abcd
> print(rex.match("abcdef", ".*", 3))
cdef
> print(rex.match("abcdef", ".*", -3))
def

gsub

The gsub function replaces the pattern in the string, with the value in the third parameter:

> print(rex.gsub("abcdef", "[abef]+", ""))
cd	2	2

This snippet replaces all the "a", "b", "e" or "f" characters with the empty string, so only "c" and "d" remain.

The fourth parameter determines how many replacements are made:

> print(rex.gsub("abcdef", "[abef]+", "", 1))
cdef	1	1

Because the regex is greedy, the "ab" is a single match, so it gets replaced.

PCRE specific functionality

The case insensitive flag:

> rex = require("rex_pcre")
> flags = rex.flags()
> print(rex.find("ABab", "a", 0, flags.CASELESS))
1	1

Coroutines

Mudlet supports Lua's coroutines starting with 3.2.0, which opens up a whole lot of possibilities for the way you program your scripts. A pretty technical description and a tutorial is available, but for a quick explanation, think of coroutines allowing you to pause and resume running a function. If you're familiar with other clients, it is something like a #wait statement where a script will stop running, except unlike a #wait which auto-resumes the script later, you resume it when it yourself whenever you'd like.

Here's an example - add this code as a new 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

Make a ^ritual$ alias - which seems big, but that's just because there's a lot of explanation inside it:

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

Now try doing the ritual command. You'll see that the send()'s are being sent one at a time, instead of all at once as they would have been without the yields. Cool, huh?

You can also install the demo as a package - paste this into Mudlet:

lua installPackage("http://wiki.mudlet.org/images/3/36/Ritual-coroutine-demo.zip")

Note that if you'll be using coroutines as part of a package you'll give to others, remember about the if mudlet.supports.coroutines then return end bit. Older Mudlets that don't support coroutines might crash, which sucks. Newer ones that do support them are completely fine, however!

Coroutines have many uses: finite state machines, running intensive tasks (yielding every once in a while so Mudlet isn't frozen), and so on.

Batching Processing with Coroutines

Say you want to process large jobs that lock up the user interface for the duration of the job, such as colouring entire maps, deleting rooms or processing large files. You can process these jobs in small amounts and then yield to the main loop where you can display a progress meter.

local function task()

  -- process a large task, like colouring all rooms in a map
  for i = 1, 100 do
    -- setRoomEnv(i, ...)                  -- perform a small part of the job, colour just one room
    echo("processed part of a task.. ")
    coroutine.yield()                      -- now yield to the progress meter function
  end
end

function batch()

  local routine = coroutine.create(task)
  
  for i = 0, 100 do
    coroutine.resume(routine, i)
    echo(i .. "% complete.\n")    
  end
end

All running lua batch() you will see;

processed part of a task.. 0% complete.
processed part of a task.. 1% complete.
processed part of a task.. 2% complete.
processed part of a task.. 3% complete.
processed part of a task.. 4% complete.
processed part of a task.. 5% complete.

...

Introspective facilities

Lua brings a helpful debug.getinfo(function) function, which gets you some information about where a function comes from: whenever it's your own, or one defined by Mudlet (in C++ or Lua). You can also use it to get more information on the Alias## / Trigger## objects you see in the error console:

If you're working on coding Mudlet itself, use this function to tell where the actual definition of a function is.

Event System

Events in Mudlet allow a paradigm of system-building that is easy to maintain (because if you’d like to restructure something, you’d have to do less work), enables interoperability (making a collection of scripts that work with each other is easier) and enables an event-based way of programming.

The essentials of it are as such: you use Scripts to define which events a function should listen to, and when the event is raised, the said function(s) will be called. Events can also have function parameters with them, which’ll be passed onto the receiving functions.

Registering an event handler via UI

Registering an event handler means that you’ll be telling Mudlet what function it should call for you when an event is raised, so it’s a two step process - you need to tell it both what function you’d like to have called, and on what event it should be called.

To tell it what function should be called, create a new script, and give the script the name of the function you’d like to be called. This is the only time where an items name matters in Mudlet. You can define the function right inside the script as well, if you’d like.

Next, we tell it what event or events this function should be called on - you can add multiple entries. To do that, enter the events name in the Add User Defined Event Handler: field, press enter, and it’ll go into the list - and that is all.

Gui-event-handler.png

Registering an event from a script

You can also register your event with the registerAnonymousEventHandler(event name, function reference, [one shot]) function inside your scripts:

-- example taken from the God Wars 2 (http://godwars2.org) Mudlet UI - forces the window to keep to a certain size
function keepStaticSize()
  setMainWindowSize(1280,720)
end -- keepStaticSize

registerAnonymousEventHandler("sysWindowResizeEvent", keepStaticSize, false)

Note Note: : registerNamedEventHandler can also be used. Doing so causes Mudlet to handle saving of the IDs for you.

Note Note: Mudlet also uses the event system in-game protocols (like GMCP, MSDP and others).

Raising a custom event

To raise an event, you'd use the raiseEvent() function:

raiseEvent(name, [arguments...])

It takes an event name as the first argument, and then any amount of arguments after it which will be passed onto the receiving functions.

Example of registering and raising an event

Add a script to each profile you need the event.

-- This is the function that will be called when the event is raised.
-- "event" is set to the event name.
-- "arg" is the argument(s) provided with raiseEvent/raiseGlobalEvent.
-- "profile" - Is the profile name from where the raiseGlobalEvent was triggered from
--             It is 'nil' if raiseEvent() was used.
function onMyEvent(event, arg, profile)
  echo("Event: " .. event .. "\n")
  echo("Arg  : " .. arg .. "\n")
  -- If the event is not raised with raiseGlobalEvent() profile will be 'nil'
  echo("Profile: " .. (profile or "Local") .. "\n")
end

-- Register the event handler.
registerAnonymousEventHandler("my_event", onMyEvent)

To raise the event you can call:

-- Trigger only in the current profile:
raiseEvent("my_event", "Hello world!")

-- Trigger the event in all OTHER profiles:
raiseGlobalEvent("my_event", "Hello World!")

To review, count and extract from an unknown number of arguments received by an event:

function eventArgs(...)
  local args = {...}
  local amount = #args
  local first = args[1]
  echo("Number of arguments: " .. amount)
  echo("\nTable of all arguments: ")
  display(args)
  echo("First argument: " .. first)
  echo("\n\n")
end

Gamepad functionality

Note Note: will be removed in 4.18 as this is blocking Mudlet updates and is not used by players.

Gamepad functions are not fully supported with all operating systems and hardware. Windows supports XInput devices like Xbox 360 and Xbox One controllers. DirectInput controllers like a PlayStation 4 controller can be translated to XInput with third party software. Mudlet on Linux can support gamepads but only if you compile it yourself. The automated builds are made using the oldest supported version of Ubuntu, and the Qt5 gamepad module was not available on Ubuntu 18.04 LTS.

Mudlet-raised events

Mudlet itself also creates events for your scripts to hook on. The following events are generated currently:

AdjustableContainerReposition

Raised while a adjustable container is re-positioned.

function repositioningContainer(eventName, containerName, width, height, x, y, mouseAction)
  print(f"{containerName}: {x}x, {y}y, {width}x{height}, using mouse? {mouseAction}")
end

registerAnonymousEventHandler("AdjustableContainerReposition", repositioningContainer)

AdjustableContainerRepositionFinish

Raised when an adjustable container done being re-positioned.

function finishedRepositioning(eventName, containerName, width, height, x, y)
  print(f"{containerName}: {x}x, {y}y, {width}x{height}")
end

registerAnonymousEventHandler("AdjustableContainerRepositionFinish", finishedRepositioning)

channel102Message

Raised when a telnet sub-option 102 message is received (which comprises of two numbers passed in the event). This is of particular use with the Aardwolf MUD who originated this protocol. See this forum topic for more about the Mudlet Aardwolf GUI that makes use of this.

mapModeChangeEvent

Raised when the mapper is switching between "view-only" and "editing" mode of the Visual Map Editor. A value of "editing" or "viewing" will be given as argument to indicate which mode was entered.

mapOpenEvent

Raised when the mapper is opened - either the floating dock or the in-Mudlet widget.

sysAppStyleSheetChange

Raised when setAppStyleSheet is called and a new stylesheet applied to Mudlet.

-- This will respond to a future (as yet unpublished) addition to the Mudlet code that will allow some
-- of a default application style sheet to be supplied with Mudlet to compensate for some text colors
-- that do not show up equally well in both light and dark desktop themes. That, perhaps, might finally
-- allow different colored texts to be uses again for the trigger item types!
function appStyleSheetChangeEvent( event, tag, source )
  if source == "system" then
    colorTable = colorTable or {}
    if tag == "mudlet-dark-theme" then
      colorTable.blue = {64, 64, 255}
      colorTable.green = {0, 255, 0}
    elseif tag == "mudlet-light-theme" then
      colorTable.blue = {0, 0, 255}
      colorTable.green = {64, 255, 64}
    end
  end
end
Mudlet VersionAvailable in Mudlet3.19+

sysBufferShrinkEvent

Raised when the oldest lines are removed from the back of any console or buffer belonging to a profile because it has reached the limit (either the default or that set by setConsoleBufferSize). The two additional arguments within the event are: the name of the console or buffer and the number of lines removed from the beginning of the buffer. This information will be useful for any situation where a line number is being stored as it will need to be decremented by that number of lines in order to continue to refer to the same line (assuming that it is still present - indicated by the number remaining positive) after the oldest ones have been removed.

Mudlet VersionAvailable in Mudlet4.17+

sysConnectionEvent

Raised when the profile becomes connected to a MUD.

sysCustomHttpDone

Raised whenever a customHTTP() request completes successfully. Arguments with the event are the URL that the request was sent to and the response body (if it's less than 10k characters) and HTTP method.

See also sysCustomHttpError.

sysCustomHttpError

Raised whenever a customHTTP() request fails. Arguments are the error message and the URL that the request was sent to and HTTP method.

See also sysCustomHttpDone.

sysDataSendRequest

Raised right before a command from the send(), sendAll() functions or the command line is sent to the game - useful for keeping track of what your last command was, manipulating input or even denying the command to be sent if necessary with denyCurrentSend().

sysDataSendRequest will send the event name and the command sent (in string form) to the functions registered to it. IE: commandSent in the example below will be "eat hambuger" if the user entered only that into command line and pressed enter, send("eat hamburger"), sendAll("eat humburger", "eat fish") or sendAll("eat fish", "eat humburger")

Note: if you'll be making use of denyCurrentSend(), you really should notify the user that you denied their command - unexperienced ones might conclude that your script or Mudlet is buggy if they don't see visual feedback. Do not mis-use this and use it as keylogger either.

-- cancels all "eat hambuger" commands
function cancelEatHamburger(eventName, commandSent)
  if commandSent == "eat hamburger" then
    denyCurrentSend() --cancels the command sent.
    cecho("<red>Denied! You can't do "..commandSent.." right now.\n")
  end --if commandSent == eat hamburger
end

registerAnonymousEventHandler("sysDataSendRequest", cancelEatHamburger)

If you wanted to control input you could set a bool after a trigger. This is useful if you want alias like control, do not know what text will be entered, but do know a trigger that WILL occur just before the user enters the command.

function controlInput(_, command) 
  if changeCMDInput then
    changeCMDInput = false --set this if check to false to it doesn't occur every input
    --Also set the bool check BEFORE any send() functions within a sysDataSendRequest function
    sendAll(command .. "some target", command .. "some other target", true) --Duplicate and unknown command
    denyCurrentSend() --Deny the original command, not the commands sent with sendAll.
  end
end
registerAnonymousEventHandler("sysDataSendRequest", controlInput)

Take note that functions registered under sysDataSendRequest WILL trigger with ALL commands that are sent.

sysDeleteHttpDone

Raised whenever a deleteHTTP() request completes successfully. Arguments with the event are the URL that the request was sent to and the response body (if it's less than 10k characters).

See also sysDeleteHttpError.

sysDeleteHttpError

Raised whenever a deleteHTTP() request fails. Arguments are the error message and the URL that the request was sent to.

See also sysDeleteHttpDone.

sysDisconnectionEvent

Raised when the profile becomes disconnected, either manually or by the game.

If you'd like to automatically reconnect when you get disconnected, make a new Script and add this inside:

registerAnonymousEventHandler("sysDisconnectionEvent", reconnect)

sysDownloadDone

Raised when Mudlet is finished downloading a file successfully - the location of the downloaded file is passed as a second argument, the file size is passed as the third, and the HTTP response is passed as the fourth.

Example - put it into a new script and save it to run:

-- create a function to parse the downloaded webpage and display a result
function downloaded_file(_, filename)
  -- is the file that downloaded ours?
  if not filename:find("achaea-who-count.html", 1, true) then return end

  -- read the contents of the webpage in
  local f, s, webpage = io.open(filename)
  if f then webpage = f:read("*a"); io.close(f) end
  -- delete the file on disk, don't clutter
  os.remove(filename)

  -- parse our downloaded file for the player count
  local pc = webpage:match([[Total: (%d+) players online]])
  display("Achaea has "..tostring(pc).." players on right now.")
end

-- register our function to run on the event that something was downloaded
registerAnonymousEventHandler("sysDownloadDone", downloaded_file)

-- download a list of fake users for a demo
downloadFile(getMudletHomeDir().."/achaea-who-count.html", "https://www.achaea.com/game/who")

You should see a result like this:

DownloadFile demo.png

sysDownloadError

Raised when downloading a file failed - the second argument contains the error message, the third the local filename that was to be used and the actual URL used (might not be the same as what was given if redirection took place).

Example
--if a download fails notify the player.
function downloadErrorEventHandler(event, errorFound, localFilename, usedUrl)
    cecho("fuction downloadErrorEventHandler, "..errorFound)
    debugc("fuction downloadErrorEventHandler, "..errorFound) --display to error screen in editor
end --function downloadErrorEventHandler
registerAnonymousEventHandler("sysDownloadError", downloadErrorEventHandler)

sysDownloadFileProgress

Raised while file is being downloaded to indicate progess of download.

The arguments passed areː: event name, url of download, bytes downloaded, total bytes (if available).

Example
-- will show progress bar while download file and hide it after file download is complete
local progressBar = Geyser.Gauge:new({
  name="downloadProgressBar",
  x="25%", y="10%",
  width="50%", height="5%",
})
progressBar:setFontSize(13)
progressBar:setAlignment("center")
progressBar:hide()


local fileUrl = "https://www.mudlet.org/download/Mudlet-4.10.1-linux-x64.AppImage.tar"
local targetFile = getMudletHomeDir() .. "/Mudlet.tar"
function handleProgress(_, url, bytesDownloaded, totalBytes)
  if url ~= fileUrl then
    return
  end
  
  progressBar:show()
  
  if not totalBytes then
    bytesDownloaded = 0
    totalBytes = 1
  end
  
  progressBar:setValue(bytesDownloaded, totalBytes, math.floor((bytesDownloaded / totalBytes) * 100) .. "%")
end

registerAnonymousEventHandler("sysDownloadFileProgress", handleProgress)

function hideProgressBar()
  tempTimer(3, function() progressBar:hide() end)
end 

registerAnonymousEventHandler("sysDownloadDone", hideProgressBar)
registerAnonymousEventHandler("sysDownloadError", hideProgressBar)

downloadFile(targetFile, fileUrl)
Mudlet VersionAvailable in Mudlet4.11+

sysDropEvent

Raised when a file is dropped on the Mudlet main window or a userwindow. The arguments passed areː filepath, suffix, xpos, ypos, and consolename - "main" for the main window and otherwise of the userwindow/miniconsole name.

Mudlet VersionAvailable in Mudlet4.8+
function onDragAndDrop(_, filepath, suffix, xpos, ypos, consolename)
  print(string.format("Dropped new file into %s: %s (suffix: %s)", consolename, filepath, suffix))
end
registerAnonymousEventHandler("sysDropEvent", onDragAndDrop)

sysDropUrlEvent

Raised when a url is dropped on the Mudlet main window or a userwindow. As an url at the moment Mudlet understands The arguments passed areː url, schema, xpos, ypos, and consolename - "main" for the main window and otherwise of the userwindow/miniconsole name.

Mudlet VersionAvailable in Mudlet4.11+
function onDragAndDropUrl(_, url, schema, xpos, ypos, consolename)
  print(string.format("Dropped new url into %s: %s (suffix: %s)", consolename, filepath, suffix))
  if schema == "http" or schema == "https" then
    print("\nOh boy... this might be a link to some website")
  end
end
registerAnonymousEventHandler("sysDropUrlEvent", onDragAndDropUrl)

sysExitEvent

Raised when Mudlet is shutting down the profile - a good event to hook onto for saving all of your data.

sysGetHttpDone

Raised whenever a getHTTP() request completes successfully. Arguments with the event are the URL that the request was sent to and the response body (if it's less than 10k characters).

This event is also raised when a post/put/delete request redirects to a GET.

See also sysGetHttpError.

Mudlet VersionAvailable in Mudlet4.10+

sysGetHttpError

Raised whenever a getHTTP() request fails. Arguments are the error message and the URL that the request was sent to.

See also sysGetHttpDone.

Mudlet VersionAvailable in Mudlet4.10+

sysInstall

Raised right after a module or package is installed by any means. This can be used to display post-installation information or setup things.

Event handlers receive the name of the installed package or module as additional argument.

Note Note: Installed modules will raise this event handler each time the synced profile is opened. sysInstallModule and sysInstallPackage are not raised by opening profiles.

Mudlet VersionAvailable in Mudlet3.1+
function myScriptInstalled(_, name)
  -- stop if what got installed isn't my thing
  if name ~= "my script name here" then return end

  print("Hey, thanks for installing my thing!")
end
registerAnonymousEventHandler("sysInstallPackage", myScriptInstalled)

sysInstallModule

Raised right after a module is installed through the module dialog. This can be used to display post-installation information or setup things.

See also sysLuaInstallModule for when a module is installed via Lua.

Event handlers receive the name of the installed module as well as the file name as additional arguments.

Mudlet VersionAvailable in Mudlet3.1+

sysInstallPackage

Raised right after a package is installed by any means. This can be used to display post-installation information or setup things.

Event handlers receive the name of the installed package as well as the file name as additional arguments.

Mudlet VersionAvailable in Mudlet3.1+

sysIrcMessage

Raised when the IRC client receives an IRC message. The sender's name, channel name, and their message are passed as arguments to this event.

Starting with Mudlet 3.3, this event changes slightly to provide more information from IRC network messages. Data such as status codes, command responses, or error messages sent by the IRC Server may be formatted as plain text by the client and posted to lua via this event.

  • sender: may be the nick name of an IRC user or the name of the IRC server host, as retrievable by getIrcConnectedHost()
  • channel: may not always contain a channel name, but will be the name of the target/recipient of a message or action. In some networks the name may be that of a service (like "Auth" for example)
Example
function onIrcMessage(_, sender, target, message)
  echo(string.format('(%s) %s says, "%s"\n', target, sender, message))
end

registerAnonymousEventHandler("sysIrcMessage", onIrcMessage)

To send a message, see sendIrc().

sysLabelDeleted

Raised after a label is deleted, with the former label's name as an argument.


sysLoadEvent

Raised after Mudlet is done loading the profile, after all of the scripts, packages, and modules are installed. Note that when it does so, it also compiles and runs all scripts - which could be a good idea to initialize everything at once, but beware - scripts are also run when saved. Hence, hooking only on the sysLoadEvent would prevent multiple re-loads as you’re editing the script.

sysLuaInstallModule

Raised right after a module is installed through the Lua installModule() command. This can be used to display post-installation information or setup things.

Event handlers receive the name of the installed module as well as the file name as additional arguments.

Mudlet VersionAvailable in Mudlet3.1+

sysLuaUninstallModule

Raised right before a module is removed through the lua uninstallModule command. This can be used to display post-removal information or for cleanup.

Event handlers receive the name of the removed module as additional argument.

Mudlet VersionAvailable in Mudlet3.1+

sysManualLocationSetEvent

Raised whenever the "set location" command (on the 2D mapper context menu) is used to reposition the current "player room". It provides the room ID of the new room ID that the player has been moved to.

Mudlet VersionAvailable in Mudlet3.0+

sysMapAreaChanged

Raised whenever the area being viewed in the mapper changes, it returns two additional arguments being the areaID numbers being changed to and the previously viewed area.

Mudlet VersionAvailable in Mudlet4.17.0+

sysMapDownloadEvent

Raised whenever an MMP map is downloaded and loaded in.

sysMediaFinished

Raised when media finishes playing. This can be used in a music player for example to start the next song.

Event handlers receive the media file name and the file path as additional arguments.

Mudlet VersionAvailable in Mudlet4.15+

sysPathChanged

Raised whenever file or directory added through addFileWatch() is modified.

For directories this event is emitted when the directory at a specified path is modified (e.g., when a file is added or deleted) or removed from disk. Note that if there are several changes during a short period of time, some of the changes might not emit this signal. However, the last change in the sequence of changes will always generate this signal.

For files this event is emitted when the file at the specified path is modified, renamed or removed from disk.

Mudlet VersionAvailable in Mudlet4.12+
Example
herbs = {}
local herbsPath = getMudletHomeDir() .. "/herbs.lua"
function herbsChangedHandler(_, path)
  if path == herbsPath then
    table.load(herbsPath, herbs)
    removeFileWatch(herbsPath)
  end
end

addFileWatch(herbsPath)
registerAnonymousEventHandler("sysPathChanged", herbsChangedHandler)

sysPostHttpDone

Raised whenever a postHTTP() request completes successfully. Arguments with the event are the URL that the request was sent to and the response body (if it's less than 10k characters).

See also sysPostHttpError.

sysPostHttpError

Raised whenever a postHTTP() request fails. Arguments are the error message and the URL that the request was sent to.

See also sysPostHttpDone.

sysProfileFocusChangeEvent

Raised whenever a profile becomes or stops being the foreground one when multi-playing, whether multi-view (show all profiles side-by-side) is active or not. The event comes with a second boolean argument which is true if the profile is now the one that has the focus, i.e. will receive keystrokes entered from the keyboard, or false if the focus has just switched from it to another profile.
Mudlet VersionAvailable in Mudlet4.17.0+

sysProtocolDisabled

Raised whenever a communications protocol is disabled, with the protocol name passed as an argument. Current values Mudlet will use for this are: GMCP, MDSP, ATCP, MXP, and channel102.

sysProtocolEnabled

Raised whenever a communications protocol is enabled, with the protocol name passed as an argument. Current values Mudlet will use for this are: GMCP, MDSP, ATCP, MXP, and channel102.

function onProtocolEnabled(_, protocol)
  if protocol == "GMCP" then
    print("GMCP enabled! Now we can use GMCP data.")
  end
end

sysPutHttpDone

Raised whenever a putHTTP() request completes successfully. Arguments with the event are the URL that the request was sent to and the response body (if it's less than 10k characters).

See also sysPutHttpError.

sysPutHttpError

Raised whenever a putHTTP() request fails. Arguments are the error message and the URL that the request was sent to.

See also sysPutHttpDone.

sysSoundFinished

This event is obsolete in Mudlet 4.15. Please replace this with sysMediaFinished()

Mudlet VersionAvailable in Mudlet3.1+

sysSpeedwalkFinished

Raised when a speedwalk finishes, either one started by speedwalk() or the generic mapping script

sysSpeedwalkPaused

Raised when a speedwalk is paused, either via pauseSpeedwalk() or the generic mapping script.

sysSpeedwalkResumed

Raised when a speedwalk is resumed after pausing, whether it's the generic mapping script or the resumeSpeedwalk() function

sysSpeedwalkStarted

Raised when a speedwalk is started, either by the speedwalk() function or the generic mapping script

sysSpeedwalkStopped

Raised when a speedwalk is stopped prematurely by stopSpeedwalk() or the generic mapping script

sysSyncInstallModule

Raised right after a module is installed through the "sync" mechanism. This can be used to display post-installation information or setup things.

Event handlers receive the name of the installed module as well as the file name as additional arguments.

Mudlet VersionAvailable in Mudlet3.1+

sysSyncUninstallModule

Raised right before a module is removed through the "sync" mechanism. This can be used to display post-removal information or for cleanup.

Event handlers receive the name of the removed module as additional argument.

Mudlet VersionAvailable in Mudlet3.1+

sysTelnetEvent

Raised whenever an unsupported telnet option is encountered, allowing you to handle it yourself. The arguments that get passed with the event are type, telnet option, and the message.

sysUninstall

Raised right before a module or package is uninstalled by any means. This can be used to display post-removal information or for cleanup.

Event handlers receive the name of the removed package or module as additional argument.

Mudlet VersionAvailable in Mudlet3.1+

sysUninstallModule

Raised right before a module is removed through the module dialog. This can be used to display post-removal information or for cleanup.

Event handlers receive the name of the removed module as additional argument.

Mudlet VersionAvailable in Mudlet3.1+

sysUninstallPackage

Raised right before a package is removed by any means. This can be used to display post-removal information or for cleanup.

Event handlers receive the name of the removed package as additional argument.

Mudlet VersionAvailable in Mudlet3.1+

sysUnzipDone

Raised when a zip file is successfully unzipped using unzipAsync()

Event handlers receive the zip file name and the unzip path as additional arguments.

Mudlet VersionAvailable in Mudlet4.6+

sysUnzipError

Raised when a zip file fails to unzip using unzipAsync()

Event handlers receive the zip file name and the unzip path as additional arguments.

Mudlet VersionAvailable in Mudlet4.6+

sysUserWindowResizeEvent

Raised when a userwindow is resized, with the new height and width coordinates and the windowname passed to the event. A common usecase for this event is to move/resize your UI elements according to the new dimensions.

See alsoː sysWindowResizeEvent

Example

This sample code will echo whenever a resize happened with the new dimensions:

function resizeEvent( event, x, y, windowname )
  echo("RESIZE EVENT: event="..event.." x="..x.." y="..y.." windowname="..windowname.."\n")
end

sysWindowMousePressEvent

Raised when a mouse button is pressed down anywhere on the main window (note that a click is composed of a mouse press and mouse release). The button number, the x,y coordinates of the click and the windowname are reported.

Note Note: Windowname reported in Mudlet 4.9+

See alsoː sysWindowMouseReleaseEvent

Example

function onClickHandler( event, button, x, y, windowname )
  echo("CLICK event:"..event.." button="..button.." x="..x.." y="..y.." windowname="..windowname.."\n")
end

sysWindowMouseReleaseEvent

Raised when a mouse button is released after being pressed down anywhere on the main window (note that a click is composed of a mouse press and mouse release).

See sysWindowMousePressEvent for example use.

sysWindowResizeEvent

Raised when the main window is resized, or when one of the borders is resized, with the new height and width coordinates passed to the event. A common usecase for this event is to move/resize your UI elements according to the new dimensions.

See alsoː sysUserWindowResizeEvent

Example

This sample code will echo whenever a resize happened with the new dimensions:

function resizeEvent( event, x, y )
  echo("RESIZE EVENT: event="..event.." x="..x.." y="..y.."\n")
end

ttsPitchChanged

Raised when text-to-speech function ttsSetPitch(...) has been called.

See also: Manual:Text-to-Speech

ttsRateChanged

Raised when text-to-speech function ttsSetRate(...) has been called.

See also: Manual:Text-to-Speech

ttsSpeechError

Raised when a text-to-speech function encountered an error while changing states (eg. from stopped to playing to pausing). Usually indicated when the operating system TTS engine is not working correctly.

See also: Manual:Text-to-Speech

ttsSpeechPaused

Raised when text-to-speech function ttsPause(...) has been called.

See also: Manual:Text-to-Speech

ttsSpeechQueued

Raised when text-to-speech function ttsQueue(...) has been called.

See also: Manual:Text-to-Speech

ttsSpeechReady

Raised when the text-to-speech engine is ready to beginning processing text.

See also: Manual:Text-to-Speech

ttsSpeechStarted

Raised when text-to-speech functions ttsSpeak(...) or ttsResume(...) have been called.

See also: Manual:Text-to-Speech

ttsVoiceChanged

Raised when text-to-speech functions ttsSetVoiceByIndex(...) or ttsSetVoiceByName(...) have been called.

See also: Manual:Text-to-Speech

ttsVolumeChanged

Raised when text-to-speech function ttsSetVolume(...) has been called.

See also: Manual:Text-to-Speech

Scripting howtos

How to convert a string to value?

Say you'd like to capture a number from a trigger, but the capture always ends up being a "string" (or, just text on which you can't do any maths on) even if it's a number. To convert it to a number, you'd want to use the tonumber() function:

<lua> myage = tonumber(matches[2]) </lua>

How to highlight my current target?

You can put the following script into your targetting alias:

<lua>if id then killTrigger(id) end id = tempTrigger(target, selectString(" .. target .. ", 1) fg("gold") resetFormat())</lua>

Where target is your target variable. Note that you have to use the full name, capitalized. If you’d like the script to auto-capitalize for you, you can use this version:

<lua> target = target:title() if id then killTrigger(id) end id = tempTrigger(target, selectString(" .. target .. ", 1) fg("gold") resetFormat()) </lua>

How to format an echo to a miniConsole?

One of the ways you can create multi-line displays in a miniConsole with variable information is with string.format and the use of [[ ]] brackets for text, which allow for multiple line in text:

<lua> local WindowWidth, WindowHeight = getMainWindowSize(); createMiniConsole("sys",WindowWidth-650,0,650,300)

local name, age, sex = "Bob", 34, "male"

cecho("sys", string.format([[

 /---------\
     %s
  %dyrs
  sex - %s 
 \---------/

]], name, age, sex)) </lua>

How to play a sound when I receive communication while afk?

Play sound when afk trigger.png

For this to work, place the line you'd like to trigger on in the first pattern box and select the appropriate pattern type. Then add return not hasFocus() with the Lua function as the second pattern, enable the AND trigger, and set the line delta to zero. Then just enable play sound, choose your sound and you're set!

Advanced scripting tips

Do stuff after all triggers/aliases/scripts are run

This little snippet will have your commands be executed right after, depending on the context you run it in, all triggers, aliases or scripts are completed:

<lua>tempTimer(0, mycode)</lua>

How to delete the previous and current lines

This little snippet comes from Iocun:

<lua> moveCursor(0,getLineCount()-1) deleteLine() moveCursor(0,getLineCount()) deleteLine() </lua>

ATCP

Since version 1.0.6, Mudlet includes support for ATCP. This is primarily available on IRE-based MUDs, but Mudlets impelementation is generic enough such that any it should work on others.

The latest ATCP data is stored in the atcp table. Whenever new data arrives, the previous is overwritten. An event is also raised for each ATCP message that arrives. To find out the available messages available in the atcp table and the event names, you can use display(atcp).

Note that while the typical message comes in the format of Module.Submodule, ie Char.Vitals or Room.Exits, in Mudlet the dot is removed - so it becomes CharVitals and RoomExits. Here’s an example:

room_number = tonumber(atcp.RoomNum) echo(room_number)

Triggering on ATCP events

If you’d like to trigger on ATCP messages, then you need to create scripts to attach handlers to the ATCP messages. The ATCP handler names follow the same format as the atcp table - RoomNum, RoomExits, CharVitals and so on.

While the concept of handlers for events is to be explained elsewhere in the manual, the quick rundown is this - place the event name you’d like your script to listen to into the Add User Defined Event Handler: field and press the + button to register it. Next, because scripts in Mudlet can have multiple functions, you need to tell Mudlet which function should it call for you when your handler receives a message. You do that by setting the Script name: to the function name in the script you’d like to be called.

For example, if you’d like to listen to the RoomExits event and have it call the process_exits() function - register RoomExits as the event handler, make the script name be process_exits, and use this in the script:

<lua> function process_exits(event, args)

   echo("Called event: " .. event .. "\nWith args: " .. args)

end </lua>

Feel free to experiment with this to achieve the desired results. A ATCP demo package is also available on the forums for using event handlers and parsing its messages into Lua datastructures.

Mudlet-specific ATCP

See here for ATCP extensions that have been added to Mudlet.

Aardwolf’s 102 subchannel

Similar to ATCP, Aardwolf includes a hidden channel of information that you can access in Mudlet since 1.1.1. Mudlet deals with it in the same way as with ATCP, so for full usage instructions see the ATCP section. All data is stored in the channel102 table, such you can do

display(channel102)

To see all the latest information that has been received. The event to create handlers on is titled channel102Message, and you can use the sendTelnetChannel102(msg) function to send text via the 102 channel back to Aardwolf.

db: Mudlet's database frontend

The DB package is meant to provide easier access to a database, so you don’t have to know SQL or use the luasql module to set and get at your data. However, it does require that the luasql module be compiled and included in Mudlet to function - and this all is available since Mudlet 1.0.6.

Creating a Database

Before you can store anything in a database, you need to create one. You may have as many independent databases as you wish, with each having as many unique tables-- what we will call sheets in this package, so as to avoid confusion with Lua tables - think spreadsheets.

To create a database, you use the db:create() function, passing it the name of your database and a Lua table containing its schema configuration. A schema is a mold for your database - it defines what goes where. Using the spreadsheet example, ths would mean that you’d define what goes into each column. A simple example:

<lua> db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes"}}) </lua>

This will create a database which contains two sheets: one named friends, the other named enemies. Each has three columns, name, city and notes-- and the datatype of each are strings, though the types are very flexible and can be changed basically whenever you would like. It’ll be stored in a file named Database_people.db in your Mudlet config directory on the hard drive should you want to share it.

It’s okay to run this function repeatedly, or to place it at the top-level of a script so that it gets run each time the script is saved: the DB package will not wipe out or clear the existing data in this case. More importantly, this allows you to add columns to an existing sheet. If you change that line to:

<lua> db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes", "enemied"}}) </lua>

It will notice that there is a new column on enemies, and add it to the existing sheet-- though the value will end up as nil for any rows which are already present. Similarly, you can add whole new sheets this way. It is not presently possible to -remove- columns or sheets without deleting the database and starting over.

A note on column or field names: you may not create a field which begins with an underscore. This is strictly reserved to the db package for special use.

Adding Data

To add data to your database, you must first obtain a reference (variable) for it. You do that with the db:get_database function, such as:

<lua>

 local mydb = db:get_database("people")

</lua>

The database object contains certain convenience functions (discussed later, but all are preceded with an underscore), but also a reference to every sheet that currently exists within the database. You then use the db:add() function to add data to the specified sheet.

<lua>

 db:add(mydb.friends, {name="Ixokai", city="Magnagora"})

</lua>

If you would like to add multiple rows at once to the same table, you can do that by just passing in multiple tables:

<lua>

 db:add(mydb.friends,
   {name="Ixokai", city="Magnagora"},
   {name="Vadi", city="New Celest"},
   {name="Heiko", city="Hallifax", notes="The Boss"}
 )

</lua>

Notice that by default, all columns of every table are considered optional-- if you don’t include it in the add, then it will be set to its default value (which is nil by default)

For those familiar with databases: with the DB package, you don’t have to worry about committing or rolling back any changes, it will commit after each action automatically. If you would like more control then this, see Transactions below.

You also cannot control what is the primary key of any sheets managed with DB, nor do you have to create one. Each row will get a unique integer ID that automatically increments, and this field can be accessed as "_row_id".

Querying

Putting data in isn’t any fun if you can’t get it out. If you want every row from the sheet, you can do:

<lua>

 db:fetch(mydb.friends)

</lua>

But rarely is that actually useful; usually you want to get only select data. For example, you only want to get people from the city of Magnagora. To do that you need to specify what criteria the system should use to determine what to return to you. It looks like this:

<lua>

 db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"))

</lua>

So the basic command is - db:fetch(_sheet_, _what to filter by_)

The following filter operations are defined:

<lua>

 db:eq(field, value[, case_insensitive]) -- Defaults to case insensitive, pass true as the last arg to
             reverse this behavior.
 db:not_eq(field, value[, case_insensitive) -- Not Equal To
 db:lt(field, value) -- Less Than
 db:lte(field, value) -- Less Than or  Equal to.
 db:gt(field, value) -- Greater Than
 db:gte(field, value) -- Greater Than or Equal To
 db:is_nil(field) -- If the column is nil
 db:is_not_nil(field) -- If the column is not nil
 db:like(field, pattern) -- A simple matching pattern. An underscore matches any single character,
         and a percent(%) matches zero or more characters. Case insensitive.
 db:not_like(field, pattern) -- As above, except it'll give you everything but what you ask for.
 db:between(field, lower_bound, upper_bound) -- Tests if the field is between the given bounds (numbers only).
 db:not_between(field, lower_bound, upper_bound) -- As above, only... not.
 db:in_(field, table) -- Tests if the field is in the values of the table. NOTE the trailing underscore!
 db:not_in(field, table) -- Tests if the field is NOT in the values of the table *

</lua>

The db:in_ operator takes a little more explanation. Given a table, it tests if any of the values in the table are in the sheet. For example:

<lua>

 db:in_(mydb.friends.city, {"Magnagora", "New Celest"})

</lua>

It tests if city == "Magnagora" OR city == "New Celest", but with a more concise syntax for longer lists of items.

There are also two logical operators:

<lua>

 db:AND(operation1, ..., operationN)
 db:OR(operation1, operation2)

</lua>

You may pass multiple operations to db:fetch in a table array, and they will be joined together with an AND by default. For example:

<lua>

 db:fetch(mydb.friends,
   {db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "X%")}
 )

</lua>

This will return every record in the sheet which is in the city of Magnagora, and has a name that starts with an X. Again note that in LIKE patterns, a percent is zero or more characters — this is the same effect as "X.*" in pcre patterns. Similarly, an underscore matches any single characters and so is the same as a dot in pcre.

Passing multiple expressions in an array to db:fetch is just a convenience, as its exactly the same as:

<lua>

 db:fetch(mydb.friends, db:AND(db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "I%")))

</lua>

The db:OR operation only takes two arguments, and will check to see if either of the two is true. You can nest these logical operators as deeply as you need to.

You can also just pass in a string directly to db:fetch, but you have to be very careful as this will be passed straight to the SQL layer. If you don’t know any SQL then you want to avoid this… for example, in SQL there’s a very big difference between double and single quotes. If you don’t know that, then stick to the db functions. But an example is:

<lua>

 db:fetch(mydb.friends, "city == 'Magnagora'")

</lua>

Now, the return value of db:fetch() is always a table array that contains a table dictionary representing the full contents of all matching rows in the sheet. These are standard Lua tables, and you can perform all normal Lua operations on them. For example, to find out how many total items are contained in your results, you can simply do #results. If a request from the friends sheet were to return one row that you stored in the results variable, it would look like this if passed into the display() function:

<lua>

 table {
 1: table {
   'name': 'Bob',
   'city': 'Magnagora',
   'notes': 'Trademaster of Tailoring'
 }

} </lua>

And if you were to echo(#results), it would show 1.

The order of the returned rows from db:fetch is generally the same as the order in which you entered them into the database, but no actual guarantees are made to this. If you care about the order then you can pass one or two optional parameters after the query to db:fetch() to control this.

The first table array of fields that indicate the column names to sort by; the second is a flag to switch from the default ascending(smallest to largest) sort, to a descending(largest to smallest) sort. For example:

<lua>

 db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city})

</lua>

This will return all your friends in Magnagora, sorted by their name, from smallest to largest. To reverse this, you would simply do:

<lua>

 db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city}, true)

</lua>

Including more then one field in the array will indicate that in the case that two rows have the same value, the second field should be used to sort them.

If you would like to return ALL rows from a sheet, but still sort them, you can do that by passing nil into the query portion. For example:

<lua>

 db:fetch(mydb.friends, nil, {mydb.friends.city, mydb.friends.name})

</lua>

This will return every friend you have, sorted first by city and then their name.

Indexes and Types

The sheets we’ve defined thus far are very simple, but you can take more control over the process if you need to. For example, you may assign default values and types to columns, and the DB package will attempt to coerce them as appropriate. To do that, you change your db:create() call as:

<lua> db:create("people", {

 friends={"name", "city", "notes"},
 enemies={
   name="",
   city="",
   notes="",
   enemied="",
   kills=0
 }

}) </lua>

This is almost the same as the original definition, but we’ve defined that our first four fields are strings with a default value of blank, and the new kills field which is an integer that starts off at 0. The only way to set a datatype is to set a default value at this time.

Please note, beneath the DB package is SQLite, and SQLite is very data-type neutral. It doesn’t really care very much if you break those rules and put an integer in a string field or vice-versa, but the DB package will — to a limited degree — attempt to convert as appropriate, especially for the operations that work on numbers.

You may also create both standard and unique indexes. A unique index enforces that a certain criteria can only happen once in the sheet. Now, before you go and make indexes, pause and consider. There is no right or wrong answer when it comes to what to index: it depends on what kind of queries you do regularly. If in doubt, don’t make an index. Indexes will speed up reading data from the sheet, but they will slow down writing data.

To add an index, pass either the _index or _unique keys in the table definition. An example:

<lua> db:create("people", {

 friends={"name", "city", "notes"},
 enemies={
   name="",
   city="",
   notes="",
   enemied="",
   kills=0,
   _index = { "city" },
   _unique = { "name" }
 }

}) </lua>

You can also create compound indexes, which is a very advanced thing to do. One could be: _unique = { {"name", "city"} }

This would produce an effect that there could be only one "Bob" in Magnagora, but he and "Bob" in Celest could coexist happily.

Now, bear in mind: _index = { "name", "city"} creates two indexes in this sheet. One on the city field, one on the name field. But, _index = { {"name", "city"} } creates one index: on the combination of the two. Compound indexes help speed up queries which frequently scan two fields together, but don’t help if you scan one or the other.

The admonition against making indexes willy-nilly holds double for compound indexes: do it only if you really need to!

Uniqueness

As was specified, the _unique key can be used to create a unique index. This will make it so a table can only have one record which fulfills that index. If you use an index such as _unique = { "name" } then names must be unique in the sheet. However, if you use an index such as _unique = { {"name", "city"} } then you will allow more then one person to have the same name — but only one per city.

Now, if you use db:add() to insert a record which would violate the unique constraint, a hard error will be thrown which will stop your script. Sometimes that level of protection is too much, and in that case you can specify how the db layer handles violations.

There are three possible ways in which the layer can handle such violations; the default is to FAIL and error out.

You can also specify that the db layer should IGNORE any commands that would cause the unique constraint to be violated, or the new data should REPLACE the existing row.

For example:

<lua> db:add(mydb.enemies, {name="Bob", city="Sacramento"}) db:add(mydb.enemies, {name="Bob", city="San Francisco"}) </lua>

With the name field being declared to be unique, these two commands can’t succeed normally. The first db:add() will create a record with the name of Bob, and the second would cause the uniqueness of the name field to be violated. With the default behavior (FAIL), the second db:add() call will raise an error and halt the script.

If you want the IGNORE behavior, the second command will not cause any errors and it will simply fail silently. Bob’s city will still be Sacramento.

With the REPLACE behavior, the second command will cause its data to completely overwrite and replace the first record. Bob’s city will now be San Francisco.

A word of caution with REPLACE, given:

<lua> db:add(mydb.enemies, {name="Bob", city="Sacramento", notes="This is something."}) db:add(mydb.enemies, {name="Bob", city="San Francisco"}) </lua>

With the REPLACE behavior, the second record will overwrite the first-- but the second record does not have the notes field set. So Bob will now not have any notes. It doesn’t -just- replace existing fields with new ones, it replaces the entire record.

To specify which behavior the db layer should use, add a _violations key to the table definition:

<lua> db:create("people", {

 friends={"name", "city", "notes"},
 enemies={
   name="",
   city="",
   notes="",
   enemied="",
   kills=0,
   _index = { "city" },
   _unique = { "name" },
   _violations = "IGNORE"
 }

}) </lua>

Note that the _violations behavior is sheet-specific.

Timestamps

In addition to strings and floats, the db module also has basic support for timestamps. In the database itself this is recorded as an integer (seconds since 1970) called an epoch, but you can easily convert them to strings for display, or even time tables to use with Lua’s built-in time support.

The most common use for the Timestamp type is where you want the database to automatically record the current time whenever you insert a new row into a sheet. The following example illustrates that:

<lua> local mydb = db:create("combat_log",

 {
   kills = {
     name = "",
     area = "",
     killed = db:Timestamp("CURRENT_TIMESTAMP"),
     _index = { {"name", "killed"} }
   }
 }

)


db:add(mydb.kills, {name="Drow", area="Undervault"})

results = db:fetch(mydb.kills) display(results) </lua>

The result of that final display would show you this on a newly created sheet:

<lua> table {

 1: table {
   '_row_id': 1
   'area': 'Undervault'
   'name': 'Drow'
   'killed': table {
     '_timestamp': 1264317670
   }
 }

} </lua>

As you can see from this output, the killed fields contains a timestamp-- and that timestamp is stored as an epoch value. For your convenience, the db.Timestamp type offers three functions to get the value of the timestamp in easy formats. They are as_string, as_number and as_table, and are called on the timestamp value itself.

The as_number function returns the epoch number, and the as_table function returns a time table. The as_string function returns a string representation of the timestamp, with a default format of "%m-%d-%Y %H:%M:%S". You can override this format to anything you would like. Details of what you can do with epoch values, time tables, and what format codes you can use are specified in the Lua manual at: http://www.lua.org/pil/22.1.html for the Lua date/time functions.

A quick example of the usage of these functions is:

<lua> results = db:fetch(mydb.kills) for _, row in ipairs(results) do

 echo("You killed " .. row.name .. " at: " .. row.killed:as_string() .."\n")

end </lua>

Deleting

The db:delete function is used to delete rows from the sheet. It takes two arguments, the first being the sheet you are deleting and the second a query string built using the same functions used to build db:fetch() queries.

For example, to delete all your enemies in the city of Magnagora, you would do:

<lua> db:delete(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora")) </lua>

Be careful in writing these! You may inadvertantly wipe out huge chunks of your sheets if you don’t have the query parameters set just to what you need them to be. Its advised that you first run a db:fetch() with those parameters to test out the results they return.

As a convenience, you may also pass in a result table that was previously retrieved via db:fetch and it will delete only that record from the table. For example, the following will get all of the enemies in Magnagora, and then delete the first one:

<lua> results = db:fetch(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora")) db:delete(mydb.enemies, db:eq(mydb.enemies._row_id, results[1]._row_id)) </lua>

That is equivalent to:

<lua> db:delete(mydb.enemies, results[1]) </lua>

You can even pass a number directly to db:delete if you know what _row_id you want to purge.

A final note of caution: if you want to delete all the records in a sheet, you can do so by only passing in the table reference. To try to protect you from doing this inadvertently, you must also pass true as the query after:

<lua> db:delete(mydb.enemies, true) </lua>

Updating

If you make a change to a table that you have received via db:fetch(), you can save those changes back to the database by doing:

<lua> db:update(mydb.enemies, results[1]) </lua>

A more powerful (and somewhat dangerous, be careful!) function to make changes to the database is db:set, which is capable of making sweeping changes to a column in every row of a sheet. Beware, if you have previously obtained a table from db:fetch, that table will NOT represent this change.

The db:set() function takes two arguments: the field to change, the value to set it to, and the db:fetch() like query to indicate which rows should be affected. If you pass true as the last argument, ALL rows will be changed.

To clear out the notes of all of our friends in Magnagora, we could do:

<lua> db:set(mydb.friends.notes, "", db:eq(mydb.friends.notes, "Magnagora")) </lua>

Be careful in writing these!

Transactions

As was specified earlier, by default the db module commits everything immediately whenever you make a change. For power-users, if you would like to control transactions yourself, the following functions are provided on your database instance:

<lua> local mydb = db:get_database("my_database") mydb._begin() mydb._commit() mydb._rollback() mydb._end() </lua>

Once you issue a mydb._begin() command, autocommit mode will be turned off and stay off until you do a mydb._end(). Thus, if you want to always use transactions explicitly, just put a mydb._begin() right after your db:create() and that database will always be in manual commit mode.

Viewing and editing the database contents

A good tool to view and edit the database contents in raw form is SQlite Sorcerer (free and available for Linux, Windows, Mac).

Debugging raw queries

If you'd like to see the queries that db: is running for you, you can enable debug mode with db.debug_sql = true.

GUI Scripting in Mudlet

Mudlet has extremely powerful GUI capabilities built into it, and with the inclusion of the Geyser layout manager in Mudlet's Lua API it is quicker and easier to get a basic UI created.

Useful Resources

There are several useful resources you can refer to when creating your GUI.

Wiki Resources
Excellent for getting an initial feel of how to use the Geyser layout manager.
The GUI section of the Lua API manual
External Resources
The Geyser Layout Manager subforum on Mudlet's forums.
A pinned thread on Mudlet's forums for showing what your GUI looks like. Useful for ideas and to see what is possible.
A forum thread which follows the evolution of the UI provided by God Wars II