Difference between revisions of "Manual:Technical Manual"
Line 27: | Line 27: | ||
{{Manual:Using Variables in Mudlet}} | {{Manual:Using Variables in Mudlet}} | ||
− | {{Manual: | + | {{Manual:Alias Engine}} |
{{Manual:Trigger Engine}} | {{Manual:Trigger Engine}} |
Revision as of 05:16, 17 January 2012
Multi Session Gaming
Mudlet lets you play several simultaneous MUD sessions. However, currently we have the restriction that you cannot use the same profile twice. Consequently, if you want to play three characters on the same MUD at the same time, you need to make 3 profiles with 3 different names e.g. [email protected], [email protected] etc.
Split Screen
Mudlet has a split screen. If you scroll up in the MUD text screen (or any other mini console window), the screen will split in two parts. The lower part will follow the MUD output while the upper part will stay focused on your scrolling. This way you can read easier through old passages without missing what is going on at the moment.
Split screen can be activated via the scroll bar, page up / page down keys or the mouse wheel. Scrolling back to the end will close the split screen automatically. A click on the middle mouse button will close the split screen immediately as well as pressing control+return on the keyboard (command+return for mac). The size of the 2 parts of the split screen can be modified by dragging the separator bar in the middle with the mouse. Split screen usage is necessary when selecting text in order to copy it to trigger editor e.g. when making triggers. If you don’t use split screen when selecting text, new arriving text will upset your selection.
Command Line Auto-Completion, Tab-Completion and Command History
Mudlets command line is especially designed for MUD and MUSH playing. It aims at reducing the amount of typing by using autocompletion and tab completion schemes that are optimized for typical MUD playing situations. The command line offers tab completion (TAB key with or without shift) and autocompletion (cursor up and cursor down keys).
Tab completion searches the last 100 lines in the MUD output buffer for words matching the word that you are currently typing. This is useful if you have to type complicated long names. You only have to type the first letters and then press the tab key until the proposal is what you want.
Autocompletion tries to match your current typing with your command history. Example: If you typed the same command a while ago, you only have to start typing the first couple of letters and then press the cursor up key until the proposal matches the command that you want to use.
Command history - if you haven’t started to type anything yet, you can browse through your command history with the cursor up and cursor down keys. However, if you have started typing pressing cursor up will result in a shot at autocompletion.
Escape key - to get out of autocompletion you can press the ESC key any time. After pressing ESC the entire text gets selected and you can overwrite it or get back into command history mode.
Manual:Logging Output to text or HTML Log Files
Using Variables in Mudlet
One of the major design goals in Mudlet was to integrate scripts and variables into triggers/aliases/buttons etc. as seamlessly as possible. The usage of variables in Mudlet is distinctly different from the other major clients, as native Lua scripting has been integrated into Mudlet from the ground up. As scripts are so closely intertwined with the core of Mudlet, variables do not need any special treatment as in other clients i.e. there is no need for code such as:
totalKills = getVariable("killedMonsters") + getVariable("killedVillains")
echo( "kills=" .. totalKills )
In Mudlet, the above code translates into:
totalKills = killedMonsters + killedVillains
echo( "kills=" .. totalKills )
If you define a variable in any given script in Mudlet, be it a trigger, a button, an alias key, an event handler, a free function, etc. It can be used from within any context without any special getVariable() type of function or any special variable symbols, such as @myVar etc.. In Mudlet all variables are native Lua variables. Each session (= profile/connection tab) runs in its own dedicated Lua interpreter. Consequently, all scripts of a profile are compiled into the same Lua interpreter and thus their code runs in the same variable space. All scripts from another simultaneously opened profile will not interfere because each profile uses its own Lua interpreter. Note Everything shares the same variables & functions.
To give you an example: Let’s make a little trigger that counts how many monsters you have killed. Each time you have made a new kill, the trigger matches on the kill message and runs a little script that increases the amount of kills by one each time the trigger fires - in other words, each time you kill a monster and the kill message is sent from the MUD. For the kill counter we declare a variable and call it myKills. This variable is going to hold the number of kills we’ve made so far. The script will look like this:
myKills = myKills + 1
Then we add a little alias, a button or a keybindings that executes another little script that prints your current kill statistics on the screen. The attached script would look like this:
echo( "I have killed " .. myKills .. " monsters today." )
Lua variables can be either a string of letters (aka string
) or a number
, among a few others. They are whatever there were initially created with or what data type they can be converted to.
a = "Jim"
b = "Tom"
c = 350
d = 1
--Then you can write:
e = c + d -- and e will equal 351
e = a .. b -- and e will equal "JimTom"
e = a .. c -- and e will equal "Jim350"
Note: You can't use a + b to concatenate string values. For this you must use the double-dot: ..
String Interpolation - the f
function
However, starting with Mudlet 4.11.0 we have included the f function to perform string interpolation. This is a very fancy way of saying "replacing code in strings with their value". When you run a string through the f function then it replaces anything inside a pair of {} with the value of the variable or outcome of the expression. To expand upon the example above:
a = "Jim"
b = "Tom"
c = 350
d = 1
function simple_addition(a,b)
return a+b
end
--Then you can write:
e = c + d -- and e will equal 351 (no change here)
e = f"{c + d}" -- end e will equal "351". This is a string, not a number, if you need to do math with e then do it like above.
e = f"{simple_addition(c,d)}" -- same as above, but shows using the function we've defined.
e = f("{a}{b}") -- and e will equal "JimTom"
e = f"{a}{c}" -- and e will equal "Jim350"
A shrewd observer might notice that I have written the above uses of f without the usual parentheses. In Lua you can omit the () when you are only passing a single string argument to the function. It is usually considered best practice to include the () but in the case of f for interpolation I find it reads a bit better without them. As shown in the example above it works with or without them in place.
For a more 'real world' example and comparison of concatenation, f, and string.format consider the following:
-- For the following, we assume the following variables exist:
-- target which holds the target's name
-- target_health_color which is a cecho color determined elsewhere
-- target_health which is the actual health the target has.
-- this function uses the concatenation operator .. to glue strings together for display
function displayTarget()
local msg = "\n<green>Target:<r> " .. target .. " <green>Health:<r> " .. target_health_color .. target_health
cecho(msg)
end
-- this function does the same thing, but uses f
function displayTarget()
local msg = f"\n<green>Target:<r> {target} <green>Health:<r> {target_health_color}{target_health}"
cecho(msg)
end
Tables as Lists
There is another form of variables in Lua called tables which can be used for lists, arrays or dictionaries. This is explained later. For an in-depth coverage of variables in Lua take a look at a Lua tutorial e. g. this one on numbers http://Lua-users.org/wiki/NumbersTutorial and this one on strings http://Lua-users.org/wiki/StringsTutorial or this one on Lua tables http://Lua-users.org/wiki/TablesTutorial
Let’s get back to our example. The trigger script expects myKills to be a number so you have to initialze the variable myKills to 0 before running the script for the first time. The best place to initialize variables is in script script outside of a function definition as this code is executed when the session is loaded or if you compile the script again after you have edited it. To do this click on the "Scripts" icon and select the standard script "My Global Variable Definitions". If you are using an older version of Mudlet or a profile that doesn’t have this script item, simply click on the "Add" icon and make your own script. You can call it whatever you want to. Add following code to initialize your new variable myKills to a number and set its value to 0:
myKills = 0
Whenever you edit this script, it will be recompiled and the code will be run as it is not part of a function definition. This is the major difference between trigger-scripts, alias-scripts etc. and script-scripts. Script-scripts can contain an unlimited amount of function definitions and also free code i. e. code outside of a function definition - code that is not enclosed by function xyz() …. end. On saving the script the script gets compiled and the free code is run instantly. All other item scripts, i. e. trigger-scripts etc., secretly define a function name for the script and thus the script is not free code, but function code. When a trigger fires Mudlet calls this invisible function name to run the script e.g. trigger738(), button82(), alias8(). This means that if you define variables inside a trigger script the variable will not be defined before the trigger runs for the first time. However, if you define this variable as free code in a script-script the definition becomes available immediately on script save. Now, whenever you add new variables to your variable definition script, the script gets run and your old variables will be reinitialized and reset to 0. This will be no big problem in most cases as you won’t work on your systems while really playing in most cases. To solve this problem you have two options:
First option: Add a script group (a folder) and add a new script item for each variable you define. This way, editing a variable definition will only reset the edited variable and none of the others that are defined in different scripts. This has the added advantage that you have a nice graphical overview of your defined variables. Note Organize your variables
Second option (more advanced): Change the variable initialization to only initialize the variable if it hasn’t been initialized before, thus keeping the values of previously defined variables. The following code initializes the variable myKills (to the number 0) but only if it hasn't been initialized before. This would look like this:
if myKills == nil then
myKills = 0
end
In Lua all undefined variables are initialized to the value nil. The value nil is not the same thing as the number 0 or the empty string "". What it means is that a variable that has the value nil has not been declared yet and does not exist. If a variable does not exist, it cannot be used as its value is undefined at this point. Consequently, increasing the value of nil by one is impossible as the variable doesn’t exist yet and will lead to a Lua error. The above script simply checks if the variable myKills has been defined yet and if it hasn’t, it will declare the variable and set its value to 0.
Having variables that hold a single value is the most important usage of variables, but very often you’ll like to define variables that hold a list of values e. g. a list of your enemies, a list the items you are currently carrying etc.. To define a variable to be a list you need to declare it to be a Lua table. Let’s declare the variable myEnemies as a list containing the names of your enemies:
myEnemies = {}
You can now add new enemies to this list by calling the Lua function table.insert( listName, item )
. For example:
table.insert( myEnemies, "Tom" )
table.insert( myEnemies, "Jim" )
To print the contents of your enemy list on the screen, you can run this script:
display( myEnemies )
Now, let’s make a little alias that adds a new name to your enemy list when you type "add enemy" followed by the name of the enemy (e.g., "add enemy Peter"). Open the alias editor by clicking on the "Aliases" icon. Click on the "Add Item" icon to add a new alias. Choose any name you like for your alias (e.g., "Add Enemy Alias"), and then define the following pattern for the alias: ^add enemy (.*)
. Next, add the following little script in the script input field below your alias:
table.insert( myEnemies, matches[2] )
echo( f"Added a new enemy:{ matches[2] }\n" )
Save the alias by clicking on "Save Item" and try it out by entering into the Mudlet's command input line. Aliases are explained in further detail below.
Another way to declare a list is to define its values directly. For example,
myEnemies = { "Peter", "Jim", "Carl", "John" }
To remove an item from the list, you can use the function table.remove( listName, item index )
.
Saving variables
You might notice that Mudlet doesn't save your variables between profiles restarts. The reasons for this are technical, but all it means is that you have flexibility in how to deal with them. There are several ways, so the most common and easiest ones will be explained here.
Tick the box in the variables view
Head to the variables view in the Mudlet editor and tick the box besides a variable - Mudlet will remember it between restarts now!
Saving by coding them in a script
The title sounds a bit complicated, but that's pretty much what you do. Go to Scripts, click `Add Item`, and create your variables like so:
myname = "Bob"
mypack = "pack1234"
rapierID = 67687
Now, since scripts are run when the profile is started, these variables will be created and assigned to those values. This was simple to do, but it has one problem - if you want to change the value of the variables to be saved, you have to edit the script by hand each time; and if you change the value of variables while playing via scripting, the new values won't be recorded.
Saving via table.save & table.load
Next, enter a more proper solution. This'll actually save the variables to a file and load them from it - so if you change the variable values, the current ones will be saved, and will be loaded next time properly. This method works with a table containing your variables though, not individual variables themselves - see Lua tables tutorial on how to create and use those.
table.save(where to save, what table to save)
takes the location and name of a file to save variables to, and a table containing your variables to save. Location can be anywhere on your computer, but a good default place is your profile folder, whose location can be obtained with getMudletHomeDir().
mychar = {
name = "Bob",
age = 26,
sex = "male"
}
local location = getMudletHomeDir() .. "/mychar.lua"
table.save(location, mychar)
-- sample echo to show where the file went:
echo(f"Variables saved in: '{location}'")
table.load(where to load from, what table to use)
is similar - it takes the location and name of the file to load, and a table name to use for the loaded variables.
mychar = mychar or {}
table.load(getMudletHomeDir() .. "/mychar.lua", mychar)
display(mychar)
echo(f"My name is: {tostring(mychar.name)}")
Now that you have a way to save and load your tables, you can create triggers to load and save your variables at appropriate times, and you'll be set.
React on input - Mudlets Alias Engine
Note: A screencast is available on getting started with aliases - watch it!
Mudlet has a feature called "aliases" that allows you to set up certain phrases or commands that will automatically trigger certain actions or responses. These actions are defined using a specific type of language called "Perl regex expressions." Aliases only work on the input that the user types into the command line, while a separate feature called "triggers" works on the output that the game sends back. When you type a command and press enter, it is sent to the alias feature to see if it matches any of the phrases or commands that have been set up. If it does match, the alias will take over and either send a different command to the game or run a script (a set of instructions) that you have defined. This system is powerful because it gives you control over what happens when a certain phrase or command is entered. You can even change the user's input as it is being processed, allowing you to manipulate the command before it is sent to the game.
The example in the diagram above shows 2 matching aliases, but only one of them sends commands to the game - and only if the player is healthy enough to attack the opponent. The other alias that matched the user input (enemy) choses a fitting opponent and sets the variable enemy accordingly, otherwise it issues a warning that attacking one of the available enemies would be too dangerous.
For an alias to match the command text the same rules as explained above in the trigger section apply. However, to simplify matters there is only one alias type. As alias are not performance critical we could reduce the amount of trigger types to just Perl regex as this type can do it all and performance is no issue with alias as the amount of data is much less. Even if you type like a madman you’ll never get close to sending the same amount of text to the game than the amount of text the game sends back to you.
What does it mean that a regex is true or "matched"? A trigger or an alias fires - or executes its commands/script - when the text matches the pattern of the trigger or alias. In most cases this means that the text contains the trigger/alias pattern. If the regex pattern is reen
then a text "The green house" will match because "reen" is contained in the text. More complex regex patterns can also hold information on where in the text a certain pattern must occur in order to match. ^tj
only matches when the letters "tj" occur at the beginning of the text. Consequently, a text like "go tj" would not match. Regex patterns can also capture data like numbers, sequences of letters, words etc. at certain positions in the text. This is very useful for game related scripting and this is why it is explained below.
Let’s get back to alias. We start with a simple example.
We want Mudlet to send "put weapon in bag" whenever we type "pwb". Consequently, the pattern is pwb
and as the task is so simple it’s enough to enter "put weapon in bag" in the send field. Then we click on save to save the changes and activate the alias by clicking on the padlock icon. Then we leave the trigger editor and test our new alias. After typing "pwb" and pressing return Mudlet will send the command "put weapon in bag" to the game.
Let’s move on to a more complicated example that is needed very often.
We want our script to automatically put the weapon in the correct bag as we have many bags and many weapons. The pattern stays the same. ^pwb The ^ at the beginning of the line means that the command starts with pwd and no other letter in front of this. If we define our pattern more clearly, the pattern will match less often. Without the ^ the alias will match and the alias script will always be run whenever there is the sequence of letters "pwb" in your commands. This may not always be what you want. This is why it’s usually a good idea to make the pattern definition as exact as needed by being less general. The more general the pattern, the more often it will match.
Back to our task: The pattern is ^pwb
. Let’s assume that we have defined 2 variables in some other script. The variable "weapon" is the weapon we use and the variable "bag" is the name of the bag. NOTE: In Mudlet global variables can be accessed anywhere from within Mudlet scripts - no matter if they have been defined in a trigger script, in an alias script or in a key or button script. As soon as it’s been defined it somewhere it is usable. To make sure that a variable is local only, i. e. cannot be referenced from other scripts, you have to use the keyword local in front of your variable definition. Back to our alias: Pattern is: ^pwb
Script is:
send(f"put {weapon} in {bag}")
Depending on the values of our variables weapon and bag the command "pwb" will be substituted with an appropriate command. To set your weapon and bag variables we use 2 more aliases: Alias to set the weapon: uw (\w)+
Script:
weapon = matches[2]
send( "wield " .. weapon )
To set our bag variable: Pattern:^set bag (.*)
bag = matches[2]
Now let’s go back to our initial problem. We want an alias to put our current weapon into our current bag. But what happens if we are in the middle of a fight and absolutely need to sip a healing potions because we are close to death and cannot take the risk that the bag may be too full to take the weapon? We want to upgrade out little alias to take into account that the bag may be full and chose an empty bag instead. To do this we set up a trigger that detects messages that indicate that the attempt to put the weapon in the bag failed. In this trigger we execute this little bag-is-full-detection-trigger Trigger Pattern: (type substring) Your bag is full.
Script:
bagIsFull = true;
This detection trigger will set the variable bagIsFull
to true as soon as it sees the message "Your bag is full.". Then you know that you have to use your spare bag to take the weapon.
Now we have the tools to write an improved version of our little alias script:
if bagIsFull then
send(f"put {weapon} in {spareBag}")
else
send(f"put {weapon} in {bag}")
end
The next example is one of the most common aliases a tell alias: Pattern:^tj (.*)
Script:
send("tell Jane " .. matches[2])
Sadly, Jane is your fiancée and the one thing she is a vegetarian and absolutely hates all words that relate to meat. Luckily, you know enough about aliases by now to make her believe that you’d never ever even think about meat. So you head to your global function script (any script item will do as long as you define your variables outside of your function definitions. See the scripts chapter below for more information. In your script "my global functions" you add a Lua table containing a list of all of all words that a vegetarian might hate. For example:
happyJaneTable = {"meat", "burger", "steak", "hamburger", "chickenburger"}
Now you can upgrade your tell-jane script to automatically search our tell for all words that Jane might not like. In case such a word is found we substitute the entire tell with "How is the weather?".
for key, value in ipairs(happyJaneTable) do -- looking at each element of the list
badWord = happyJaneTable[key] -- check out the Lua table chapter below for more info
begin, end = string.find(command, badWord) -- begin holds the start position of the word, end* the end-position
if begin ~= nil then -- we have found a bad word
send("tell Jane How is the weather?")
return
end
end
Alias Examples
Alias:
^cc( .+)?
Script:
local target = matches[2] or ""
send("c cure critical" .. target)
Default keybindings in the main window
Keypress | Action |
Left Arrow | Moves the cursor one character to the left. |
Shift+Left Arrow | Moves and selects text one character to the left. |
Right Arrow | Moves the cursor one character to the right. |
Shift+Right Arrow | Moves and selects text one character to the right. |
Home | Moves the cursor to the beginning of the line. |
End | Moves the cursor to the end of the line. |
Backspace | Deletes the character to the left of the cursor. |
Ctrl+Backspace | Deletes the word to the left of the cursor. |
Delete | Deletes the character to the right of the cursor. |
Ctrl+Delete | Deletes the word to the right of the cursor. |
Ctrl+A | Select all. |
Ctrl+C | Copies the selected text to the clipboard. |
Ctrl+Insert | Copies the selected text to the clipboard. |
Ctrl+K | Deletes to the end of the line. |
Ctrl+V | Pastes the clipboard text into line edit. |
Shift+Insert | Pastes the clipboard text into line edit. |
Ctrl+X | Deletes the selected text and copies it to the clipboard. |
Shift+Delete | Deletes the selected text and copies it to the clipboard. |
Ctrl+Z | Undoes the last operation. |
Ctrl+Y | Redoes the last undone operation. |
Ctrl+<Click> | Select the whole line. |
Ctrl+Enter | Close split screen (scroll all the way to the bottom). |
Ctrl+F | Search for text in the main output window. |
Considerations
- Mudlet 4.17.2 currently does not support named capture groups for aliases.
- You will need to use
matches[1]
,matches[2]
etc. instead.
- You will need to use
- You can use modifiers for these regexes. Declare these at the start, in the form
(?modifiers)
- For example, to match the command
test
, ignoring case, you would need to declare the regex as being case-insensitive, like so:(?i)^test$
- For example, to match the command
Trigger Engine
Unlike alias that define patterns that match on user input, triggers define patterns that match on game output. In other words, triggers react to text that has been sent by the game, whereas alias react to user commands that have been typed into the command line.
Simple Trigger Matching
Note: QUICKSTART: Here is a simple video tutorial on how to make basic triggers in Mudlet.
Steps To Create A Trigger
- Click on the "Add" button to create a new trigger.
- Put the text pattern that you’d like to trigger on into the trigger conditions table on the right side of the screen above the script editor.
- Choose the correct pattern type of your trigger pattern in the drop down list on the right side of the trigger pattern i.e. in the second column of the trigger pattern table. If you define multiple patterns in the same trigger, your trigger will run whenever any one of these patterns matches unless you chose the AND-trigger option, in which case the trigger will only fire if all patterns match within a specified amount of lines from the game. For more advanced information on AND and OR trigger see the corresponding AND/OR trigger section below.
- Define what happens when the trigger matches. Usually, this will be done by a Lua script in the Lua editor below the table with the pattern list. In the beginning, you can simply choose the "highlight trigger" option to make your trigger highlight the text that it has triggered on or send a clear text command to the game whenever the trigger fires until you have learned enough Lua to more meaningful scripts. Clear text command can be defined in the "send plain text" input box next to the trigger name above the pattern table.
- Save the new trigger and then activate it with the padlock icon button. By default, new triggers are deactivated and thus will do nothing unless you explicitly activate them. Activated triggers show a green tick in the little blue box on the left side of the trigger name in the trigger tree on the left side of the script editor dialog.
There are three ways to save your changes; click on the save icon, adding another new trigger, clicking on another trigger item. Triggers that have not been saved yet cannot be activated.
Note: If you see a bug icon instead of an activation box, there is some error that prevents activation of your trigger. Check the error message above the pattern table.
Example: You want to trigger on the word "pond" in a line such as: "The frog swims in the pond. Plop." All you need to do is add "pond" as a pattern and choose "substring" as a pattern type. Then enter the command you like to send to the game if the trigger matches. For example enter "drink water" into the "send plain text" input box and activate your new trigger. Your new trigger will send the command "drink water" to the game any time it sees the word "pond" somewhere in the game output.
Simple Highlighter Triggers
Note: Beginners should use Mudlets automated highlight triggers in the beginning to get the hang of the different trigger and pattern types quicker.
- Create a new trigger and add a pattern as detailed above.
- Click on the "highlight trigger" option and pick a foreground and a background color. When the trigger matches it automatically highlights its pattern.
- Save.
This is one of the most used form of triggers in mudding. It is a quick way of highlighting words that are important to you at the moment. This helps you spot things at a single glance instead of reading the entire text. You don’t have to know anything about scripting, regular expressions etc. to use highlight triggers. Just type in the word you like to be highlighted, select appropriate colors, save the new trigger and activate it.
More advanced users will often want to do custom highlighting from within scripts. This is how it works: If you want to highlight the word "pond" in the above example you have to add the following little Lua script to the script editor on the lower right side of the Script Editor window:
selectString( "pond", 1 )
fg( "red" )
bg( "blue" )
resetFormat()
Testing Triggers
You can test your triggers with the `echo
alias from the command line. This requires the echo package, which is installed by default on a new profile.
`echo A test line.
`echo One line.$Next line.$Third line.
Or from your Lua code using feedTriggers
feedTriggers([[
You are carrying:
a credstick a pair of steel toed boots (worn)
a Soko Sukeban (worn) a medikit
a cherry moon katana a GPS device
a NV contacts a Tsunami MkII
a MeshGirl Helm an Alto 500 CellPhone
a Kantan Sukeban (worn)
Your load barely slows you down.
]])
Alternatively, you can also record a replay and play it - triggers will be matched on it.
Pattern Matching Types
Substring
This trigger type will match any text on a single line. Useful for matching strings of text which don't change at all.
Examples:
Pattern: | pond |
String: | The frog swims in the pond. |
Outcome: | match (pond matches the word pond) |
Pattern: | pond |
String: | The frog swims in the swamp. |
Outcome: | no match (pond does not match swamp) |
Perl Regex
This trigger type will match text using Regular Expressions (Regex). Regex in Perl is a special text string for describing a search pattern within a given text. For example \w+ is used to match a single word, or \d is used to match a single digit.
Pattern matching also allows the match to be saved in the matches table variable if enclosed in parentheses. matches[1] contains the entire line of the trigger whereas matches[2] contains the first regex pattern (in our examples pond or swamp).
Examples:
Pattern: | The frog swims in the (\w+). |
String: | The frog swims in the pond. |
Outcome: | match (\w+ matches pond, and is also saved to matches[2] variable) |
Pattern: | The frog swims in the (\w+). |
String: | The frog swims in the swamp. |
Outcome: | match (\w+ matches swamp) |
Pattern: | The frog swims in the (\w+). |
String: | The fish swims in the pond. |
Outcome: | no match (fish should be changed to a regex pattern \w+) |
Note: regex101.com is highly recommended to testing out your regular expressions. It includes a pattern tester, cheat sheet and explanations about the matches.
Start of Line
This is a combined variation of substring matching and regex matching. This type will match any line with text at the start of the string only.
Examples:
Pattern: | The frog |
String: | The frog swims in the pond. |
Outcome: | match (The frog is at the start of the string) |
Pattern: | frog swims |
String: | The frog swims in the swamp. |
Outcome: | no match (frog swims is not at the start of the string) |
Note: This is exactly the same as using a Perl regex pattern with the ^ anchor (start of a line).
Exact Match
The string must match exactly the pattern. On a whole line, this is the same as using a perl regex pattern with the ^ and $ (start and end of line) anchors.
Examples:
Pattern: | The frog swims in the pond. |
String: | The frog swims in the pond. |
Outcome: | match (the entire line matches) |
Pattern: | The frog swims in the pond. |
String: | The frog swims in the pond. The bird flies in the sky. |
Outcome: | no match (The entire line does not match as the bird information has been added. This would match a start of line pattern though) |
Lua Function
The purpose of Lua function triggers is to be able to run Lua scripts as trigger conditions. This trigger types takes an actual Lua function as the pattern. This function must return true or false to evaluate correctly, but will only match when returned true.
For example, let's say we wish to enable our entire offensive combat triggers when we commence combat, but otherwise be disabled when we are not in combat. One would create a trigger chain that contains all the offensive triggers. The chain head (the root trigger of the chain) simply contains this Lua function condition as its only trigger condition: return inCombat
Now, whenever you enter combat you set the variable "inCombat" to true and thus automatically enable you entire combat triggers that are otherwise disabled.
Examples of patterns:
if myVar > 20 then return true else return false end
if myFunc() < myVar then return true else return false end
return myFunc()
if myFunc returns a boolean value
Another example:
Create a new trigger with the pattern checkHealth()
. Define a new Lua function checkHealth()
. Open the script editor, add a new script "health monitor" and add this code to the new script.
function checkHealth()
if health <= 100 then -- health is a variable we capture from e.g. the prompt or GMCP
echo( "WARNING: Low health! I have to run away!\n" )
send( "flee" ) -- we send a command to run away to the game
return true
else
return false
end
end
Lua Syntax Primer: Expressions
Note: Mudlet used to only allow the boolean true value as the return value to consider the pattern to have been a match. It has since been improved to allow any value Lua would consider true - but if you see any Lua Function Conditions (LFC) making use of and true or false at the end, that'd be why.
if A == B then (...) ----> if A equals B
if A ~= B then (...) ----> if A *NOT* equals B
if A <= B then (...) ----> if A is smaller or equal to B
if A >= B then (...) ----> if A is greater or equal to B
if A < B then (...) ----> if A is smaller than B
if A > B then (...) ----> if A is greater than B
The operators and and or behave differently in Lua than in most other programming languages as they are not guaranteed to return a boolean value.[1]
Lua Function Conditions (LFC) effectively means that you run the Lua code they represent on every single line that is received from the game, unless the LFCs are part of a multi-condition trigger, in which case the LFCs would only be run when they would be the current test-condition of their respective trigger state. LFCs are implemented in such a way that the trigger engine only calls the compiled byte code, but running as few scripts as possible is always the best idea. LFCs are nice and handy, and for most people the performance aspect will not be relevant at all.
Line Spacer
To make the engine skip lines in a multiline / AND trigger, use the line spacer pattern: select 'line spacer' as the type and put in a number on the left to make it go to the next X lines, for example 1 will make it go to the next line. See the multline section below for further explanation.
Color Trigger
This type of trigger matches the color of the text, as opposed to the actual text incoming. Both foreground and background matching is available and you will be prompted to select the required colors.
Examples:
Pattern: | Foreground color [ANSI 12], Background color [ANSI 15] (blue foreground on a white background) |
String: | The bird flies in the sky. |
Outcome: | match (the COLOR matches) |
Prompt
This type of trigger will only work with servers that have implemented the telnet suboption GO AHEAD. If you server does not implement GA this will be shown in the bottom right corner of Mudlet with the characters No GA.
This option has one function, to match your prompt and will fire when it does.
Multi-Line Triggers and Multi-Condition Triggers
Multi Condition Triggers are the most advanced feature of Mudlets trigger engine. Like trigger chains or filter chains they are a form of AND-Triggers. All conditions in the list must have matched within the specified margin of lines (delta), in order to trigger the script. Normal triggers fire and run the script as soon as one of the conditions in the regex list is met i.e. if one of the regex/string matches match - or the Lua function condition returns true, the trigger script is run. In multiline triggers, however, each single regex/string/Lua function condition in the list has to have matched within the specified margin of lines at least once to trigger the script. The sequence of the conditions is binding. This means that if the 10th regex on the regex list would be matched on the eleventh line after the match of the first line happened, the trigger will NOT run unless the margin of lines is set to 11. If condition #3 is true but currently #2 is waiting to be true, condition #3 is ignored and must be true again after condition #2 has been true. Conditions can also be Lua Functions or plain Lua code that returns a boolean truth value. You can mix all types of conditions to build complex multi-condition triggers that only fire if all conditions are met. This is a very powerful feature as it reduces the amount of scripting to a minimum or even takes away with the need to script formerly complex processes completely. Multicondition triggers are multi-line triggers, i. e. the conditions can all be met in a single line or many lines after the first condition has been fulfilled. This effectively reduces the amount of complexity as you have all the important conditions placed into a single trigger and all the tedious bookkeeping, variable and condition state accounting is being done by Mudlets trigger engine. The result of this is that the amount of manual condition checking via many different trigger scripts and legions of if condition1 == true then check condition2 can be forgotten about. All you have to do is to define the conditions and the final action that is taken if the trigger fires.
Note: to retrieve wildcards in multi-line or multi-condition triggers, use the multimatches[line][match] table instead of matches[].
Note: This diagram shows what steps are involved in the process of problem solving with Mudlets trigger engine. The main question is: How do we arrive at a solution to our problem, and how can we simplify the problem as much as possible?
Example: Let’s go back to our pond & frog example. We have explained the difference between AND-triggers and OR-triggers. If you have a room description consisting of 3 lines:
- You see a pond
- You see a frog.
- The frog sits on a stone.
Every single one of these 3 lines will be fed into the trigger engine one after another. If we define an OR-trigger with a condition list consisting of 3 condition patterns:
- condition #1 pattern = pond
- condition #2 pattern = frog
- condition #3 pattern = stone
Whether or not a condition is found to be true also depends on another property, namely the type of the condition. The condition type can be among others:
- substring matching → the condition is true if the condition pattern is a substring of the output line from the game. In other words: If the pattern "pond" is contained in any line of output, the condition is true.
- begin of line matching → the condition is only true if the condition pattern can be found at the beginning of a line from the game.
- Perl regular expression → the condition is true if the Perl regex pattern is matched. You’ll find more information on Perl regex below.
- Lua code that returns a truth value e.g. a call to a function check() that return either true or false depending on the condition
In our example we chose condition type "substring" for all three conditions. Consequently, the trigger will fire the command or script 3 times i. e. the trigger will do on each line it is matched against because in every line at least one condition evaluates to true because the pattern is a substring of the line.
- in line #1 we get: pond = true.
- in line #2 we get frog = true and
- in line #3 two conditions are true i.e. frog=true and stone = true
Because an OR-trigger fires on the first condition that is true and ignores the rest the trigger will only execute 3 times on these 3 lines although 4 conditions are true.
Note: CAUTION: The multi line condition switch must be turned off to get an OR-trigger! If the multi-line condition switch is turned on the trigger becomes an AND trigger which means that the trigger only fires if all conditions are true and fulfilled in the correct sequence. With OR-triggers the sequence is irrelevant.
To complicate matters, however, you don’t want your trigger to fire 3 commands, because you want to use this room description as a whole to fire your trigger e. g. this pond is the only kind of ponds in the entire world that doesn’t have poisoned water. So you want to make sure that you only drink water from a pond of this kind and from no other pond. Your solution is to use Multi Condition Triggers (MCT). If you check the MCT checkbox this trigger will fire only once from now on - and only if all conditions are met i e. when you can guarantee that you only drink water from a good pond because your drinking trigger is matching on the entire room description despite that this room description my be spread over a number of lines. (NOTE: If you have word wrap turned off in your game chances are that the entire room description will be contained in a single line, but we are trying to keep the examples as easy as possible.)
Sadly, there are many unfriendly people in this world and somebody goes around and poisons your good ponds. Consequently, you would want to examine the frog and find out if it is poisoned before drinking water from the pond. This is difficult because the villain is a mean magician who used illusion spells to make everything look like the good pond. To solve the problem you can now resort to Lua function conditions in the trigger condition list that perform certain check ups to put the current room description into a wider context e. g. check if you have been here before etc. This adds yet another level of complexity to your problem but this is a very powerful means to use the full potential of Mudlets MCTs.
You can combine all forms of conditions with trigger chains, filters and Lua functions. Mudlet gives you relatively easy to use tools that require no programming background. However, these tools can evolve into complex powerful problem solving mechanisms if they are combined with each other thus enabling non-programmer users to solve problems that would need a profound programming background in other gameclients. However, unlocking the full potential of Mudlet requires you do learn some Lua basics. In this manual we’ll try to be as easy on you as we can in this respect, but it’s highly recommended that you dig deeper into Lua after a while. It’s one of the easiest fully fledged scripting languages available, easy to learn and the fastest of them all, and this is why it has been chosen by us. You don’t need to become a programmer to be able to use Mudlet effectively. All you have to know is how to work with variables and how to do if conditions and maybe a few loops. But knowing more won’t harm you and it will make your systems more effective.
"multiline / AND" and "OR" Condition Triggers
Multiline, or AND triggers execute their respective command or script only if all conditions in their respective conditions expression list are fulfilled.
OR-Triggers execute when any one of their conditions is true. To make OR-Triggers you simply have to add a few conditions to the conditions list e.g. in our example: "pond", "frog", "Plop". The trigger will now also execute on lines like this: "A frog is green" or "You hear a loud Plop!" because "frog" matched in the first line and "Plop" matched in the second line.
Multiline / AND triggers execute when all of their conditions are true. Taking the previous patterns of "pond", "frog", "Plop" and clicking the 'AND' button will make the trigger not fire on "A frog is green" or "You hear a loud Plop!" anymore, because all 3 patterns don't match. It however would fire on "In a brown pond, the frog said Plop!" because all 3 patterns matched. As you might have noticed, all 3 patterns are on the same line: this means that in a multiline/AND trigger, the next pattern starts matching on the same line to make this work.
To make the engine skip lines in a multiline / AND trigger, use the line spacer pattern: select 'line spacer' as the type and put in a number on the left to make it go to the next X lines, for example 1 will make it go to the next line.
The simplest form of AND-Triggers in Mudlet are Trigger Chains or Filter Chains, whatever you’d like to call it.
Trigger Chains & Filter Chains
"Chains" and "filters" are different trigger group entities in Mudlet and serve completely different ends.
Chains
A chain is defined in Mudlet by making a trigger group and adding a trigger pattern to the group. A group without a pattern is a simple trigger group that serves no other purposes than adding structure to your trigger system in order to organize your triggers better. Such a normal trigger group will always grant access to its children unless it has been explicitly disabled (= all access to itself or any of its children is locked) either manually or by a script.
A trigger group with a defined trigger pattern, however, will behave like a normal trigger and match if the pattern matches. Such a trigger group is called "chain head". A chain head will only grant access to its children if the trigger pattern has matched and the chain has been enabled. Thus, chains can be looked at as a mechanism to automatically enable and disable triggers or groups of triggers when a certain condition has been met i. e. the trigger pattern of the chain head has been matched. (However, technically this is not correct as disabled child triggers will not be invoked if the chain head matches. In other words, in chains you can still have enabled/disabled elements. The idea of a chain can better be described by necessary and sufficient condition - both of which need to be met before a child trigger is being run.)
Adding child triggers to this group will add elements to the trigger chain. These chain elements will only be activated if the chain head has matched before and thus opened the trigger chain. The chain stays open until the "keep chain open for x lines" value has been reached. The default is 0 which means that the chain only stays open for the current line. When access to the chain has been granted all child triggers will be tested against the content of the current line. Consequently, trigger chains are a means to automatically enable/disable trigger groups without the hassle of enabling and disabling trigger groups manually. This has 2 important advantages: Chains are faster than conventional solutions and chains reduce the complexity of your scripts and greatly reduce the usual system bugs that inevitably go along with enable/disable trigger xy function calls as it’s very difficult and error prone to enable and disable your triggers correctly in large complex trigger systems. This is one of the most powerful features in Mudlet and should be used whenever possible.
Let’s look at a practical example for a trigger chain:
My newbie prompt on Achaea looks like this 500h, 500m ex-
when I have balance and 500h, 500m e-
when I have lost balance. We are going to develop a prompt detection trigger that raises the event gotPrompt and sets the variables myBalance=true
or myBalance=false
To begin with, we add a new trigger group and add the Perl regex pattern to detect the prompt:
^(\d+)h, (\d+)m
The pattern reads in plain English: At the beginning of a prompt line there are is a number directly followed by the letter h, a comma, a space and another number followed by the letter h. Whenever this pattern matches the trigger will fire and we’ll know that we have a prompt line. We use the 2 numbers that we captured in our pattern to update our health and mana stats.
Detecting balance is more difficult as balance is indicated by the letters ex- on the same line after the prompt and imbalance is indicated by the letters e-. As we have set a pattern in a trigger group (folder), the folder is turned into a trigger chain head. It will now only let data through to its children when its own pattern is matched. In other words, the child triggers of the trigger chain will only receive data on prompt lines. We are going to take advantage of this by adding two simple substring triggers to detect balance and imbalance. The balance trigger pattern is ex- and the imbalance detector pattern is e-.
In the two balance detection triggers we now write myBalance=false
and myBalance=true
respectively plus a little echo on the screen that shows our current balance status.
We could now add a call deleteLine()
to the prompt detection trigger to erase the prompt from the screen if we don’t want to see it as we have computed all relevant information.
Filters
You can turn a trigger chain head into a filter by checking the "only pass matches" option. This changes the content of what is forwarded as trigger text to the children triggers in the chain. Chains forward the content of the current line as trigger text whereas filters forward the matched pattern instead of the current line. In other words, the text of the current line is filtered according to the pattern of the filter. For example: You want to know the exits in the current room. The simplest solution is a simple filter on You see exits to: (.*)
Then you simply add triggers to the filter chain such as north
, south
, west
, east
etc. The direction triggers will only be called if the filter head has matched.
Imagine the following scenario: You want to collect some berries. You know that the room contains some berries if the room description contains the words "You are inside a forest. There are some strawberries." or "You are inside a forest. There are some blackberries." and there are always only one kind of berry in the room. You make a new substring trigger for this line, but instead of choosing a regular trigger, you chose to add a new trigger group. Now you add You are inside a forest\. There are some (\w+)\.
as an Regular Expression to the expression list of the trigger group. When adding conditions to a trigger group, the trigger group turns from an organizational unit into a filter unit, if the "filter" option is checked. From now on this folder is a filter and will only let its matches pass through. In our case this is exactly what we want, because we don’t want to collect all sorts of berries, but we only want 2 particular kinds, namely, strawberries and blackberries, and we know that these berries can only be trusted if they are picked inside a forest as other areas may contain contaminated berries. Now you add two regular triggers to our berry-in-the-forest filter - one containing the condition: strawberries
and the other one blackberries
. Then we add the commands to pick the particular kind of berry to both triggers (send field). Now what happens is that as soon as a room description reads "You are inside a forest. There are some strawberries/blackberries." the filter will let the kind of berry pass through to our two berry triggers and they will issue commands to pick berries, if there are such berries. However, in any other situation the words "strawberries" and "blackberries" will NOT trigger a pick - only in the above scenario when the filter parent’s condition is met. This is a very nice way to solve complicated problems with a few easy filter chains. This example is trivial, but using filter chains will rapidly show you how formerly complicated things that could only be solved with very complicated regular expressions can be solved easily now with a couple of filter chains.
Named Capture Patterns
You can use named patterns - that is, giving your captures names instead of [2], [3], [4] numbers - by adding ?<variablename> to your capture in Mudlet 4.11+:
-- before:
(\w+) = matches[2]
-- now:
(?<name>\w+) = matches.name or matches["name"]
(?<weapon>\w+) = matches.weapon or matches["weapon"]
This can be particularly handy if you have several patterns in a trigger and the location of matches[n] moves around. Try this out in a demo package.
Named capture groups still will be available under their respective number in matches
.
You can programatically check if your Mudlet supports this feature with mudlet.supports.namedPatterns
.
Example:
Let's assume we want to capture name and class of character, but MUD outputs it two ways.
My name is Dargoth. My class is a warrior.
I am a wizard and my name is Delwing.
Instead of creating two different triggers we can create only one containing alternative matching patterns:
^My name is (?<name>\w+)\. My class is a (?<class>\w+)\.
^I am a (?<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")
Note: To verify whether named groups are available you can use flag mudlet.supports.namedGroups
.
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: 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.
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: 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?
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.
Here's the Mudlet XML you can download for this!
How to target self or something else?
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:
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: 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 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:
What you just need to do is create a webhook in a channel. Go to channel settings, then create this:
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
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:
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:
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:
- Create a discord application
- Upload game logo for "rich presence invite image"
- Upload same game logo in "Rich Presence Assets" with the name of
server-icon
- Upon receiving "External.Discord.Hello" from the game client, reply with "External.Discord.Info" with
applicationid
(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 some ideas for filling it in.
- 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:
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.
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 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:
- 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.
Required | Key | Value | Purpose |
---|---|---|---|
Yes | "url" | <url> |
|
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> |
|
Maybe | "url" | <url> |
|
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> |
| |
Maybe | "url" | <url> |
| |
No | "type" | "sound" or "music" | "sound" |
|
No | "tag" | <tag> |
| |
No | "volume" | 1 to 100 | 50 |
|
No | "fadein" | <msec> |
| |
No | "fadeout" | <msec> |
| |
No | "start" | <msec> | 0 |
|
No | "finish" | <msec> | 0 |
|
No | "loops" | -1, or >= 1 | 1 |
|
No | "priority" | 1 to 100 |
| |
No | "continue" | true or false | true |
|
No | "key" | <key> |
|
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: 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> |
|
No | "type" | "sound" or "music" |
|
No | "tag" | <tag> |
|
No | "priority" | 1 to 100 |
|
No | "key" | <key> |
|
No | "fadeaway" | true or false |
|
No | "fadeout" |
|
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:
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()
.
Timer Engine
Mudlet supports four different sorts of timers:
The first can be configured by clicking and will start some code in regular intervals. The others can be used in other scripts you code. All of them are described in more detail below.
Regular GUI Timers
Regular GUI Timers that fire repeatedly in a certain interval specified by the user. To make one, go to the Timers section (1) in Mudlet, click Add (2), select the time periods you'd like the timer to be going off at - save (3) and activate it (4). The timer will go off after your specified interval and then at regular specified intervals after that, until disabled.
You can also enable/disable this timer via scripting with enableTimer() and disableTimer():
enableTimer("Newbie timer") -- enables the timer, so 2s after being enabled, it'll tick - and every 2s after that
disableTimer("Newbie timer") -- disables it, so it won't go off anymore
Temporary Timers
Temporary Timers are timers that go off only once and are the most common type of timer used for scripting purposes. You don't work with them from the Timers section - you just code with them only.
For a basic introduction to temporary timers, read up about them here. Here, we'll continue expanding on what you've learnt so far.
One thing to keep in mind when working with tempTimers is that they are unlike #wait statements you might see in other clients. While #waits are typically cumulative, tempTimers aren't - they go off from the same point in time. Thus if you'd like two timers to go off - one after 3s, and another 1s after the first one - you'd set the second timer to go off at 4s. For example:
tempTimer(3, [[ echo("this is timer #1 going off 3s after being made\n") ]])
tempTimer(4, [[ echo("this is timer #2 going off 4s after being made - and 1s after the first one\n") ]])
Note: Temporary timers cannot be accessed from the GUI and are not saved in profiles.
Stopping
To stop a temporary timer once you've made it and before before it went off - you use killTimer(). You give killTimer(id) the ID of the timer that you've made - which you get from Mudlet when you make it. Here's an example:
-- get and store the timer ID in the global "greeting_timer_id" variable
greeting_timer_id = tempTimer(2, [[ echo("hello!\n") ]])
-- delete the timer - thus nothing will actually happen!
killTimer(greeting_timer_id)
Refreshing
You can also use killTimer() to "refresh" a timer - the following code example will delete the previous timer if one exists and create the new one, thus making sure you don't get multiple copies of it:
if portal_timer then killTimer(portal_timer) end
portal_timer = tempTimer(2, [[ send("enter portal") ]])
Time remaining
You can use remainingTime() to check how much time remains on a provided Timer.
tid = tempTimer(600, [[echo("\nYour ten minutes are up.\n")]])
echo("\nYou have " .. remainingTime(tid) .. " seconds left, use it wisely... \n")
-- Will produce something like:
You have 599.923 seconds left, use it wisely...
-- Then ten minutes time later:
Your ten minutes are up.
Using variables
To embed a value of a variable in tempTimer code, you might try using this given what you've learnt:
tempTimer(1.4, [[ echo("hello, "..matches[2].."!\n") ]])
But that won't work as you'd expect it to - that will try and use the value of matches[2] when the timer goes off - while by then, the variable could have changed! Instead, you take it outside the square [[ ]] brackets - this is correct:
tempTimer(1.4, [[ echo("hello, ]]..matches[2]..[[!\n") ]])
-- or with less brackets:
tempTimer(1.4, function() echo("hello, "..matches[2].."!\n") end)
Nesting
If you'd like, you can also nest tempTimers one inside another - though the first [[]]'s will become [=[ ]=]:
tempTimer(1, [=[
echo("this is timer #1 reporting, 1s after being made!\n")
tempTimer(1, [[
echo("this is timer #2 reporting, 1s after the first one and 2s after the start\n")
]])
]=])
If you'd like to nest more of them, you'd increase the amount of ='s on the outside:
tempTimer(1, [==[
echo("this is timer #1 reporting, 1s after being made!\n")
tempTimer(1, [=[
echo("this is timer #2 reporting, 1s after the first one and 2s after the start\n")
tempTimer(1, [[
echo("this is timer #2 reporting, 1s after the second one, 2s after the first one, 3s after the start\n")
]])
]=])
]==])
Closures
Last but not least, you can also use closures with tempTimer - using a slightly different syntax that has advantages. For example, you have access variables in its scope, when it goes off:
local name = matches[2]
tempTimer(2.4, function() echo("hello, "..name.."!\n") end)
Also syntax highlighting will work as expected, because the function will not be given as a string.
Note: In this case, you mustn't use matches[2] directly in the echo() command. It will not work as expected. Instead, bind the it to a new variable like name as seen in the example above.
Offset Timers
Offset Timers are child timers of a parent timer and fire a single shot after a specified timeout after their parent fired its respective timeout. This interval is an offset to the interval of its parent timer. To make them, add a regular GUI timer (see above), then create another timer and drag it onto the timer. This will make the timer that is "inside" the timer (the child inside the parent) go off at a certain time after its parent goes off. Offset timers differ visually from regular timers and are represented with a + icon for offset. Offset timers can be turned on and off by the user just like any other timer. For example - a parent timer fires every 30 seconds and by doing so kicks off 3 offset timers with an offset of 5 seconds each. Consequently, the 3 children fire 5 seconds after each time the parent timer fired. To make this happen, make the parent timer tic every 30 seconds, drag 3 timers into it with an offset of 5s on each:
Uses and examples
Enable/disable triggers
This'll make use of tempTimer to enable a trigger and disable it after a short period of time:
enableTrigger("Get enemy list")
tempTimer(3, [[disableTrigger("Get enemy list")]])
Running a script after the current triggers
A useful trick to get your code to run right after all of the current triggers (GUI and temporary) ran would be to use a time of 0:
-- in a script, this will run after all scripts were loaded - including the system, wherever in the order it is.
tempTimer(0, function()
print("this timer is running after all triggers have run")
end)
Have more examples you'd like to see? Please add or request them!
Stopwatches
The stopwatch feature can be used to time how long things take, e.g. long running functions or how long a fight lasts. Stopwatches can be started, stopped and reset just like a regular physical stopwatch. They can also be given a name and made to be persistent, meaning that they can keep running (or timing) when Mudlet has been closed. This gives you the ability to time real-life events. Of course, you can also query how much time has passed and delete a stopwatch no longer required.
Creating and Starting
fightStopWatch = fightStopWatch or createStopWatch() -- create, or re-use a stopwatch, and store the watch variable ID in a global variable to access it from anywhere
-- then you start the stopwatch in some trigger/alias/script with;
startStopWatch(fightStopWatch)
The watch will now be running. Alternatively, you can create and instantly start a stopwatch in one line;
fightStopWatch = fightStopWatch or createStopWatch(true)
See also: createStopWatch(), startStopWatch()
Stopping and Resetting
You can stop a stopwatch and retrieve its run time as follows;
-- in a trigger script for example you can write:
fightTime = stopStopWatch(fightStopWatch)
echo("The fight lasted for " .. fightTime .. " seconds.")
Then reset it for the next fight.
resetStopWatch(fightStopWatch)
Note that resetting a stopwatch does not stop or start it, it's simply resets the time value to zero.
See also: stopStopWatch(), resetStopWatch()
Deleting a Stopwatch
To delete a stopwatch and free up the variable ID simply;
deleteStopWatch(fightTime)
See also: deleteStopWatch()
More Ways to Retrieve Running Time
The time can be retrieved at any point of a started and currently running or stopped stopwatch.
getStopWatchTime
- returns the time as a decimal number of seconds with up to three decimal places to give a milli-secondsgetStopWatches
- returns a table of the details for each stopwatch in existencegetStopWatchBrokenDownTime
- returns a table of times for a given stopwatch; days, hours, minutes, etc...
See also: getStopWatchTime(), getStopWatches(), getStopWatchBrokenDownTime()
Using Named Stopwatches
All variable names can be replaced with a string name. Let's rewrite the above code with named stopwatches instead.
fightStopWatch = fightStopWatch or createStopWatch("Fight Stopwatch", true) -- using the name 'Fight Stopwatch' and starting it automatically
-- some time has passed
stopStopWatch("Fight Stopwatch")
echo("The fight lasted for " .. getStopWatchTime("Fight Stopwatch") .. " seconds.")
resetStopWatch("Fight Stopwatch")
Using Persistence
It is preferable (for technical reasons) to assign a name to a stopwatch that will persist over Mudlet sessions.
fightStopWatch = fightStopWatch or createStopWatch("Fight Stopwatch")
setStopWatchPersistence("Fight Stopwatch") -- enable this stopwatch to persist over Mudlet sessions
display(getStopWatches()) -- will now show that the stopwatch is persistent
See also: setStopWatchPersistence()