Difference between revisions of "Manual:Scripting"

From Mudlet
Jump to navigation Jump to search
 
(110 intermediate revisions by 14 users not shown)
Line 1: Line 1:
==Scripting with Mudlet==
+
{{TOC right}}
 +
=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 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>
+
<syntaxhighlight lang="lua">
 
  a = "Tom"
 
  a = "Tom"
 
  matches[2] = "Betty"
 
  matches[2] = "Betty"
 
  b = matches[2]
 
  b = matches[2]
 
  c = a .. b and e will equal "TomBetty"
 
  c = a .. b and e will equal "TomBetty"
</lua>
+
</syntaxhighlight>
  
 
To output a table you can use a convenience function - ''display(mytable)'', which is built into Mudlet.
 
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==
+
==Lua interface functions - triggers, timers etc.==
  
 
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.
 
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.
Line 24: Line 25:
 
<code>You have (\d+) weapons and they are (?:(\b\w+\W+)+)</code>
 
<code>You have (\d+) weapons and they are (?:(\b\w+\W+)+)</code>
  
<lua>
+
<syntaxhighlight lang="lua">
 
  number_of_weapons = matches[2]
 
  number_of_weapons = matches[2]
 
  status_of_weapons = matches[3]
 
  status_of_weapons = matches[3]
Line 39: Line 40:
  
 
  selectCaptureGroup( 3 )
 
  selectCaptureGroup( 3 )
  setFgColor( 0,0,255 )</lua>
+
  setFgColor( 0,0,255 )</syntaxhighlight>
  
 
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).
 
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).
Line 49: Line 50:
 
To come back to our question how to select all occurrences of "Tom" and highlight them:
 
To come back to our question how to select all occurrences of "Tom" and highlight them:
  
<lua>
+
<syntaxhighlight lang="lua">
 
   function setBgColorAll( word, r, g, b )
 
   function setBgColorAll( word, r, g, b )
 
     i = 0
 
     i = 0
Line 62: Line 63:
 
     end
 
     end
 
   end
 
   end
</lua>
+
</syntaxhighlight>
  
 
Then you simply define a substring matching trigger on the word "Tom" and in the trigger script you call above function:
 
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>
+
<syntaxhighlight lang="lua">setBgColorAll("Tom", 255,50,50)</syntaxhighlight>
  
==Sending commands to the MUD or printing information messages on the screen==
+
Since Mudlet 4.11+ it is possible to use name capture groups. You can verify if feature availability by checking <code>mudlet.supports.namedPatterns</code> flag.
 +
Let's say you have multiple patterns that you want to handle with one code.
  
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.
+
<code>^My name is (?<name>\w+)\. My class is (?<class>\w+)\.</code>
 +
 
 +
<code>^I am (?<class\w+)\ and my name is (?<name>\w+)\.</code>
 +
 
 +
Notice that class and name come in different order, therefore in first case we will have name under <code>matches[2]</code> and in second case under <code>matches[3]</code>.
 +
With named capture groups in both cases we will have name available under <code>matches.name</code> and class under <code>matches.class</code>.
 +
 
 +
Code example to handle named matches above:
 +
<syntaxhighlight lang="lua">
 +
  echo("Name: " .. matches.name .. "\n")
 +
  echo("Class: " .. matches.class .. "\n")
 +
</syntaxhighlight>
 +
 
 +
==Sending commands or printing information messages==
 +
 
 +
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:
 
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:
  
<code>
+
<code> say (\w+).*(\w*).*(.*) </code>
say (\w+).*(\w*).*(.*)
 
</code>
 
  
 
script:
 
script:
  
<lua>
+
<syntaxhighlight lang="lua">
 
send( "say " .. matches[4] .." " .. matches[2] .." ".. matches[3] )
 
send( "say " .. matches[4] .." " .. matches[2] .." ".. matches[3] )
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.)===
+
===Changing and formatting text from the MUD===
  
 
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".
 
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".
Line 92: Line 107:
 
By manipulating the value, the command can easily be changed before it is being sent to the MUD.
 
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.
+
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.
 
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.
Line 98: Line 113:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
selectString("Tom",1)
 
selectString("Tom",1)
 
replace("Betty")
 
replace("Betty")
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 107: Line 122:
 
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().
 
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>
+
<syntaxhighlight lang="lua">
 
selectString( "Tom", 2 )
 
selectString( "Tom", 2 )
 
replace( "Betty" )
 
replace( "Betty" )
</lua>
+
</syntaxhighlight>
  
 
This code would change the second "Tom" and leave the first "Tom" alone. The function call
 
This code would change the second "Tom" and leave the first "Tom" alone. The function call
  
<lua>replaceAll( "Betty" )</lua>
+
<syntaxhighlight lang="lua">replaceAll( "Betty" )</syntaxhighlight>
  
 
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.
 
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.
Line 122: Line 137:
 
You add a new trigger and define the regex: ugly monster In the script you write:
 
You add a new trigger and define the regex: ugly monster In the script you write:
  
<lua>
+
<syntaxhighlight lang="lua">
 
selectString("ugly monster", 1 )
 
selectString("ugly monster", 1 )
 
setFgColor(255,0,0)
 
setFgColor(255,0,0)
 
setBgColor(255,255,255)
 
setBgColor(255,255,255)
 
resetFormat()
 
resetFormat()
</lua>
+
</syntaxhighlight>
  
 
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.
 
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>
+
<syntaxhighlight lang="lua">selectSection( 28, 3 )</syntaxhighlight>
  
 
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.
 
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>
+
<syntaxhighlight lang="lua">selectCaptureGroup( number )</syntaxhighlight>
  
 
This function selects the captureGroup number if you use Perl regular expressions containing capture groups. The first capture group starts with index 1.
 
This function selects the captureGroup number if you use Perl regular expressions containing capture groups. The first capture group starts with index 1.
Line 141: Line 156:
 
==Deleting Text - Gagging==
 
==Deleting Text - Gagging==
  
<lua>deleteLine()</lua>
+
<syntaxhighlight lang="lua">deleteLine()</syntaxhighlight>
  
 
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:
 
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:
Line 147: Line 162:
 
If you get this line form the MUD: "Mary and Tom walk to the diner."
 
If you get this line form the MUD: "Mary and Tom walk to the diner."
  
<lua>selectString( "Tom", 1 )
+
<syntaxhighlight lang="lua">selectString( "Tom", 1 )
replace( "" )</lua>
+
replace( "" )</syntaxhighlight>
  
 
Then the output will be changed to: "Mary and walk to the diner."
 
Then the output will be changed to: "Mary and walk to the diner."
  
 
==Cursor Movement and Cursor Placement==
 
==Cursor Movement and Cursor Placement==
 +
Mudlet allows you to insert text arbitrarily within the buffer - in the previous line, in the previous ten lines, or even the first line that you've ever saw. These are the tools to help you with the job:
  
<code>moveCursor( windowName, x, y )</code> This will move the user cursor of window windowName to the absolute (x/y) coordinates in the text.
+
<code>[[Manual:UI_Functions#moveCursor|moveCursor( windowName, x, y )]]</code> This will move the user cursor of window windowName to the absolute (x/y) coordinates in the text.
  
<code>moveCursor( "main", 20, 3950 )</code> 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.
+
<code>[[Manual:UI_Functions#moveCursor|moveCursor( "main", 20, 3950 )]]</code> 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 [[Manual:UI_Functions#getLastLineNumber|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.
  
<code>number_of_last_line_in_text = getLineCount()</code> 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.
+
<code>number_of_last_line_in_text = [[Manual:UI_Functions#getLineCount|getLineCount()]]</code> 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.
  
<code>line_number_of_the_current_cursor_position = getLineNumber()</code>
+
<code>line_number_of_the_current_cursor_position = [[Manual:UI_Functions#getLineNumber|getLineNumber()]]</code>
  
<code>column_number_of_the_current_cursor_position = getColumnNumber()</code>
+
<code>column_number_of_the_current_cursor_position = [[Manual:UI_Functions#getColumnNumber|getColumnNumber()]]</code>
  
<code>luaTable_containing_textlines = getLines( absolute_line_number_from, absolute_line_number_to )</code> 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:
+
<code>luaTable_containing_textlines = [[Manual:UI_Functions#getLines|getLines( absolute_line_number_from, absolute_line_number_to )]]</code> this will return a Lua table containing all lines between the absolute positions from and to.
  
<code>moveCursor() return true or false depending on whether the move was possible.</code>
+
{{note}} This function uses absolute line numbers, not relative ones like in [[Manual:UI_Functions#moveCursor|moveCursor()]].
  
==User defined dockable windows==
+
<code>[[Manual:UI_Functions#moveCursor|moveCursor()]]</code> returns true or false depending on whether the move was possible and done.
  
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:
+
=== Inserting text in the middle of the line ===
 +
Here's an example where we'll append the text ''(one)'' right after the word ''exit'' in the line of <code>You see a single exit leading west.</code>
  
<code>openUserWindow( string window_name )</code>
+
[[File:Inserting text.png|center|650px]]
  
<code>echoUserWindow( string window_name, string text )</code>
+
The pattern we used was an ''exact match'' for <code>You see a single exit leading west.</code> and the code is:
  
<code>setWindowSize( int x, int y )</code>
+
<syntaxhighlight lang="lua">
 +
if moveCursor(string.find(line, "exit")+#("exit")-1, getLineNumber()) then
 +
  insertText("(one)")
 +
end
 +
</syntaxhighlight>
  
<code>clearWindow( string window_name )</code>
+
What happened here:
 +
* we triggered on the desired line using our pattern above
 +
* we used [[Manual:UI_Functions#moveCursor|moveCursor()]] to place the cursor in the desired position within the line. [[Manual:UI_Functions#moveCursor|moveCursor()]] takes both the line number and the position within the line to place our virtual cursor on, so
 +
** we used [http://www.lua.org/manual/5.1/manual.html#pdf-string.find string.find()] to find the position of the word "exit" within the line
 +
** but [http://www.lua.org/manual/5.1/manual.html#pdf-string.find string.find()] returns us the position of the first character of the word we want, while we want to have the text be right after it - so we find out how long our word is with <code>#("exit")</code>, add that to the number returned by [http://www.lua.org/manual/5.1/manual.html#pdf-string.find string.find()] which gets us the first character + the length of the word - which isn't quite at the end of the word, but the word plus the space (because of [http://www.lua.org/manual/5.1/manual.html#pdf-string.find string.find()] and the first character) - so we substract 1 to move our cursor right at the end of the word
 +
** we then use [[Manual:UI_Functions#getLineNumber|getLineNumber()]] to get the current line number, so our cursor is placed on the current line, at our desired position
 +
* lastly we finally insert the text with [[Manual:UI_Functions#insertText|insertText()]], which puts the text exactly where our virtual cursor is
  
==Dynamic Timers==
+
Done! You can also use [[Manual:UI_Functions#cinsertText|cinsertText()]] for coloured inserts, and other functions [[Manual:Scripting#Cursor_Movement_and_Cursor_Placement|mentioned above]] and in the [[Manual:UI_Functions|API manual]] to help you do what you'd like.
  
<code>
+
==User defined dockable windows==
tempTimer( double timeout, string/function lua code_to_execute, string/float/int timer_name )
 
</code>
 
  
<code>
+
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. Check out [[Manual:Geyser#Geyser.UserWindow|Geyser.UserWindow]] on how to add them!
disableTimer( name )
 
</code>
 
 
 
<code>
 
enableTimer( name )
 
</code>
 
  
 
==Dynamic Triggers==
 
==Dynamic Triggers==
Line 202: Line 221:
 
<code>triggerID = tempBeginOfLineTrigger( begin of line pattern, code ) creates a fast begin of line substring trigger</code>
 
<code>triggerID = tempBeginOfLineTrigger( begin of line pattern, code ) creates a fast begin of line substring trigger</code>
  
=Handling Tables in Lua=
+
=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:
  
Nick Gammon has written a very nice overview on how to deal with Lua tables. You can find it here: http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=6036.
+
<syntaxhighlight lang="lua">
How to use multilinematches[n][m]
+
myage = tonumber(matches[2])
 +
</syntaxhighlight>
  
(The following example can be tested on the MUD batmud.bat.org)
+
==How to highlight my current target?==
  
In the case of a multiline trigger with these 2 Perl regex as conditions:
+
You can put the following script into your targetting alias:
  
<code>^You have (\w+) (\w+) (\w+) (\w+)</code>
+
<syntaxhighlight lang="lua">if id then killTrigger(id) end
 +
id = tempTrigger(target, function() selectString(target, 1) fg("gold") deselect() resetFormat() end)</syntaxhighlight>
  
<code>^You are (\w+).*(\w+).*</code>
+
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:
  
The command "score" generates the following output on batMUD:
+
<syntaxhighlight lang="lua">
 +
target = target:title()
 +
if id then killTrigger(id) end
 +
id = tempTrigger(target, function() selectString(target, 1) fg("gold") deselect() resetFormat() end)</syntaxhighlight>
  
You have an almost non-existent ability for avoiding hits.
+
{{note}} Ensure you're on Mudlet 3.5.0+ so you can put ''function()'' in a ''[[Manual:Lua_Functions#tempTrigger|tempTrigger()]]''.
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:
+
==How to format an  echo to a miniConsole?==
  
showMultimatches()
+
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:
  
The script, i.e. the call to the function showMultimatches() generates this output:
+
<syntaxhighlight lang="lua">
<lua>
+
local WindowWidth, WindowHeight = getMainWindowSize();
-------------------------------------------------------
+
createMiniConsole("sys",WindowWidth-650,0,650,300)
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
 
-------------------------------------------------------
 
</lua>
 
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:
+
local name, age, sex = "Bob", 34, "male"
<lua>
 
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 }
 
}
 
</lua>
 
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 MUD = "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:
 
  
<lua>
+
cecho("sys", string.format([[
myGold = myGold + tonumber( matches[2] )
 
mySilver = mySilver + tonumber( matches[3] )
 
</lua>
 
  
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:
+
  /---------\
 +
      %s
 +
  %dyrs
 +
  sex - %s
 +
  \---------/
 +
]], name, age, sex))
 +
</syntaxhighlight>
  
<lua>
+
==How to play a sound when I receive communication while afk?==
myGold = myGold + tonumber( multimatches[1][2] )
 
mySilver = mySilver + tonumber( multimatches[1][3] )
 
</lua>
 
  
What makes multiline triggers really shine is the ability to react to MUD 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.
+
[[File:Play_sound_when_afk_trigger.png]]
  
=Event System=
+
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!
 
 
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 should a function to 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 should it 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 be called, and on what event should it 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 should this function be called on - you can add multiple ones. 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.
 
 
 
== Registering an event from a script ==
 
You can also register your event with the ''registerAnonymousEventHandler(event name, function name)'' function inside your scripts:
 
 
 
<lua>
 
-- 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")
 
</lua>
 
 
 
Note: Mudlet also uses the event system for the ATCP and GMCP events.
 
 
 
== Raising an event==
 
To raise an event, you’d use the raiseEvent function:
 
 
 
<lua>raiseEvent(name, [arguments...])</lua>
 
 
 
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.
 
 
 
;Mini-tutorial
 
As an example, our prompt trigger could raise an ''onPrompt'' event if you want to attach 2 functions to it. In your prompt trigger, all you’d need to do is ''raiseEvent("onPrompt")''. Now we go about creating functions that attach to the event - lets say the first one is ''check_health_stuff()'' and the other is ''check_salve_stuff()''. We would like these to be executed when the event is raised. So create a script and give it a name of ''check_health_stuff''. In the '''Add user defined event handler''', type ''onPrompt'', and press enter to add it to the list. In the script box, create:
 
 
 
<lua>
 
function check_health_stuff()
 
  echo("I work!\n")
 
end
 
</lua>
 
 
 
When the onPrompt event comes along, that script catches it, and runs ''check_health_stuff()'' for you.
 
 
 
[[Link title]]
 
==Mudlet-raised events==
 
Mudlet itself also creates events for your scripts to hook on. The following events are generated currently:
 
===sysWindowResizeEvent===
 
 
 
Raised when the main window 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.
 
Example
 
 
 
This sample code will echo whenever a resize happened with the new dimensions:
 
 
 
<lua>
 
function resizeEvent( event, x, y )
 
  echo("RESIZE EVENT: event="..event.." x="..x.." y="..y.."\n")
 
end
 
</lua>
 
 
 
===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 and the x,y coordinates of the click are reported.
+
==How can I make a key that toggles between commands?==
Example
+
To make a key that toggles between two actions, for example - sleep/wake, you can use this as the script:
  
<lua>
+
<syntaxhighlight lang="lua">
function onClickHandler( event, button, x, y )
+
if not sleeping then
   echo("CLICK event:"..event.." button="..button.." x="..x.." y="..y.."\n")
+
   send("sleep")
 +
  sleeping = true
 +
else
 +
  send("wake")
 +
  sleeping = false
 
end
 
end
</lua>
+
</syntaxhighlight>
  
===sysWindowMouseReleaseEvent===
+
<!-- Old Heading:  How can I make a trigger that goes off only if the next line is not a particular one? -->
 +
==How can I make a trigger with a next-line condition?==
  
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.
+
Here's how you can set it up - replace ''line1'' and ''line2'' with appropriate lines.
  
===sysLoadEvent===
+
[[File:Trigger_multiline_not_another_example.png|center]]
  
Raised when Mudlet is loading the profile. Note that when it does so, it also compiles and runs all scripts - which could be a good idea to initialize everything at, 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.
+
Here's [[Media:Match only of next line is *not* line2.xml.zip|the Mudlet XML]] you can download for this!
  
===sysExitEvent===
+
==How to target self or something else?==
  
Raised when Mudlet is shutting down the profile - a good event to hook onto for saving all of your data.
+
[[File:Buffselforother.png|border|350px]]
  
===sysDownloadDone===
+
Using regex and an 'or' statement allows us to consolidate multiple aliases, or an if statement, into a single more simple alias.
 +
When a regex is found it will use it, but if you don't specify one then the alternative will be used. The use of 'or' can be used with any function in Mudlet.
 +
In this example, one can type "buff" to buff themself, or "buff ally" to buff their ally instead.
  
Raised when Mudlet is finished downloading a file successfully - the location of the downloaded file is passed as a second argument. For a practical example, see the [[Manual:Lua_Functions#downloadFile|downloadFile()]] function.
+
<syntaxhighlight lang="lua">
 +
^buff(?: (\w+))?$
  
===sysDownloadError===
+
send("cast buff on "..(matches[2] or "myself"))
 +
</syntaxhighlight>
  
Raised when downloading a file failed - the second argument contains the error message. Does not specify which file download has failed yet, however.
+
==How to delete everything between two lines?==
 +
When deleteLine() is called, every following line shifts back in numerical position. Thus if we know where we're starting and how many lines to expect, we can use a for loop to gag everything between line a and b by leaving the cursor in one spot. In the below example, line_a_number is the number of the line you want to start at, and line_b_number is the ending line. To gag x number of lines instead, use line_a_number + x in place of line_b_number.
  
===sysIrcMessage===
+
<syntaxhighlight lang="lua">
 
+
for i = line_a_number, line_b_number, 1
Raised when you see or receive an IRC message. The speakers name, channel and their message will follow as arguments.
+
do
 
+
  moveCursor(0, line_a_number)
<lua>
+
  selectCurrentLine()
function onIrcMessage(_, person, channel, message)
+
  deleteLine()
  echo(string.format('(%s) %s says, "%s"\n', channel, person, message))
 
 
end
 
end
</lua>
+
</syntaxhighlight>
  
<sub>Added to Mudlet in an unreleased 2.0rc8</sub>
+
== How to capture foreground line color information? ==
 +
Here's an example that loops through each character in a line of text and prints the character and its foreground color to a miniconsole if the color has changed from the previous character:
  
===sysDataSendRequest===
+
Trigger: <code>^</code> (regex)
  
Raised right before a command from the send() function or the command line is sent to the game - useful for keeping track of what your last command was, or even denying the command to be sent if necessary with ''denyCurrentSend()''.
+
Code:
  
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 [http://en.wikipedia.org/wiki/Keylogger keylogger] either.
+
<syntaxhighlight lang="lua">
 +
miniconsoleContainer =
 +
  miniconsoleContainer or Adjustable.Container:new({name = "miniconsoleContainer"})
 +
myMiniconsole =
 +
  Geyser.MiniConsole:new(
 +
    {
 +
      name = "myMiniconsole",
 +
      x = 0,
 +
      y = 0,
 +
      autoWrap = true,
 +
      color = "black",
 +
      scrollBar = false,
 +
      fontSize = 8,
 +
      width = "100%",
 +
      height = "100%",
 +
    },
 +
    miniconsoleContainer
 +
  )
  
<lua>
+
local index = 0
function onNetworkOutput(_, command)
+
local line = getCurrentLine()
   if math.random(2) == 1 then
+
local linesize = #line
     echo("Hello! Sending "..command.." to the game.\n")
+
local currentlinenumber = getLineNumber()
 +
local r, g, b = 0, 0, 0
 +
local cr, cg, cb -- last seen colors
 +
while index < linesize do
 +
  index = index + 1
 +
  moveCursor("main", index, currentlinenumber)
 +
  selectString(line:sub(index), 1)
 +
   r, g, b = getFgColor()
 +
  if cr ~= r or cg ~= g or cb ~= b then
 +
     cr, cg, cb = r, g, b
 +
    myMiniconsole:echo(string.format("rgb(%d,%d,%d)%s", r, g, b, line:sub(index, index)))
 
   else
 
   else
     echo("Not your day! Denying "..command..".\n")
+
     myMiniconsole:echo(string.format("%s", line:sub(index, index)))
 
   end
 
   end
 +
  line:sub(index, index)
 
end
 
end
</lua>
+
myMiniconsole:echo("\n")
 +
</syntaxhighlight>
  
==GMCP==
+
==How to enter a carriage return / enter command?==
GMCP is a protocol for MUD servers to communicate information with MUD clients in a separate channel from the one which carries all of the text that makes up the game itself. Mudlet can be configured to use GMCP by clicking on the Settings button (or Options->Preferences in the menu, or <alt>p). The option is on the General tab.  
+
Say you'd like to gag the following line, and just continue ...
Once GMCP is enabled, you will need to reconnect to the MUD so that Mudlet can inform the server it is ready to receive GMCP information. Mudlet will automatically enable some GMCP modules for you once GMCP has been enabled. To get an idea of what information is available, you can use
 
<lua>
 
display(gmcp)
 
</lua>
 
  
When working with GMCP on IRE games, their [http://ironrealms.com/gmcp-doc GMCP reference] is a useful tool.
+
[ Press Enter to continue ]
  
===Using GMCP===
+
<syntaxhighlight lang="lua">
====Receiving GMCP Data====
+
Trigger: [ Press Enter to continue ]  (set to exact match)
To use the GMCP messages you'll need to create a new script and give it a name. Now register an event handler for the module you want to use. Now you'll need to create a function with exactly the same name as the script file. This function will be called each time you get the GMCP message. The information itself will be saved in the corresponding field of the gmcp table.
+
Code: deleteLine()
 +
      send("\n")
 +
</syntaxhighlight>
  
Here's a screenshot of the setup you need:
+
==How to repeat commands with #?==
 +
Install the [https://forums.mudlet.org/viewtopic.php?f=6&t=1275 following alias], and with it <code>#5 mycommand</code> will get repeated five times.
  
[[File:using_gmcp.png]]
+
== How to move the command line over? ==
  
====Sending GMCP Data====
+
See the first example in the [[Manual:Lua_Functions#setCmdLineStyleSheet | setCmdLineStyleSheet function]]. Read the note attached to the function description.
Certain modules will only send data when a request is made by your client. In Mudlet, you can make such a request using the command '''sendGMCP("command")'''. Read your MUD's relevant documentation, such as the [http://www.ironrealms.com/gmcp-doc IRE document on GMCP], for information about specific modules.
 
  
:See Also: [[Manual:Lua_Functions#sendGMCP]]
+
== How to add a background for Mudlets buttons? ==
 +
You can give Mudlets buttons a background to have them blend in with your UI by doing this:
  
===Managing GMCP Modules===
+
<syntaxhighlight lang="lua">
While some GMCP modules are enabled by Mudlet by default when you connect with a GMCP enabled MUD, others may not be 'standard' modules and are instead specific to the MUD itself. In order to provide a way to manage GMCP modules without scripts causing modules in use by other scripts to be disabled, the gmod lua module has been included with Mudlet (rc2.0+).  
+
-- replace the /home/vadi/Pictures/tile.png location below with the one of your picture!
 +
-- you can download the sample tile.png from https://opengameart.org/content/bevouliin-free-game-background-for-game-developers
 +
local backgroundpath = [[/home/vadi/Pictures/tile.png]]
 +
-- Windows:
 +
-- local backgroundpath = [[C:/Users/Vadim/Downloads/Free game background/transparent PNG/tile.png]]
  
====Registering User====
+
setAppStyleSheet([[
While this step is no longer strictly required, you can register your 'user' with gmod using
+
QToolBar {
<lua>
+
  border-image: url(]]..backgroundpath..[[);
gmod.registerUser("MyUser")
 
</lua>
 
Where ''MyUser'' is your plugin/addon/whatever name. However, your user will be automatically registered if you enable or disable any modules using it. Which leads us to...
 
  
====Enabling Modules====
+
}
Enabling a GMCP module is as easy as:
 
<lua>
 
gmod.enableModule("MyUser", "Module.Name")
 
</lua>
 
  
If MyUser has not been registered previously, then they will be automatically registered when you call this function. An example of a module which would need to be enabled this way is the IRE.Rift module provided by IRE MUDs.
+
QToolBar QToolButton:!hover {
<lua>
+
  color: white;
gmod.enableModule("MyUser", "IRE.Rift")
+
}
</lua>
+
QToolBar QToolButton:hover {
 
+
  color: black;
====Disabling Modules====
+
}
Disabling a GMCP module is just as easy as enabling it:
+
]])
<lua>
+
</syntaxhighlight>
gmod.disableModule("MyUser", "Module.Name")
 
</lua>
 
The main difference being that the module will be turned on as soon as you enable it if it is not already enabled. If you disable it, it will not be disabled with the server until every user of that module has disabled it. This prevents script A from disabling modules that script B may still be using.
 
 
 
=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?==
+
Result:
 
 
[[File: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!
+
[[File:Mudlet tiled buttons.png|frame|left]]
 +
<br clear=all>
  
 
= Advanced scripting tips =
 
= Advanced scripting tips =
Line 510: Line 419:
 
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:
 
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>
+
<syntaxhighlight lang="lua">tempTimer(0, [[mycode]])</syntaxhighlight>
  
 
== How to delete the previous and current lines ==
 
== How to delete the previous and current lines ==
Line 516: Line 425:
 
This little snippet comes from [http://forums.mudlet.org/viewtopic.php?f=9&t=2411&p=10685#p10685 Iocun]:
 
This little snippet comes from [http://forums.mudlet.org/viewtopic.php?f=9&t=2411&p=10685#p10685 Iocun]:
  
<lua>
+
<syntaxhighlight lang="lua">
 
moveCursor(0,getLineCount()-1)
 
moveCursor(0,getLineCount()-1)
 
deleteLine()
 
deleteLine()
 
moveCursor(0,getLineCount())
 
moveCursor(0,getLineCount())
 
deleteLine()
 
deleteLine()
</lua>
+
</syntaxhighlight>
 
 
=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 [http://wiki.mudlet.org/w/Manual:ATCP_Extensions 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=
 
=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.
+
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.
  
 
==Creating a Database==
 
==Creating a Database==
Line 569: Line 440:
 
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.
 
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:
+
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, this would mean that you’d define what goes into each column. A simple example:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes"}})
 
db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes"}})
</lua>
+
</syntaxhighlight>
  
 
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.
 
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:
+
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.  
 +
 
 +
{{Note}} You may not create a column or field name which begins with an underscore. This is strictly reserved to the db package for special use.
 +
 
 +
{{Note}} Adding new columns to an existing database did not work in Mudlet 2.1 - see [[Manual:Lua_Functions#db:create|db:create()]] on how to deal with this.
 +
 
 +
<!--
 +
More importantly, this allows you to add columns to an existing sheet. If you change that line to:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes", "enemied"}})
 
db:create("people", {friends={"name", "city", "notes"}, enemies={"name", "city", "notes", "enemied"}})
</lua>
+
</syntaxhighlight>
  
 
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.
 
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==
 
==Adding Data==
Line 591: Line 468:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   local mydb = db:get_database("people")
 
   local mydb = db:get_database("people")
</lua>
+
</syntaxhighlight>
  
 
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.
 
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>
+
<syntaxhighlight lang="lua">
 
   db:add(mydb.friends, {name="Ixokai", city="Magnagora"})
 
   db:add(mydb.friends, {name="Ixokai", city="Magnagora"})
</lua>
+
</syntaxhighlight>
  
 
If you would like to add multiple rows at once to the same table, you can do that by just passing in multiple tables:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:add(mydb.friends,
 
   db:add(mydb.friends,
 
     {name="Ixokai", city="Magnagora"},
 
     {name="Ixokai", city="Magnagora"},
Line 609: Line 486:
 
     {name="Heiko", city="Hallifax", notes="The Boss"}
 
     {name="Heiko", city="Hallifax", notes="The Boss"}
 
   )
 
   )
</lua>
+
</syntaxhighlight>
  
 
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)
 
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.
+
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 than 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".
 
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".
Line 621: Line 498:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends)
 
   db:fetch(mydb.friends)
</lua>
+
</syntaxhighlight>
  
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"))
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"))
</lua>
+
</syntaxhighlight>
  
 
So the basic command is - db:fetch(_sheet_, _what to filter by_)
 
So the basic command is - db:fetch(_sheet_, _what to filter by_)
Line 635: Line 512:
 
The following filter operations are defined:
 
The following filter operations are defined:
  
<lua>
+
<syntaxhighlight lang="lua">
 
   db:eq(field, value[, case_insensitive]) -- Defaults to case insensitive, pass true as the last arg to
 
   db:eq(field, value[, case_insensitive]) -- Defaults to case insensitive, pass true as the last arg to
 
               reverse this behavior.
 
               reverse this behavior.
Line 652: Line 529:
 
   db:in_(field, table) -- Tests if the field is in the values of the table. NOTE the trailing underscore!
 
   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 *
 
   db:not_in(field, table) -- Tests if the field is NOT in the values of the table *
</lua>
+
</syntaxhighlight>
  
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:in_(mydb.friends.city, {"Magnagora", "New Celest"})
 
   db:in_(mydb.friends.city, {"Magnagora", "New Celest"})
</lua>
+
</syntaxhighlight>
  
 
It tests if city == "Magnagora" OR city == "New Celest", but with a more concise syntax for longer lists of items.
 
It tests if city == "Magnagora" OR city == "New Celest", but with a more concise syntax for longer lists of items.
Line 664: Line 541:
 
There are also two logical operators:
 
There are also two logical operators:
  
<lua>
+
<syntaxhighlight lang="lua">
 
   db:AND(operation1, ..., operationN)
 
   db:AND(operation1, ..., operationN)
 
   db:OR(operation1, operation2)
 
   db:OR(operation1, operation2)
</lua>
+
</syntaxhighlight>
  
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends,
 
   db:fetch(mydb.friends,
 
     {db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "X%")}
 
     {db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "X%")}
 
   )
 
   )
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 681: Line 558:
 
Passing multiple expressions in an array to db:fetch is just a convenience, as its exactly the same as:
 
Passing multiple expressions in an array to db:fetch is just a convenience, as its exactly the same as:
  
<lua>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, db:AND(db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "I%")))
 
   db:fetch(mydb.friends, db:AND(db:eq(mydb.friends.city, "Magnagora"), db:like(mydb.friends.name, "I%")))
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 689: Line 566:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, "city == 'Magnagora'")
 
   db:fetch(mydb.friends, "city == 'Magnagora'")
</lua>
+
</syntaxhighlight>
  
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   table {
 
   table {
 
   1: table {
 
   1: table {
Line 703: Line 580:
 
   }
 
   }
 
}
 
}
</lua>
+
</syntaxhighlight>
  
 
And if you were to echo(#results), it would show 1.
 
And if you were to echo(#results), it would show 1.
Line 709: Line 586:
 
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 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:
+
The first table is an 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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city})
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city})
</lua>
+
</syntaxhighlight>
  
 
This will return all your friends in Magnagora, sorted by their name, from smallest to largest. To reverse this, you would simply do:
 
This will return all your friends in Magnagora, sorted by their name, from smallest to largest. To reverse this, you would simply do:
  
<lua>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city}, true)
 
   db:fetch(mydb.friends, db:eq(mydb.friends.city, "Magnagora"), {mydb.friends.city}, true)
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 725: Line 602:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
   db:fetch(mydb.friends, nil, {mydb.friends.city, mydb.friends.name})
 
   db:fetch(mydb.friends, nil, {mydb.friends.city, mydb.friends.name})
</lua>
+
</syntaxhighlight>
  
 
This will return every friend you have, sorted first by city and then their name.
 
This will return every friend you have, sorted first by city and then their name.
Line 735: Line 612:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
db:create("people", {
 
db:create("people", {
 
   friends={"name", "city", "notes"},
 
   friends={"name", "city", "notes"},
Line 746: Line 623:
 
   }
 
   }
 
})
 
})
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 756: Line 633:
 
To add an index, pass either the _index or _unique keys in the table definition. An example:
 
To add an index, pass either the _index or _unique keys in the table definition. An example:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:create("people", {
 
db:create("people", {
 
   friends={"name", "city", "notes"},
 
   friends={"name", "city", "notes"},
Line 769: Line 646:
 
   }
 
   }
 
})
 
})
</lua>
+
</syntaxhighlight>
 
 
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.
 
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.
Line 781: Line 654:
 
===Uniqueness===
 
===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.
+
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. You can specify more than one key to be unique; in that case they will be checked in an OR condition.
  
 
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.
 
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.
Line 791: Line 664:
 
For example:
 
For example:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:add(mydb.enemies, {name="Bob", city="Sacramento"})
 
db:add(mydb.enemies, {name="Bob", city="Sacramento"})
 
db:add(mydb.enemies, {name="Bob", city="San Francisco"})
 
db:add(mydb.enemies, {name="Bob", city="San Francisco"})
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 804: Line 677:
 
A word of caution with REPLACE, given:
 
A word of caution with REPLACE, given:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:add(mydb.enemies, {name="Bob", city="Sacramento", notes="This is something."})
 
db:add(mydb.enemies, {name="Bob", city="Sacramento", notes="This is something."})
 
db:add(mydb.enemies, {name="Bob", city="San Francisco"})
 
db:add(mydb.enemies, {name="Bob", city="San Francisco"})
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 813: Line 686:
 
To specify which behavior the db layer should use, add a _violations key to the table definition:
 
To specify which behavior the db layer should use, add a _violations key to the table definition:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:create("people", {
 
db:create("people", {
 
   friends={"name", "city", "notes"},
 
   friends={"name", "city", "notes"},
Line 827: Line 700:
 
   }
 
   }
 
})
 
})
</lua>
+
</syntaxhighlight>
  
 
Note that the _violations behavior is sheet-specific.
 
Note that the _violations behavior is sheet-specific.
Line 837: Line 710:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
local mydb = db:create("combat_log",
 
local mydb = db:create("combat_log",
 
   {
 
   {
Line 849: Line 722:
 
)
 
)
  
 
+
-- observe that we don't provide the killed field here - DB automatically fills it in for us.
 
db:add(mydb.kills, {name="Drow", area="Undervault"})
 
db:add(mydb.kills, {name="Drow", area="Undervault"})
  
 
results = db:fetch(mydb.kills)
 
results = db:fetch(mydb.kills)
 
display(results)
 
display(results)
</lua>
+
</syntaxhighlight>
  
 
The result of that final display would show you this on a newly created sheet:
 
The result of that final display would show you this on a newly created sheet:
  
<lua>
+
<syntaxhighlight lang="lua">
 
table {
 
table {
 
   1: table {
 
   1: table {
Line 869: Line 742:
 
   }
 
   }
 
}
 
}
</lua>
+
</syntaxhighlight>
  
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.
+
As you can see from this output, the killed fields contains a timestamp-- and that timestamp is stored as an epoch value in the GMT timezone. For your convenience, the db.Timestamp type offers three functions to get the value of the timestamp in easy formats. They are <code>as_string</code>, <code>as_number</code> and <code>as_table</code>, 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.
 
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.
Line 877: Line 750:
 
A quick example of the usage of these functions is:
 
A quick example of the usage of these functions is:
  
<lua>
+
<syntaxhighlight lang="lua">
 
results = db:fetch(mydb.kills)
 
results = db:fetch(mydb.kills)
 
for _, row in ipairs(results) do
 
for _, row in ipairs(results) do
   echo("You killed " .. row.name .. " at: " .. row.killed:as_string() .."\n")
+
   echo("You killed " .. row.name .. " at: " .. (row.killed and row.killed:as_string() or "no time") .."\n")
 
end
 
end
</lua>
+
</syntaxhighlight>
  
 
==Deleting==
 
==Deleting==
Line 890: Line 763:
 
For example, to delete all your enemies in the city of Magnagora, you would do:
 
For example, to delete all your enemies in the city of Magnagora, you would do:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:delete(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora"))
 
db:delete(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora"))
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 898: Line 771:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
results = db:fetch(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora"))
 
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))
 
db:delete(mydb.enemies, db:eq(mydb.enemies._row_id, results[1]._row_id))
</lua>
+
</syntaxhighlight>
  
 
That is equivalent to:
 
That is equivalent to:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:delete(mydb.enemies, results[1])
 
db:delete(mydb.enemies, results[1])
</lua>
+
</syntaxhighlight>
  
 
You can even pass a number directly to db:delete if you know what _row_id you want to purge.
 
You can even pass a number directly to db:delete if you know what _row_id you want to purge.
Line 913: Line 786:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
db:delete(mydb.enemies, true)
 
db:delete(mydb.enemies, true)
</lua>
+
</syntaxhighlight>
  
 
==Updating==
 
==Updating==
Line 921: Line 794:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
db:update(mydb.enemies, results[1])
 
db:update(mydb.enemies, results[1])
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 931: Line 804:
 
To clear out the notes of all of our friends in Magnagora, we could do:
 
To clear out the notes of all of our friends in Magnagora, we could do:
  
<lua>
+
<syntaxhighlight lang="lua">
 
db:set(mydb.friends.notes, "", db:eq(mydb.friends.notes, "Magnagora"))
 
db:set(mydb.friends.notes, "", db:eq(mydb.friends.notes, "Magnagora"))
</lua>
+
</syntaxhighlight>
  
 
Be careful in writing these!
 
Be careful in writing these!
Line 941: Line 814:
 
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:
 
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>
+
<syntaxhighlight lang="lua">
 
local mydb = db:get_database("my_database")
 
local mydb = db:get_database("my_database")
mydb._begin()
+
mydb:_begin()
mydb._commit()
+
mydb:_commit()
mydb._rollback()
+
mydb:_rollback()
mydb._end()
+
mydb:_end()
</lua>
+
</syntaxhighlight>
  
 
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.
 
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.
Line 958: Line 831:
  
 
If you'd like to see the queries that db: is running for you, you can enable debug mode with ''db.debug_sql = true''.
 
If you'd like to see the queries that db: is running for you, you can enable debug mode with ''db.debug_sql = true''.
 +
 +
=Discord Messages=
 +
 +
Can you send messages from Mudlet to Discord? Totally. Here is an example:
 +
 +
<syntaxhighlight lang="lua">
 +
 +
function sendToDiscord(data)
 +
  local url = "https://discord.com/api/webhooks/1204151397977821184/Cu8ZQQo9R11AyIt3DxpOEeB8MtVMA6_AEfXoZaYxqEAOu_6hwhtA2cRLIw-VIs7py7p8"
 +
  local headers = {["Content-Type"] = "application/json"}
 +
  local json = yajl.to_string(data)
 +
  postHTTP(json, url, headers)
 +
end
 +
 +
local data = {
 +
  content = "Hello, Discord!",
 +
  embeds = {
 +
    {
 +
      title = "This is an embed",
 +
      url = "https://example.com",
 +
      image = {
 +
        url = "https://example.com/image.jpg"
 +
      }
 +
    }
 +
  }
 +
}
 +
 +
sendToDiscord(data)
 +
 +
</syntaxhighlight>
 +
 +
Sure enough, after starting this script, you can see a message in Discord:
 +
 +
[[File:Message to Discord.png|border|center]]
 +
 +
What you just need to do is create a webhook in a channel. Go to channel settings, then create this:
 +
 +
[[File:Webhook in Discord.png|border|thumbnail|center]]
 +
 +
Here you can find more documentation on which fields Discord allows: https://discord.com/developers/docs/resources/webhook#execute-webhook
 +
 +
All this is one way though. Mudlet won't listen to messages from Discord. For that you'd probably have to make a Discord bot first.
 +
 +
=Discord Rich Presence=
 +
[[File:Rich_Presence_Midmud.gif|frame]]
 +
<!-- [[File:Rich Presence Achaea.gif|frame]] -->
 +
 +
Discord Rich Presence in Mudlet allows you, the player, to show off the game you're playing and you, the game author, to get your game shown off! Advertise your game for free alongside AAA titles in server player lists :)
 +
 +
To respect the players privacy, this option is disabled by default - enable it by ticking the Discord option for the profile:
 +
 +
[[File:Enable Discord.png|border|center]]
 +
 +
Once enabled, a Discord-capable game will show information in your Discord!
 +
 +
To fine-tune the information you'd like to see displayed, go to Settings, Chat tab:
 +
 +
[[File:Discord Privacy.png|center|400px]]
 +
 +
== Implementing ==
 +
As a game author, you have two ways of adding Discord support to your game: via GMCP (easy) or via Lua.
 +
 +
Adding support via GMCP means that your players don't have to install any Lua scripts which is a nicer experience. To do so, implement the [[Standards:Discord_GMCP|Discord over GMCP]] specification and [https://www.mudlet.org/contact/ get in touch] to let us know your game implements GMCP.
 +
 +
=== GMCP ===
 +
Here's a quick cheatsheet to implement the [[Standards:Discord_GMCP|GMCP spec]]:
 +
 +
# Create a discord [https://discordapp.com/developers/applications/me application]
 +
# Upload game logo for "rich presence invite image"
 +
# Upload same game logo in "Rich Presence Assets" with the name of <code>server-icon</code>
 +
# Upon receiving "External.Discord.Hello" from the game client, reply with "External.Discord.Info" with <code>applicationid</code> ([[:File:Mudlet Discord ApplicationID.png|see screenshot]]) and "External.Discord.Status"
 +
# Send "External.Discord.Status" whenever you need to send data to the client, and fill it with the values have changed since the last status. Discord has [https://discordapp.com/developers/docs/rich-presence/best-practices#tips some ideas] for filling it in.
 +
# Done. Mudlet will enable the Discord checkbox upon receiving the <code>applicationid</code> from the game at least once.
 +
 +
=== Lua ===
 +
An alternative way to add Discord support as a game author is [[Manual:Discord_Functions|via Lua]] - all of Discords functionality can be manipulated from it. Create a discord [https://discordapp.com/developers/applications/me application] and set it with [[Manual:Discord_Functions#setDiscordApplicationID|setDiscordApplicationID()]], then set the set the rest of the options as necessary:
 +
 +
[[File:Discord fields.png|frame|center|Credit: Discord]]
 +
 +
If you're a game player and your game doesn't support Discord, you can still use the functionality to set the detail, status, party and time information as desired.
 +
 +
{{MudletVersion|3.14}}
 +
 +
=MUD Client Media Protocol=
 +
 +
==Client.Media==
 +
Want to add accessibility and excitement into your game? How about implementing sound and music via GMCP?
 +
 +
The Client.Media family of GMCP packages provide a way for games to send sound and music events. GMCP media events are sent in one direction: from game server to to game client.
 +
 +
===Client.Media in Mudlet===
 +
 +
Media files may be downloaded manually or automatically if certain conditions are met.
 +
 +
{{MudletVersion|4.4}}
 +
 +
Mudlet accepts case-insensitive GMCP for the Client.Media namespace (for example, these are all acceptable: "Client.Media.Play", "client.media.play", "cLiEnT.MeDiA.Play").
 +
 +
===Enabling Media===
 +
When a new profile opens, Mudlet adds to the Core.Supports.Set GMCP package "Client.Media 1" to signal to servers that it supports processing media files via GMCP:
 +
 +
<syntaxhighlight lang="json">
 +
Core.Supports.Set ["Client.Media 1", ...]
 +
</syntaxhighlight>
 +
 +
To process Client.Media GMCP data in Mudlet, these boxes in the Settings window of Mudlet must be checked:
 +
 +
# The "Enable GMCP" box in the Miscellaneous section.
 +
# The "Allow server to download and play media" box in the Game protocols section.
 +
 +
These boxes are checked by default upon the start of a new profile in Mudlet.
 +
 +
===Storing Media===
 +
 +
Mudlet plays files cached in the "media" folder within a game's profile. To get files into the "media" folder you could perform either of these options:
 +
 +
# Copy media files or extract them from an archive (zip)
 +
# Automatically download them from external resources such as your game's web server on the Internet.
 +
 +
To enable the second option (Internet), use the "url" parameter made available in the "Client.Media.Default", "Client.Media.Load" and "Client.Media.Play" GMCP packages described below.
 +
 +
===Loading Media===
 +
 +
Send a '''Client.Media.Default''' GMCP event to setup the default URL directory to load sound or music files from external resources. This would typically be done once upon player login.
 +
 +
{| class="wikitable"
 +
! Required
 +
! Key
 +
! Value
 +
! style="text-align:left;"| Purpose
 +
|- style="color: green;"
 +
| style="text-align:center;"| Yes
 +
| "url"
 +
| <url>
 +
| style="text-align:left;"|
 +
* Resource location where the media file may be downloaded.
 +
|-
 +
|}
 +
 +
For example, to setup the default URL directory for your game:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Default {
 +
  "url": "https://www.example.com/sounds/"
 +
}
 +
</syntaxhighlight>
 +
 +
If downloading from external resources, it is recommended to use Client.Media.Default, set this default URL directory once, and let Mudlet download all of your media files automatically from this one resource location. Otherwise, explicitly set the "url" parameter on all of your Client.Media.Load GMCP and Client.Media.Play GMCP events to ensure that files are downloaded from the intended location(s).
 +
 +
Send '''Client.Media.Load''' GMCP events to load sound or music files. This could be done when the player logs into your game or at the beginning of a zone that will need the media.
 +
 +
{| class="wikitable"
 +
! Required
 +
! Key
 +
! Value
 +
! style="text-align:left;"| Purpose
 +
|- style="color: green;"
 +
| style="text-align:center;"| Yes
 +
| "name"
 +
| <file name>
 +
| style="text-align:left;"|
 +
* Name of the media file.
 +
* May contain directory information (i.e. weather/lightning.wav).
 +
|- style="color: blue;"
 +
| style="text-align:center;"| Maybe
 +
| "url"
 +
| <url>
 +
| style="text-align:left;"|
 +
* Resource location where the media file may be downloaded.
 +
* Only required if a url was not set above with Client.Media.Default.
 +
|-
 +
|}
 +
 +
For example, download the sound of a sword swooshing within the "media" folder:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Load {
 +
  "name": "sword1.wav",
 +
  "url": "https://www.example.com/sounds/"
 +
}
 +
</syntaxhighlight>
 +
 +
===Playing Media===
 +
 +
Send '''Client.Media.Play''' GMCP events to play sound or music files.
 +
 +
{| class="wikitable"
 +
! Required
 +
! Key
 +
! Value
 +
! Default
 +
! style="text-align:left;"| Purpose
 +
|- style="color: green;"
 +
| style="text-align:center;"| Yes
 +
| "name"
 +
| <file name>
 +
| &nbsp;
 +
| style="text-align:left;"|
 +
* Name of the media file.
 +
* May contain directory information (i.e. weather/lightning.wav).
 +
* Wildcards ''*'' and ''?'' may be used within the name to randomize media files selection.
 +
|- style="color: blue;"
 +
| style="text-align:center;"| Maybe
 +
| "url"
 +
| <url>
 +
| &nbsp;
 +
| style="text-align:left;"|
 +
* Resource location where the media file may be downloaded.
 +
* Only required if the file is to be downloaded remotely and a url was not set above with Client.Media.Default or Client.Media.Load.
 +
|-
 +
| style="text-align:center;"| No
 +
| "type"
 +
| "sound" or "music"
 +
| "sound"
 +
| style="text-align:left;"|
 +
* Identifies the type of media.
 +
|-
 +
| style="text-align:center;"| No
 +
| "tag"
 +
| <tag>
 +
| &nbsp;
 +
| style="text-align:left;"|
 +
* Helps categorize media.
 +
|-
 +
| style="text-align:center;"| No
 +
| "volume"
 +
| 1 to 100
 +
| 50
 +
| style="text-align:left;"|
 +
* Relative to the volume set on the player's client.
 +
|-
 +
| style="text-align:center;"| No
 +
| "fadein"
 +
|<msec>
 +
|
 +
|
 +
*Volume increases, or fades in, ranged across a linear pattern from one to the volume set with the "volume" key.
 +
*Start position:  Start of media.
 +
*End position:  Start of media plus the number of milliseconds (msec) specified.
 +
*1000 milliseconds = 1 second.
 +
|-
 +
| style="text-align:center;" |No
 +
|"fadeout"
 +
|<msec>
 +
|
 +
|
 +
*Volume decreases, or fades out, ranged across a linear pattern from the volume set with the "volume" key to one.
 +
*Start position:  End of the media minus the number of milliseconds (msec) specified.
 +
*End position:  End of the media.
 +
*1000 milliseconds = 1 second.
 +
|-
 +
| style="text-align:center;"| No
 +
| "start"
 +
| <msec>
 +
| 0
 +
| style="text-align:left;"|
 +
* Begin play at the specified position in milliseconds.
 +
* 1000 milliseconds = 1 second.
 +
|-
 +
| style="text-align:center;"| No
 +
| "finish"
 +
| <msec>
 +
| 0
 +
| style="text-align:left;"|
 +
* End play at the specified position in milliseconds.
 +
* 1000 milliseconds = 1 second.
 +
|-
 +
| style="text-align:center;" |No
 +
|"loops"
 +
| -1, or >= 1
 +
|1
 +
| style="text-align:left;"|
 +
* Number of iterations that the media plays.
 +
* A value of -1 allows the sound or music to loop indefinitely.
 +
|-
 +
| style="text-align:center;" |No
 +
|"priority"
 +
|1 to 100
 +
|&nbsp;
 +
| style="text-align:left;" |
 +
*Halts the play of current or future played media files with a lower priority while this media plays.
 +
|-
 +
| style="text-align:center;" |No
 +
|"continue"
 +
| true or false
 +
|true
 +
| style="text-align:left;" |
 +
*Only valid for media files with a "type" of "music".
 +
*Continues playing matching new music files when true.
 +
*Restarts matching new music files when false.
 +
|-
 +
| style="text-align:center;" |No
 +
|"key"
 +
|<key>
 +
|&nbsp;
 +
| style="text-align:left;" |
 +
*Uniquely identifies media files with a "key" that is bound to their "name" or "url".
 +
*Halts the play of current media files with the same "key" that have a different "name" or "url" while this media plays.
 +
|-
 +
|}
 +
 +
==== name====
 +
 +
For example, to simply play the sound of a cow mooing already stored in the "media" folder:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "cow.wav"
 +
}
 +
</syntaxhighlight>
 +
 +
Or some lightning stored in a "weather" sub-folder under the "media" folder.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "weather/lightning.wav"
 +
}
 +
</syntaxhighlight>
 +
 +
The "name" parameter may be used for stopping media with the Client.Media.Stop GMCP event.
 +
 +
====url====
 +
 +
If you maintain your sound files on the Internet and don't set a default URL with Client.Media.Default, or preload them with Client.Media.Load, include the "url" parameter:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "cow.wav",
 +
  "url": "https://www.example.com/sounds/"
 +
}
 +
</syntaxhighlight>
 +
 +
====type: sound====
 +
 +
Media files default to a "type" of "sound" when the "type" parameter is not specified, such as in the example above. It is good practice to specify the "type" parameter to best keep media organized within your implementation:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "cow.wav",
 +
  "type": "sound"
 +
}
 +
</syntaxhighlight>
 +
 +
The "type" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.
 +
 +
====tag====
 +
 +
To play the sound of a sword swooshing and categorized as "combat":
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "sword1.wav",
 +
  "type": "sound",
 +
  "tag": "combat"
 +
}
 +
</syntaxhighlight>
 +
 +
The "tag" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.
 +
 +
====type: music====
 +
 +
Add background music to your environment through use of the "music" value set upon the "type" parameter:
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "river.wav",
 +
  "type": "music",
 +
  "tag": "environment"
 +
}
 +
</syntaxhighlight>
 +
 +
The "type" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.
 +
 +
====volume: 1 to 100====
 +
 +
As the character draws nearer to or farther from the environmental feature, consider adjusting the "volume" parameter.  Example values, followed by syntax examples:
 +
 +
*Maximum: 100 (recommended to use rarely)
 +
*High: 75
 +
*Default: 50
 +
* Low: 25
 +
* Minimum: 1 (recommended to use rarely)
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "river.wav",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "volume": 75
 +
}
 +
</syntaxhighlight>
 +
 +
Although using the integer type is recommended, Mudlet parses "volume" values of type string ("75") into integers (75) as needed.
 +
 +
====fadein====
 +
 +
Increase volume from the start of the media [start position, volume one] to the number in milliseconds specified with the "fadein" parameter [position start + milliseconds, volume specified with the "volume" parameter].  Example values, followed by syntax examples:
 +
 +
*1000 milliseconds (1 second)
 +
*60000 milliseconds (1 minute)
 +
*0 milliseconds (no fade in)
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "monty_python.mp3",
 +
  "type": "music",
 +
  "volume": 100,
 +
  "fadein": 1000
 +
}
 +
</syntaxhighlight>
 +
        +---------------------------+-----------------------+
 +
        | Fade in 1000 milliseconds |        Normal        |
 +
        +---------------------------+-----------------------+
 +
Volume 1|........::::::::::iiiiiiiii%%%%%%%%%%%%%%%%%%%%%%%%|Volume 100
 +
        +---------------------------+-----------------------+
 +
Although using the integer type is recommended, Mudlet parses "fadein" values of type string ("1000") into integers (1000) as needed.
 +
 +
==== fadeout====
 +
 +
Decrease volume from a position in milliseconds subtracted from the duration of the media [position duration - milliseconds, volume specified with the "volume" parameter] to the end of the media [position duration, volume one].  Example values, followed by syntax examples:
 +
 +
*1000 milliseconds (1 second)
 +
*60000 milliseconds (1 minute)
 +
*0 milliseconds (no fade out)
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "the_matrix.mp3",
 +
  "type": "music",
 +
  "volume": 50,
 +
  "fadeout": 333
 +
}
 +
</syntaxhighlight>
 +
          +-----------------------+---------------------------+
 +
          |        Normal        | Fade out 333 milliseconds |
 +
          +-----------------------+---------------------------+
 +
Volume 50|%%%%%%%%%%%%%%%%%%%%%%%%iiiiiiiii::::::::::........|Volume 1
 +
          +-----------------------+---------------------------+
 +
Although using the integer type is recommended, Mudlet parses "fadeout" values of type string ("333") into integers (333) as needed.
 +
 +
==== start ====
 +
 +
Start playing a media track at the number of milliseconds specified.  Example values, followed by syntax examples:
 +
 +
*1000 milliseconds (1 second)
 +
*60000 milliseconds (1 minute)
 +
*0 milliseconds (start from beginning)
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "mudlet_beatboxing.mp3",
 +
  "type": "music",
 +
  "volume": 75,
 +
  "start": 1000
 +
}
 +
</syntaxhighlight>
 +
 +
Although using the integer type is recommended, Mudlet parses "start" values of type string ("100") into integers (100) as needed.
 +
 +
==== finish ====
 +
 +
Finish playing a media track at the number of milliseconds specified.  Example values, followed by syntax examples:
 +
 +
*1000 milliseconds (1 second)
 +
*60000 milliseconds (1 minute)
 +
*0 milliseconds (start from beginning)
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "mudlet_beatboxing.mp3",
 +
  "type": "music",
 +
  "volume": 75,
 +
  "finish": 35000
 +
}
 +
</syntaxhighlight>
 +
 +
Although using the integer type is recommended, Mudlet parses "finish" values of type string ("100") into integers (100) as needed.
 +
 +
==== loops: -1, 1 or more ====
 +
 +
The "loops" parameter controls how many times the sound repeats and defaults to 1 if not specified. A value of -1 will loop the file indefinitely.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "clock/hour_bell.wav",
 +
  "type": "sound",
 +
  "loops": 3
 +
}
 +
 +
Client.Media.Play {
 +
  "name": "underdark.mp3",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "loops": -1
 +
}
 +
</syntaxhighlight>
 +
 +
Although using the integer type is recommended, Mudlet parses "loops" values of type string ("-1") into integers (-1) as needed.
 +
 +
==== priority: 1 to 100 ====
 +
 +
The "priority" parameter sets precedence for Client.Media.Play GMCP events that include a "priority" setting. The values for the "priority" parameter range between 1 and 100. Given that two Client.Media.Play GMCP events have the same priority, the media already playing will continue playing. In Mudlet, media without priority set is not included comparisons based on priority.
 +
 +
A common place to find priority implemented is with combat. In the following example, imagine a combat scenario where some sounds of sword thrusts were interrupted by a successful blocking maneuver.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "sword1.wav",
 +
  "type": "sound",
 +
  "tag": "combat",
 +
  "loops": 3,
 +
  "priority": 60
 +
}
 +
 +
Client.Media.Play {
 +
  "name": "block1.wav",
 +
  "type": "sound",
 +
  "tag": "combat",
 +
  "priority": 70
 +
}
 +
</syntaxhighlight>
 +
 +
Although using the integer type is recommended, Mudlet parses "priority" values of type string ("25") into integers (25) as needed.
 +
 +
The "priority" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.
 +
 +
====continue: true or false (for music)====
 +
 +
Typically sent with the "type" of "music" is the parameter "continue", which presents the following values:
 +
 +
*true: continues music that is already playing if it is requested again through a new Client.Media.Play GMCP event
 +
*false: restarts music that is already playing if it is requested again through a new Client.Media.Play GMCP event
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "city.mp3",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "continue": true
 +
}
 +
</syntaxhighlight>
 +
 +
If the "continue" parameter is not specified, behavior will default to ''true'' for music.
 +
 +
Although using the boolean type is recommended, Mudlet parses "continue" values of type string ("true" and "false") into boolean (''true'' and ''false'') as needed, as not all game drivers support a boolean type.
 +
 +
==== key====
 +
 +
The "key" parameter enables media categorization, similar to the "tag" parameter, however it adds a feature of uniqueness that halts media currently playing with the same key, replacing it with the media that arrived from a new Client.Media.Play GMCP event. This update will only occur if the "name" or "url" associated with the currently playing media does not match the new media, while the key is identical.
 +
 +
In the example below, consider that a player moves from a sewer area to a village.  The "key" parameter is used to stop the previously playing ''sewer.mp3'' and start playing ''village.mp3'' within the second Client.Media.Play GMCP event.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "sewer.mp3",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "loops": -1,
 +
  "continue": true,
 +
  "key": "area-music"
 +
}
 +
 +
Client.Media.Play {
 +
  "name": "village.mp3",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "loops": -1,
 +
  "continue": true,
 +
  "key": "area-music"
 +
}
 +
</syntaxhighlight>
 +
 +
The "key" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.
 +
 +
{{note}} Where music is bound to change, continue or stop as a player enters a new room, consider allowing for a slight pause of 1 heart beat or tick to send the Client.Media.Play or Client.Media.Stop GMCP events. This practice reduces the number of Client.Media.Play GMCP events sent when the player travels through several rooms quickly.
 +
 +
==== randomization====
 +
 +
Wildcards ''*'' (asterisk) and ''?'' (question mark) may be used within the name to randomize media file(s) selection.
 +
 +
Given that media files underdark.wav, underdark.mp3 and underdark_dark_elven_moshpit.mp3 were all present in the media/environment directory under the current game profile, the following Client.Media.Play GMCP event would randomly choose one of those files to play using the ''*'' wildcard.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "underdark*",
 +
  "type": "music",
 +
  "tag": "environment",
 +
  "loops": -1,
 +
  "continue": false
 +
}
 +
</syntaxhighlight>
 +
 +
If media files sword1.wav, sword2.wav and sword3.wav were all present in the media/combat directory under the current game profile, the following Client.Media.Play GMCP event would randomly choose one of those files to play per each loop using the ''?'' wildcard.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Play {
 +
  "name": "combat/sword?.wav",
 +
  "type": "sound",
 +
  "tag": "combat",
 +
  "loops": 3
 +
}
 +
</syntaxhighlight>
 +
 +
=== Stopping Media ===
 +
 +
Send '''Client.Media.Stop''' GMCP events to stop sound or music media.
 +
 +
{| class="wikitable"
 +
!Required
 +
!Key
 +
!Value
 +
! style="text-align:left;" |Purpose
 +
|-
 +
| style="text-align:center;" | No
 +
| "name"
 +
|<file name>
 +
| style="text-align:left;" |
 +
*Stops playing media by name matching the value specified.
 +
|-
 +
| style="text-align:center;" | No
 +
|"type"
 +
|"sound" or "music"
 +
| style="text-align:left;" |
 +
*Stops playing media by type matching the value specified.
 +
|-
 +
| style="text-align:center;" |No
 +
| "tag"
 +
|<tag>
 +
| style="text-align:left;" |
 +
*Stops playing media by tag matching the value specified.
 +
|-
 +
| style="text-align:center;" |No
 +
|"priority"
 +
|1 to 100
 +
| style="text-align:left;" |
 +
*Stops playing media with priority less than or equal to the value.
 +
|-
 +
| style="text-align:center;" |No
 +
|"key"
 +
|<key>
 +
| style="text-align:left;" |
 +
*Stops playing media by key matching the value specified.
 +
|-
 +
| style="text-align:center;"| No
 +
| "fadeaway"
 +
| true or false
 +
| style="text-align:left;" |
 +
* Decrease volume from the current position for a given duration, then stops the track.
 +
* Given duration is the lesser of the remaining track duration or the fadeout specified in Client.Media.Play.
 +
* If fadeout was not specified in Client.Media.Play, then the optional fadeout parameter from Client.Media.Stop or a default of 5000 milliseconds will be applied.
 +
|-
 +
| style="text-align:center;"| No
 +
| "fadeout"
 +
|
 +
| style="text-align:left;" |
 +
* Default duration in milliseconds to decrease volume to the end of the track.
 +
* Only used if fadeout was not defined in Client.Media.Play.
 +
|-
 +
|}
 +
 +
====all====
 +
 +
Send the Client.Media.Stop GMCP event with no parameters to stop all playing media within a given game profile.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {}
 +
</syntaxhighlight>
 +
 +
====parameters====
 +
 +
Send any combination of the name, type, tag, priority or sound parameters and valid corresponding values to stop playing media that matches ''all'' combinations of the parameters.
 +
 +
Stop the rain.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "name": "rain.wav"
 +
}
 +
</syntaxhighlight>
 +
 +
Stop all media using the "type" of "sound".
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "type": "sound"
 +
}
 +
</syntaxhighlight>
 +
 +
Stop all media categorized with the "tag" of "combat" ''and'' that has a "priority" less than or equal to 50.
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "tag": "combat",
 +
  "priority": 50
 +
}
 +
</syntaxhighlight>
 +
 +
Stop all media categorized with the "key" of "area-music".
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "key": "area-music"
 +
}
 +
</syntaxhighlight>
 +
 +
Decrease volume for 5 seconds (or the fadeout parameter defined in Client.Media.Play) and then stop all playing music files for this profile
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "fadeaway": true
 +
}
 +
</syntaxhighlight>
 +
 +
Decrease volume for 10 seconds all media categorized with the "key" of "area-music" and then stop the music
 +
 +
<syntaxhighlight lang="json">
 +
Client.Media.Stop {
 +
  "key": "area-music",
 +
  "fadeaway": true,
 +
  "fadeout": 10000
 +
}
 +
</syntaxhighlight>
  
 
=GUI Scripting in Mudlet=
 
=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.
+
Mudlet has extremely powerful GUI capabilities built into it, and with the inclusion of the Geyser and Vyzors layout managers in Mudlet's Lua API it is quicker and easier to get a basic UI created.
 +
 
 +
==Geyser==
 +
Geyser is an object oriented framework for creating, updating and organizing GUI elements within Mudlet - it allows you to make your UI easier on Mudlet, and makes it easier for the UI to be compatible with different screen sizes.
 +
 
 +
Geyser's manual is quite extensive - [[Manual:Geyser|follow here]] to read up on it.
 +
 
 +
===UI template===
 +
Akaya has created a UI template based on Geyser that can serve as a quick start for customizing the Mudlet interface to your character:
 +
 
 +
[[File:Geyser UI template.png|center|800px|border]]
 +
 
 +
To get started with it, see the forum thread about [http://forums.mudlet.org/viewtopic.php?f=6&t=4098 Geyser UI Template].
 +
 
 +
==Simple Window Manager==
 +
Simple Window Manager is a light-weight script designed to provide the GUI object placement and sizing functionality seen in Geyser and Vyzor without any of the extras. Any GUI object, once created normally, can be added to the SWM along with a corner to base its positioning info off of, the distance from that corner, and the size of the object in any combination of pixels and percentages of the screen. SWM does all the behind the scenes work to keep all objects it has been given to manage in their correct places and sizes as the screen changes size. All other interactions with GUI objects are handled using standard functions built into Mudlet.
 +
 
 +
Simple Window Managers manual, and download, can be [http://forums.mudlet.org/viewtopic.php?f=6&t=3529 found here].
  
 
==Useful Resources==
 
==Useful Resources==
 
There are several useful resources you can refer to when creating your GUI.
 
There are several useful resources you can refer to when creating your GUI.
<br/>
+
<br />
  
 
;Wiki Resources
 
;Wiki Resources
* [[Manual:Geyser|Geyser Tutorial]]:
+
*[[Manual:Geyser|Geyser Tutorial]]:
: Excellent for getting an initial feel of how to use the Geyser layout manager.
+
:Excellent for getting an initial feel of how to use the Geyser layout manager.
* [[Manual:Lua_Functions#UI_Functions|GUI API]]:
+
*[[Manual:Lua_Functions#UI_Functions|GUI API]]:
: The GUI section of the Lua API manual
+
:The GUI section of the Lua API manual
 
;External Resources
 
;External Resources
* [http://forums.mudlet.org/viewforum.php?f=11 Geyser subforum]:
+
*[http://forums.mudlet.org/viewforum.php?f=11 Geyser subforum]:
: The Geyser Layout Manager subforum on Mudlet's forums.
+
:The Geyser Layout Manager subforum on Mudlet's forums.
* [http://forums.mudlet.org/viewtopic.php?f=5&t=1376 GUI Screenshots]:
+
*[http://forums.mudlet.org/viewtopic.php?f=5&t=1376 GUI Screenshots]:
: A pinned thread on Mudlet's forums for showing what your GUI looks like. Useful for ideas and to see what is possible.
+
:A pinned thread on Mudlet's forums for showing what your GUI looks like. Useful for ideas and to see what is possible.
* [http://forums.mudlet.org/viewtopic.php?f=9&t=1974 Godwars II Mudlet UI]:
+
*[http://forums.mudlet.org/viewtopic.php?f=9&t=1974 Godwars II Mudlet UI]:
: A forum thread which follows the evolution of the UI provided by [http://www.godwars2.org God Wars II]
+
:A forum thread which follows the evolution of the UI provided by [http://www.godwars2.org God Wars II]
 
[[Category:Mudlet Manual]]
 
[[Category:Mudlet Manual]]
 +
 +
==Resetting your UI==
 +
While scripting your UI, you can reset it completely and have your scripts be loaded from scratch with the [[Manual:Miscellaneous_Functions#resetProfile|resetProfile()]] function - in the input line, type <code>lua resetProfile()</code>.
 +
 +
 +
{{note}} Developer tip:  Make a keybinding for resetting your profile while you develop your GUI so you don't have to type <code>lua resetProfile()</code> command every time you want to see the changes (e.g. CTRL+SHIFT+R)

Latest revision as of 07:50, 2 December 2024

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.

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

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

Lua interface functions - triggers, timers etc.

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

 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 )

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:

  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

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

setBgColorAll("Tom", 255,50,50)

Since Mudlet 4.11+ it is possible to use name capture groups. You can verify if feature availability by checking mudlet.supports.namedPatterns flag. Let's say you have multiple patterns that you want to handle with one code.

^My name is (?<name>\w+)\. My class is (?<class>\w+)\.

^I am (?<class\w+)\ and my name is (?<name>\w+)\.

Notice that class and name come in different order, therefore in first case we will have name under matches[2] and in second case under matches[3]. With named capture groups in both cases we will have name available under matches.name and class under matches.class.

Code example to handle named matches above:

  echo("Name: " .. matches.name .. "\n")
  echo("Class: " .. matches.class .. "\n")

Sending commands or printing information messages

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:

send( "say " .. matches[4] .." " .. matches[2] .." ".. matches[3] )

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 and formatting text from the MUD

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:

selectString("Tom",1)
replace("Betty")

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

selectString( "Tom", 2 )
replace( "Betty" )

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

replaceAll( "Betty" )

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:

selectString("ugly monster", 1 )
setFgColor(255,0,0)
setBgColor(255,255,255)
resetFormat()

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.

selectSection( 28, 3 )

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.

selectCaptureGroup( number )

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

deleteLine()

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

selectString( "Tom", 1 )
replace( "" )

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

Cursor Movement and Cursor Placement

Mudlet allows you to insert text arbitrarily within the buffer - in the previous line, in the previous ten lines, or even the first line that you've ever saw. These are the tools to help you with the job:

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 Note: This function uses absolute line numbers, not relative ones like in moveCursor().

moveCursor() returns true or false depending on whether the move was possible and done.

Inserting text in the middle of the line

Here's an example where we'll append the text (one) right after the word exit in the line of You see a single exit leading west.

Inserting text.png

The pattern we used was an exact match for You see a single exit leading west. and the code is:

if moveCursor(string.find(line, "exit")+#("exit")-1, getLineNumber()) then
  insertText("(one)")
end

What happened here:

  • we triggered on the desired line using our pattern above
  • we used moveCursor() to place the cursor in the desired position within the line. moveCursor() takes both the line number and the position within the line to place our virtual cursor on, so
    • we used string.find() to find the position of the word "exit" within the line
    • but string.find() returns us the position of the first character of the word we want, while we want to have the text be right after it - so we find out how long our word is with #("exit"), add that to the number returned by string.find() which gets us the first character + the length of the word - which isn't quite at the end of the word, but the word plus the space (because of string.find() and the first character) - so we substract 1 to move our cursor right at the end of the word
    • we then use getLineNumber() to get the current line number, so our cursor is placed on the current line, at our desired position
  • lastly we finally insert the text with insertText(), which puts the text exactly where our virtual cursor is

Done! You can also use cinsertText() for coloured inserts, and other functions mentioned above and in the API manual to help you do what you'd like.

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. Check out Geyser.UserWindow on how to add them!

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

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:

myage = tonumber(matches[2])

How to highlight my current target?

You can put the following script into your targetting alias:

if id then killTrigger(id) end
id = tempTrigger(target, function() selectString(target, 1) fg("gold") deselect() resetFormat() end)

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:

target = target:title()
if id then killTrigger(id) end
id = tempTrigger(target, function() selectString(target, 1) fg("gold") deselect() resetFormat() end)

Note Note: Ensure you're on Mudlet 3.5.0+ so you can put function() in a tempTrigger().

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:

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

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!

How can I make a key that toggles between commands?

To make a key that toggles between two actions, for example - sleep/wake, you can use this as the script:

if not sleeping then
  send("sleep")
  sleeping = true
else
  send("wake")
  sleeping = false
end

How can I make a trigger with a next-line condition?

Here's how you can set it up - replace line1 and line2 with appropriate lines.

Trigger multiline not another example.png

Here's the Mudlet XML you can download for this!

How to target self or something else?

Buffselforother.png

Using regex and an 'or' statement allows us to consolidate multiple aliases, or an if statement, into a single more simple alias. When a regex is found it will use it, but if you don't specify one then the alternative will be used. The use of 'or' can be used with any function in Mudlet. In this example, one can type "buff" to buff themself, or "buff ally" to buff their ally instead.

^buff(?: (\w+))?$

send("cast buff on "..(matches[2] or "myself"))

How to delete everything between two lines?

When deleteLine() is called, every following line shifts back in numerical position. Thus if we know where we're starting and how many lines to expect, we can use a for loop to gag everything between line a and b by leaving the cursor in one spot. In the below example, line_a_number is the number of the line you want to start at, and line_b_number is the ending line. To gag x number of lines instead, use line_a_number + x in place of line_b_number.

for i = line_a_number, line_b_number, 1
do
   moveCursor(0, line_a_number)
   selectCurrentLine()
   deleteLine()
end

How to capture foreground line color information?

Here's an example that loops through each character in a line of text and prints the character and its foreground color to a miniconsole if the color has changed from the previous character:

Trigger: ^ (regex)

Code:

miniconsoleContainer =
  miniconsoleContainer or Adjustable.Container:new({name = "miniconsoleContainer"})
myMiniconsole =
  Geyser.MiniConsole:new(
    {
      name = "myMiniconsole",
      x = 0,
      y = 0,
      autoWrap = true,
      color = "black",
      scrollBar = false,
      fontSize = 8,
      width = "100%",
      height = "100%",
    },
    miniconsoleContainer
  )

local index = 0
local line = getCurrentLine()
local linesize = #line
local currentlinenumber = getLineNumber()
local r, g, b = 0, 0, 0
local cr, cg, cb -- last seen colors
while index < linesize do
  index = index + 1
  moveCursor("main", index, currentlinenumber)
  selectString(line:sub(index), 1)
  r, g, b = getFgColor()
  if cr ~= r or cg ~= g or cb ~= b then
    cr, cg, cb = r, g, b
    myMiniconsole:echo(string.format("rgb(%d,%d,%d)%s", r, g, b, line:sub(index, index)))
  else
    myMiniconsole:echo(string.format("%s", line:sub(index, index)))
  end
  line:sub(index, index)
end
myMiniconsole:echo("\n")

How to enter a carriage return / enter command?

Say you'd like to gag the following line, and just continue ...

[ Press Enter to continue ]

Trigger: [ Press Enter to continue ]  (set to exact match)
Code: deleteLine()
      send("\n")

How to repeat commands with #?

Install the following alias, and with it #5 mycommand will get repeated five times.

How to move the command line over?

See the first example in the setCmdLineStyleSheet function. Read the note attached to the function description.

How to add a background for Mudlets buttons?

You can give Mudlets buttons a background to have them blend in with your UI by doing this:

-- replace the /home/vadi/Pictures/tile.png location below with the one of your picture!
-- you can download the sample tile.png from https://opengameart.org/content/bevouliin-free-game-background-for-game-developers
local backgroundpath = [[/home/vadi/Pictures/tile.png]]
-- Windows:
-- local backgroundpath = [[C:/Users/Vadim/Downloads/Free game background/transparent PNG/tile.png]]

setAppStyleSheet([[
QToolBar {
  border-image: url(]]..backgroundpath..[[);

}

QToolBar QToolButton:!hover {
  color: white;
}
QToolBar QToolButton:hover {
  color: black;
}
]])

Result:

Mudlet tiled buttons.png


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:

tempTimer(0, [[mycode]])

How to delete the previous and current lines

This little snippet comes from Iocun:

moveCursor(0,getLineCount()-1)
deleteLine()
moveCursor(0,getLineCount())
deleteLine()

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.

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, this would mean that you’d define what goes into each column. A simple example:

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

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.

Note Note: You may not create a column or field name which begins with an underscore. This is strictly reserved to the db package for special use.

Note Note: Adding new columns to an existing database did not work in Mudlet 2.1 - see db:create() on how to deal with this.


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:

  local mydb = db:get_database("people")

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.

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

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

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

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

  db:fetch(mydb.friends)

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:

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

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

The following filter operations are defined:

  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 *

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:

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

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:

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

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:

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

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:

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

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:

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

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:

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

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 is an 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:

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

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

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

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:

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

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:

db:create("people", {
  friends={"name", "city", "notes"},
  enemies={
    name="",
    city="",
    notes="",
    enemied="",
    kills=0
  }
})

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:

db:create("people", {
  friends={"name", "city", "notes"},
  enemies={
    name="",
    city="",
    notes="",
    enemied="",
    kills=0,
    _index = { "city" },
    _unique = { "name" }
  }
})

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. You can specify more than one key to be unique; in that case they will be checked in an OR condition.

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:

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

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:

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

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:

db:create("people", {
  friends={"name", "city", "notes"},
  enemies={
    name="",
    city="",
    notes="",
    enemied="",
    kills=0,
    _index = { "city" },
    _unique = { "name" },
    _violations = "IGNORE"
  }
})

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:

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

-- observe that we don't provide the killed field here - DB automatically fills it in for us.
db:add(mydb.kills, {name="Drow", area="Undervault"})

results = db:fetch(mydb.kills)
display(results)

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

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

As you can see from this output, the killed fields contains a timestamp-- and that timestamp is stored as an epoch value in the GMT timezone. 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:

results = db:fetch(mydb.kills)
for _, row in ipairs(results) do
  echo("You killed " .. row.name .. " at: " .. (row.killed and row.killed:as_string() or "no time") .."\n")
end

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:

db:delete(mydb.enemies, db:eq(mydb.enemies.city, "Magnagora"))

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:

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

That is equivalent to:

db:delete(mydb.enemies, results[1])

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:

db:delete(mydb.enemies, true)

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:

db:update(mydb.enemies, results[1])

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:

db:set(mydb.friends.notes, "", db:eq(mydb.friends.notes, "Magnagora"))

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:

local mydb = db:get_database("my_database")
mydb:_begin()
mydb:_commit()
mydb:_rollback()
mydb:_end()

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.

Discord Messages

Can you send messages from Mudlet to Discord? Totally. Here is an example:

function sendToDiscord(data)
  local url = "https://discord.com/api/webhooks/1204151397977821184/Cu8ZQQo9R11AyIt3DxpOEeB8MtVMA6_AEfXoZaYxqEAOu_6hwhtA2cRLIw-VIs7py7p8"
  local headers = {["Content-Type"] = "application/json"}
  local json = yajl.to_string(data)
  postHTTP(json, url, headers)
end

local data = {
  content = "Hello, Discord!",
  embeds = {
    {
      title = "This is an embed",
      url = "https://example.com",
      image = {
        url = "https://example.com/image.jpg"
      }
    }
  }
}

sendToDiscord(data)

Sure enough, after starting this script, you can see a message in Discord:

Message to Discord.png

What you just need to do is create a webhook in a channel. Go to channel settings, then create this:

Webhook in Discord.png

Here you can find more documentation on which fields Discord allows: https://discord.com/developers/docs/resources/webhook#execute-webhook

All this is one way though. Mudlet won't listen to messages from Discord. For that you'd probably have to make a Discord bot first.

Discord Rich Presence

Rich Presence Midmud.gif

Discord Rich Presence in Mudlet allows you, the player, to show off the game you're playing and you, the game author, to get your game shown off! Advertise your game for free alongside AAA titles in server player lists :)

To respect the players privacy, this option is disabled by default - enable it by ticking the Discord option for the profile:

Enable Discord.png

Once enabled, a Discord-capable game will show information in your Discord!

To fine-tune the information you'd like to see displayed, go to Settings, Chat tab:

Discord Privacy.png

Implementing

As a game author, you have two ways of adding Discord support to your game: via GMCP (easy) or via Lua.

Adding support via GMCP means that your players don't have to install any Lua scripts which is a nicer experience. To do so, implement the Discord over GMCP specification and get in touch to let us know your game implements GMCP.

GMCP

Here's a quick cheatsheet to implement the GMCP spec:

  1. Create a discord application
  2. Upload game logo for "rich presence invite image"
  3. Upload same game logo in "Rich Presence Assets" with the name of server-icon
  4. Upon receiving "External.Discord.Hello" from the game client, reply with "External.Discord.Info" with applicationid (see screenshot) and "External.Discord.Status"
  5. Send "External.Discord.Status" whenever you need to send data to the client, and fill it with the values have changed since the last status. Discord has some ideas for filling it in.
  6. Done. Mudlet will enable the Discord checkbox upon receiving the applicationid from the game at least once.

Lua

An alternative way to add Discord support as a game author is via Lua - all of Discords functionality can be manipulated from it. Create a discord application and set it with setDiscordApplicationID(), then set the set the rest of the options as necessary:

Credit: Discord

If you're a game player and your game doesn't support Discord, you can still use the functionality to set the detail, status, party and time information as desired.

Mudlet VersionAvailable in Mudlet3.14+

MUD Client Media Protocol

Client.Media

Want to add accessibility and excitement into your game? How about implementing sound and music via GMCP?

The Client.Media family of GMCP packages provide a way for games to send sound and music events. GMCP media events are sent in one direction: from game server to to game client.

Client.Media in Mudlet

Media files may be downloaded manually or automatically if certain conditions are met.

Mudlet VersionAvailable in Mudlet4.4+

Mudlet accepts case-insensitive GMCP for the Client.Media namespace (for example, these are all acceptable: "Client.Media.Play", "client.media.play", "cLiEnT.MeDiA.Play").

Enabling Media

When a new profile opens, Mudlet adds to the Core.Supports.Set GMCP package "Client.Media 1" to signal to servers that it supports processing media files via GMCP:

Core.Supports.Set ["Client.Media 1", ...]

To process Client.Media GMCP data in Mudlet, these boxes in the Settings window of Mudlet must be checked:

  1. The "Enable GMCP" box in the Miscellaneous section.
  2. The "Allow server to download and play media" box in the Game protocols section.

These boxes are checked by default upon the start of a new profile in Mudlet.

Storing Media

Mudlet plays files cached in the "media" folder within a game's profile. To get files into the "media" folder you could perform either of these options:

  1. Copy media files or extract them from an archive (zip)
  2. Automatically download them from external resources such as your game's web server on the Internet.

To enable the second option (Internet), use the "url" parameter made available in the "Client.Media.Default", "Client.Media.Load" and "Client.Media.Play" GMCP packages described below.

Loading Media

Send a Client.Media.Default GMCP event to setup the default URL directory to load sound or music files from external resources. This would typically be done once upon player login.

Required Key Value Purpose
Yes "url" <url>
  • Resource location where the media file may be downloaded.

For example, to setup the default URL directory for your game:

Client.Media.Default {
  "url": "https://www.example.com/sounds/"
}

If downloading from external resources, it is recommended to use Client.Media.Default, set this default URL directory once, and let Mudlet download all of your media files automatically from this one resource location. Otherwise, explicitly set the "url" parameter on all of your Client.Media.Load GMCP and Client.Media.Play GMCP events to ensure that files are downloaded from the intended location(s).

Send Client.Media.Load GMCP events to load sound or music files. This could be done when the player logs into your game or at the beginning of a zone that will need the media.

Required Key Value Purpose
Yes "name" <file name>
  • Name of the media file.
  • May contain directory information (i.e. weather/lightning.wav).
Maybe "url" <url>
  • Resource location where the media file may be downloaded.
  • Only required if a url was not set above with Client.Media.Default.

For example, download the sound of a sword swooshing within the "media" folder:

Client.Media.Load {
  "name": "sword1.wav",
  "url": "https://www.example.com/sounds/"
}

Playing Media

Send Client.Media.Play GMCP events to play sound or music files.

Required Key Value Default Purpose
Yes "name" <file name>  
  • Name of the media file.
  • May contain directory information (i.e. weather/lightning.wav).
  • Wildcards * and ? may be used within the name to randomize media files selection.
Maybe "url" <url>  
  • Resource location where the media file may be downloaded.
  • Only required if the file is to be downloaded remotely and a url was not set above with Client.Media.Default or Client.Media.Load.
No "type" "sound" or "music" "sound"
  • Identifies the type of media.
No "tag" <tag>  
  • Helps categorize media.
No "volume" 1 to 100 50
  • Relative to the volume set on the player's client.
No "fadein" <msec>
  • Volume increases, or fades in, ranged across a linear pattern from one to the volume set with the "volume" key.
  • Start position: Start of media.
  • End position: Start of media plus the number of milliseconds (msec) specified.
  • 1000 milliseconds = 1 second.
No "fadeout" <msec>
  • Volume decreases, or fades out, ranged across a linear pattern from the volume set with the "volume" key to one.
  • Start position: End of the media minus the number of milliseconds (msec) specified.
  • End position: End of the media.
  • 1000 milliseconds = 1 second.
No "start" <msec> 0
  • Begin play at the specified position in milliseconds.
  • 1000 milliseconds = 1 second.
No "finish" <msec> 0
  • End play at the specified position in milliseconds.
  • 1000 milliseconds = 1 second.
No "loops" -1, or >= 1 1
  • Number of iterations that the media plays.
  • A value of -1 allows the sound or music to loop indefinitely.
No "priority" 1 to 100  
  • Halts the play of current or future played media files with a lower priority while this media plays.
No "continue" true or false true
  • Only valid for media files with a "type" of "music".
  • Continues playing matching new music files when true.
  • Restarts matching new music files when false.
No "key" <key>  
  • Uniquely identifies media files with a "key" that is bound to their "name" or "url".
  • Halts the play of current media files with the same "key" that have a different "name" or "url" while this media plays.

name

For example, to simply play the sound of a cow mooing already stored in the "media" folder:

Client.Media.Play {
  "name": "cow.wav"
}

Or some lightning stored in a "weather" sub-folder under the "media" folder.

Client.Media.Play {
  "name": "weather/lightning.wav"
}

The "name" parameter may be used for stopping media with the Client.Media.Stop GMCP event.

url

If you maintain your sound files on the Internet and don't set a default URL with Client.Media.Default, or preload them with Client.Media.Load, include the "url" parameter:

Client.Media.Play {
  "name": "cow.wav",
  "url": "https://www.example.com/sounds/"
}

type: sound

Media files default to a "type" of "sound" when the "type" parameter is not specified, such as in the example above. It is good practice to specify the "type" parameter to best keep media organized within your implementation:

Client.Media.Play {
  "name": "cow.wav",
  "type": "sound"
}

The "type" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.

tag

To play the sound of a sword swooshing and categorized as "combat":

Client.Media.Play {
  "name": "sword1.wav",
  "type": "sound",
  "tag": "combat"
}

The "tag" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.

type: music

Add background music to your environment through use of the "music" value set upon the "type" parameter:

Client.Media.Play {
  "name": "river.wav",
  "type": "music",
  "tag": "environment"
}

The "type" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.

volume: 1 to 100

As the character draws nearer to or farther from the environmental feature, consider adjusting the "volume" parameter. Example values, followed by syntax examples:

  • Maximum: 100 (recommended to use rarely)
  • High: 75
  • Default: 50
  • Low: 25
  • Minimum: 1 (recommended to use rarely)
Client.Media.Play {
  "name": "river.wav",
  "type": "music",
  "tag": "environment",
  "volume": 75
}

Although using the integer type is recommended, Mudlet parses "volume" values of type string ("75") into integers (75) as needed.

fadein

Increase volume from the start of the media [start position, volume one] to the number in milliseconds specified with the "fadein" parameter [position start + milliseconds, volume specified with the "volume" parameter]. Example values, followed by syntax examples:

  • 1000 milliseconds (1 second)
  • 60000 milliseconds (1 minute)
  • 0 milliseconds (no fade in)
Client.Media.Play {
  "name": "monty_python.mp3",
  "type": "music",
  "volume": 100,
  "fadein": 1000
}
        +---------------------------+-----------------------+
        | Fade in 1000 milliseconds |         Normal        |
        +---------------------------+-----------------------+
Volume 1|........::::::::::iiiiiiiii%%%%%%%%%%%%%%%%%%%%%%%%|Volume 100
        +---------------------------+-----------------------+

Although using the integer type is recommended, Mudlet parses "fadein" values of type string ("1000") into integers (1000) as needed.

fadeout

Decrease volume from a position in milliseconds subtracted from the duration of the media [position duration - milliseconds, volume specified with the "volume" parameter] to the end of the media [position duration, volume one]. Example values, followed by syntax examples:

  • 1000 milliseconds (1 second)
  • 60000 milliseconds (1 minute)
  • 0 milliseconds (no fade out)
Client.Media.Play {
  "name": "the_matrix.mp3",
  "type": "music",
  "volume": 50,
  "fadeout": 333
}
         +-----------------------+---------------------------+
         |        Normal         | Fade out 333 milliseconds |
         +-----------------------+---------------------------+
Volume 50|%%%%%%%%%%%%%%%%%%%%%%%%iiiiiiiii::::::::::........|Volume 1
         +-----------------------+---------------------------+

Although using the integer type is recommended, Mudlet parses "fadeout" values of type string ("333") into integers (333) as needed.

start

Start playing a media track at the number of milliseconds specified. Example values, followed by syntax examples:

  • 1000 milliseconds (1 second)
  • 60000 milliseconds (1 minute)
  • 0 milliseconds (start from beginning)
Client.Media.Play {
  "name": "mudlet_beatboxing.mp3",
  "type": "music",
  "volume": 75,
  "start": 1000
}

Although using the integer type is recommended, Mudlet parses "start" values of type string ("100") into integers (100) as needed.

finish

Finish playing a media track at the number of milliseconds specified. Example values, followed by syntax examples:

  • 1000 milliseconds (1 second)
  • 60000 milliseconds (1 minute)
  • 0 milliseconds (start from beginning)
Client.Media.Play {
  "name": "mudlet_beatboxing.mp3",
  "type": "music",
  "volume": 75,
  "finish": 35000
}

Although using the integer type is recommended, Mudlet parses "finish" values of type string ("100") into integers (100) as needed.

loops: -1, 1 or more

The "loops" parameter controls how many times the sound repeats and defaults to 1 if not specified. A value of -1 will loop the file indefinitely.

Client.Media.Play {
  "name": "clock/hour_bell.wav",
  "type": "sound",
  "loops": 3
}

Client.Media.Play {
  "name": "underdark.mp3",
  "type": "music",
  "tag": "environment",
  "loops": -1
}

Although using the integer type is recommended, Mudlet parses "loops" values of type string ("-1") into integers (-1) as needed.

priority: 1 to 100

The "priority" parameter sets precedence for Client.Media.Play GMCP events that include a "priority" setting. The values for the "priority" parameter range between 1 and 100. Given that two Client.Media.Play GMCP events have the same priority, the media already playing will continue playing. In Mudlet, media without priority set is not included comparisons based on priority.

A common place to find priority implemented is with combat. In the following example, imagine a combat scenario where some sounds of sword thrusts were interrupted by a successful blocking maneuver.

Client.Media.Play {
  "name": "sword1.wav",
  "type": "sound",
  "tag": "combat",
  "loops": 3,
  "priority": 60
}

Client.Media.Play {
  "name": "block1.wav",
  "type": "sound",
  "tag": "combat",
  "priority": 70
}

Although using the integer type is recommended, Mudlet parses "priority" values of type string ("25") into integers (25) as needed.

The "priority" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.

continue: true or false (for music)

Typically sent with the "type" of "music" is the parameter "continue", which presents the following values:

  • true: continues music that is already playing if it is requested again through a new Client.Media.Play GMCP event
  • false: restarts music that is already playing if it is requested again through a new Client.Media.Play GMCP event
Client.Media.Play {
  "name": "city.mp3",
  "type": "music",
  "tag": "environment",
  "continue": true
}

If the "continue" parameter is not specified, behavior will default to true for music.

Although using the boolean type is recommended, Mudlet parses "continue" values of type string ("true" and "false") into boolean (true and false) as needed, as not all game drivers support a boolean type.

key

The "key" parameter enables media categorization, similar to the "tag" parameter, however it adds a feature of uniqueness that halts media currently playing with the same key, replacing it with the media that arrived from a new Client.Media.Play GMCP event. This update will only occur if the "name" or "url" associated with the currently playing media does not match the new media, while the key is identical.

In the example below, consider that a player moves from a sewer area to a village. The "key" parameter is used to stop the previously playing sewer.mp3 and start playing village.mp3 within the second Client.Media.Play GMCP event.

Client.Media.Play {
  "name": "sewer.mp3",
  "type": "music",
  "tag": "environment",
  "loops": -1,
  "continue": true,
  "key": "area-music"
}

Client.Media.Play {
  "name": "village.mp3",
  "type": "music",
  "tag": "environment",
  "loops": -1,
  "continue": true,
  "key": "area-music"
}

The "key" parameter may be used for stopping matching media with the Client.Media.Stop GMCP event.

Note Note: Where music is bound to change, continue or stop as a player enters a new room, consider allowing for a slight pause of 1 heart beat or tick to send the Client.Media.Play or Client.Media.Stop GMCP events. This practice reduces the number of Client.Media.Play GMCP events sent when the player travels through several rooms quickly.

randomization

Wildcards * (asterisk) and ? (question mark) may be used within the name to randomize media file(s) selection.

Given that media files underdark.wav, underdark.mp3 and underdark_dark_elven_moshpit.mp3 were all present in the media/environment directory under the current game profile, the following Client.Media.Play GMCP event would randomly choose one of those files to play using the * wildcard.

Client.Media.Play {
  "name": "underdark*",
  "type": "music",
  "tag": "environment",
  "loops": -1,
  "continue": false
}

If media files sword1.wav, sword2.wav and sword3.wav were all present in the media/combat directory under the current game profile, the following Client.Media.Play GMCP event would randomly choose one of those files to play per each loop using the ? wildcard.

Client.Media.Play {
  "name": "combat/sword?.wav",
  "type": "sound",
  "tag": "combat",
  "loops": 3
}

Stopping Media

Send Client.Media.Stop GMCP events to stop sound or music media.

Required Key Value Purpose
No "name" <file name>
  • Stops playing media by name matching the value specified.
No "type" "sound" or "music"
  • Stops playing media by type matching the value specified.
No "tag" <tag>
  • Stops playing media by tag matching the value specified.
No "priority" 1 to 100
  • Stops playing media with priority less than or equal to the value.
No "key" <key>
  • Stops playing media by key matching the value specified.
No "fadeaway" true or false
  • Decrease volume from the current position for a given duration, then stops the track.
  • Given duration is the lesser of the remaining track duration or the fadeout specified in Client.Media.Play.
  • If fadeout was not specified in Client.Media.Play, then the optional fadeout parameter from Client.Media.Stop or a default of 5000 milliseconds will be applied.
No "fadeout"
  • Default duration in milliseconds to decrease volume to the end of the track.
  • Only used if fadeout was not defined in Client.Media.Play.

all

Send the Client.Media.Stop GMCP event with no parameters to stop all playing media within a given game profile.

Client.Media.Stop {}

parameters

Send any combination of the name, type, tag, priority or sound parameters and valid corresponding values to stop playing media that matches all combinations of the parameters.

Stop the rain.

Client.Media.Stop {
  "name": "rain.wav"
}

Stop all media using the "type" of "sound".

Client.Media.Stop {
  "type": "sound"
}

Stop all media categorized with the "tag" of "combat" and that has a "priority" less than or equal to 50.

Client.Media.Stop {
  "tag": "combat",
  "priority": 50
}

Stop all media categorized with the "key" of "area-music".

Client.Media.Stop {
  "key": "area-music"
}

Decrease volume for 5 seconds (or the fadeout parameter defined in Client.Media.Play) and then stop all playing music files for this profile

Client.Media.Stop {
  "fadeaway": true
}

Decrease volume for 10 seconds all media categorized with the "key" of "area-music" and then stop the music

Client.Media.Stop {
  "key": "area-music",
  "fadeaway": true,
  "fadeout": 10000
}

GUI Scripting in Mudlet

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

Geyser

Geyser is an object oriented framework for creating, updating and organizing GUI elements within Mudlet - it allows you to make your UI easier on Mudlet, and makes it easier for the UI to be compatible with different screen sizes.

Geyser's manual is quite extensive - follow here to read up on it.

UI template

Akaya has created a UI template based on Geyser that can serve as a quick start for customizing the Mudlet interface to your character:

Geyser UI template.png

To get started with it, see the forum thread about Geyser UI Template.

Simple Window Manager

Simple Window Manager is a light-weight script designed to provide the GUI object placement and sizing functionality seen in Geyser and Vyzor without any of the extras. Any GUI object, once created normally, can be added to the SWM along with a corner to base its positioning info off of, the distance from that corner, and the size of the object in any combination of pixels and percentages of the screen. SWM does all the behind the scenes work to keep all objects it has been given to manage in their correct places and sizes as the screen changes size. All other interactions with GUI objects are handled using standard functions built into Mudlet.

Simple Window Managers manual, and download, can be found here.

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

Resetting your UI

While scripting your UI, you can reset it completely and have your scripts be loaded from scratch with the resetProfile() function - in the input line, type lua resetProfile().


Note Note: Developer tip: Make a keybinding for resetting your profile while you develop your GUI so you don't have to type lua resetProfile() command every time you want to see the changes (e.g. CTRL+SHIFT+R)