Difference between revisions of "Manual:Best Practices"
(21 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
{{TOC right}} | {{TOC right}} | ||
+ | {{#description2:Collection of Mudlet best practices for building your scripts.}} | ||
= Best Practices = | = Best Practices = | ||
− | : The hope is that this page will provide a place to collect a list of Mudlet 'best practices' in an effort to help people keep their Mudlet scripts and packages clean, efficient, and running smoothly. Another way to think of it is a collection of tips and tricks for making Mudlet run its best. They largely fall under | + | : The hope is that this page will provide a place to collect a list of Mudlet 'best practices' in an effort to help people keep their Mudlet scripts and packages clean, efficient, and running smoothly. Another way to think of it is a collection of tips and tricks for making Mudlet run its best. They largely fall under these categories. |
− | * Lua best practices | + | * [[#Lua|Lua best practices]] |
** Since Lua is the scripting language Mudlet makes use of, the majority of the best practices for Lua will apply when writing Mudlet scripts. | ** Since Lua is the scripting language Mudlet makes use of, the majority of the best practices for Lua will apply when writing Mudlet scripts. | ||
** We do not aim to replace the internet as a source of Lua knowledge, but will try to highlight the most beneficial ones | ** We do not aim to replace the internet as a source of Lua knowledge, but will try to highlight the most beneficial ones | ||
** Potential exceptions will be noted with their entries | ** Potential exceptions will be noted with their entries | ||
− | * GUI best practices | + | * [[#GUI|GUI best practices]] |
** keeping your UI looking it best in as many situations as possible | ** keeping your UI looking it best in as many situations as possible | ||
− | * Mudlet best practices | + | * [[#Mudlet|Mudlet best practices]] |
** Optimizations and items specific to Mudlet's triggers, aliases, API, etc | ** Optimizations and items specific to Mudlet's triggers, aliases, API, etc | ||
+ | * [[#Generic_Mapper|Generic mapper best practices]] | ||
== Lua == | == Lua == | ||
Line 20: | Line 22: | ||
You should really be using local variables wherever you can. | You should really be using local variables wherever you can. | ||
If a variable is only needed for the duration of a function, alias, or trigger, then make it a local every time. | If a variable is only needed for the duration of a function, alias, or trigger, then make it a local every time. | ||
+ | If you will need to access it repeatedly in your script, make it local first. | ||
+ | Always be asking yourself, "Could I be using a local instead?" | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
Line 35: | Line 39: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | There are three main reasons for doing so | + | There are three main reasons for doing so: |
− | * Making things local keeps them from colliding with | + | * Making things local keeps them from colliding with variables in use by other package developers. You are probably not the first person to use "tbl" for a temporary table name. |
* You can make the code easier to both read and write. | * You can make the code easier to both read and write. | ||
− | ** | + | ** <code>self.mc[tabName]</code> is a bit much to type and remember, but if you first do <code>local console = self.mc[tabName]</code> then it's obvious you're referring to a console where you use it in future |
− | * It's faster | + | * It's faster: |
** local variables are inherently faster in Lua, as they exist in the virtual machine registers and are a simple index lookup to access, whereas globals reside in a table and are therefore a hash lookup | ** local variables are inherently faster in Lua, as they exist in the virtual machine registers and are a simple index lookup to access, whereas globals reside in a table and are therefore a hash lookup | ||
*** this means you can increase the speed of intensive processes just by making functions and variables local before using them. | *** this means you can increase the speed of intensive processes just by making functions and variables local before using them. | ||
− | *** For instance, if you are going to cycle through a large table and cecho items from within it, adding | + | *** For instance, if you are going to cycle through a large table and cecho items from within it, adding <code>local cecho = cecho</code> above the loop can improve performance. |
+ | |||
+ | === Conversely, avoid _G when possible === | ||
+ | |||
+ | It's not only good practice to use local variables where possible, but you should avoid referencing the global table, _G. The very thing which makes _G useful (being able to reference elements if you don't know their name before hand) is what makes it dangerous, as it can be easy to overwrite things you shouldn't and leave yourself in a broken state. For instance, all it takes is accidentally writing to _G["send"] and suddenly all of your scripts which send commands to the game are broken. | ||
+ | |||
+ | Get into a habit of using a namespace for your scripts instead - see below. | ||
+ | |||
+ | === Avoid overwriting your variables accidentally === | ||
+ | |||
+ | Often times you need to initialize your tables at the top of your scripts. But if you click on that script again later and back off it will save and overwrite your current data. You can use 'or' to avoid this as shown below. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- have to make a table before you can add things to it | ||
+ | |||
+ | -- risks overwriting the entire table if I resave the script | ||
+ | demonnic = {} | ||
+ | |||
+ | -- if the variable has already been defined, it is just set back to itself. If it has not, then it's set to a new table | ||
+ | demonnic = demonnic or {} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | If you wish for parts of your script to not be executed when clicking on a script, you may use a guard variable to only code execute once during some initialization step. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- Include at the top of your script. | ||
+ | -- If MyScript exists, isInitialized will remain true (as set at the bottom), otherwise MyScript will be initialized as | ||
+ | -- a table with the variable isInitialized set to false | ||
+ | MyScript = MyScript or { | ||
+ | isInitialized = false | ||
+ | } | ||
+ | |||
+ | if not MyScript.isInitialized then | ||
+ | -- Execute code that you do *not* want to be executed each time the script is clicked on, edited, or saved | ||
+ | end | ||
+ | |||
+ | -- Include at the bottom of your script to mark your script as initialized | ||
+ | MyScript.isInitialized = true | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Ternary statements in Lua (set a variable based on a condition) === | ||
+ | |||
+ | Lua does not have a ternary operator as such, but you can construct a statement in such a way it functions as one, allowing for shorter and more readable code. For example: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- you can rewrite this if statement | ||
+ | if something then | ||
+ | myVariable = "x" | ||
+ | else | ||
+ | myVariable = "y" | ||
+ | end | ||
+ | |||
+ | -- as | ||
+ | myVariable = something and "x" or "y" | ||
+ | </syntaxhighlight> | ||
=== Group your globals together in a table === | === Group your globals together in a table === | ||
Line 49: | Line 107: | ||
Sometimes you have to use globals, to pass values easily between items or make functions and values available to others. Lots of package makers may have a reason to track health and want to be able to pass it easily between triggers and functions without having to make it a parameter for every function they write. Obviously they can't all use the 'health' variable though, especially if one is using percentages and the other direct values. So it's best if you keep the variables for your package grouped together within a table. "Demonnic.health" is a lot less likely to collide than just 'health' There is a [https://forums.mudlet.org/viewtopic.php?f=8&t=1211 forum post] with more information. | Sometimes you have to use globals, to pass values easily between items or make functions and values available to others. Lots of package makers may have a reason to track health and want to be able to pass it easily between triggers and functions without having to make it a parameter for every function they write. Obviously they can't all use the 'health' variable though, especially if one is using percentages and the other direct values. So it's best if you keep the variables for your package grouped together within a table. "Demonnic.health" is a lot less likely to collide than just 'health' There is a [https://forums.mudlet.org/viewtopic.php?f=8&t=1211 forum post] with more information. | ||
− | === | + | === Declaring functions in tables: myTable:function() vs myTable.function() === |
Lua does not have as much syntactic sugar as some other languages, but using a : to call a function within a table is one of them. The following declarations are functionally identical | Lua does not have as much syntactic sugar as some other languages, but using a : to call a function within a table is one of them. The following declarations are functionally identical | ||
Line 76: | Line 134: | ||
You can use this to make it easier to keep your code self-contained and grouped together. You can combine it with setmetatable() to allow a sort of Object Oriented like programming pattern. Which leads us to | You can use this to make it easier to keep your code self-contained and grouped together. You can combine it with setmetatable() to allow a sort of Object Oriented like programming pattern. Which leads us to | ||
− | === Create objects and 'classes' using metatables and : === | + | === Create objects and 'classes' using metatables and: === |
Geyser makes extensive use of this pattern and it allows you to create classes and objects within Lua's procedural environment. The [https://www.lua.org/pil/16.html chapter in Programming in Lua] on Object Oriented Programming does a good job of explaining it. | Geyser makes extensive use of this pattern and it allows you to create classes and objects within Lua's procedural environment. The [https://www.lua.org/pil/16.html chapter in Programming in Lua] on Object Oriented Programming does a good job of explaining it. | ||
− | ==GUI== | + | == GUI == |
+ | |||
+ | === Give your Geyser elements unique names === | ||
+ | |||
+ | While Geyser will generate a name for you if you do not provide one, if you do provide one it gives you some measure of protection against accidental duplication, as miniconsoles and labels with the same name are reused. Likewise, you should make sure you use names which are unlikely to be used by anyone else. "healthgauge" or "container" are maybe a little generic. Try "my_package_name_health_gauge" or similar. It's more typing up front, but you shouldn't need to type it again and it will help a lot in ensuring your UI behaves as expected. | ||
=== Use percentages === | === Use percentages === | ||
Line 90: | Line 152: | ||
If you use Adjustable.Container for groupings of your UI it allows your users to resize and move them around if they don't like the way you've arranged things. This means more people using your package in the end. | If you use Adjustable.Container for groupings of your UI it allows your users to resize and move them around if they don't like the way you've arranged things. This means more people using your package in the end. | ||
− | ==Mudlet== | + | == Mudlet == |
=== Shield your complicated regular expression triggers with a substring trigger === | === Shield your complicated regular expression triggers with a substring trigger === | ||
− | Regular expressions are very useful, but are one of the slower trigger types to process for Mudlet. Using a substring trigger gate or a multiline trigger with the substring trigger ahead of the regular expression and a line delta of 0 you can reduce the processing time of your triggers overall by quite a bit. There has been | + | : Regular expressions are very useful, but are one of the slower trigger types to process for Mudlet. Using a substring trigger gate or a multiline trigger with the substring trigger ahead of the regular expression and a line delta of 0 you can reduce the processing time of your triggers overall by quite a bit. There has been [https://forums.mudlet.org/viewtopic.php?f=12&t=1441 benchmarking done] |
+ | |||
+ | === Avoid <code>expandAlias()</code> where possible === | ||
+ | |||
+ | : There's a whole wiki page on this at [[Manual:Functions_vs_expandAlias|Functions_vs_expandAlias]] | ||
+ | |||
+ | === feedTriggers is for testing, not production === | ||
+ | |||
+ | : the c/d/h/feedTriggers functions are great for testing your triggers without having to make something actually happen in your game. But you should not be using it as a functional part of your system. Much like with expandAlias, you should be calling a function from the trigger and the place you are calling feedTriggers to do the same thing, rather than getting the trigger engine itself involved. | ||
+ | |||
+ | === Capture the IDs returned by your temp items and anonymous event handlers === | ||
+ | |||
+ | : You can now skip the following by using [[Manual:Lua_Functions#registerNamedEventHandler|registerNamedEventHandler]] or [[Manual:Lua_Functions#registerNamedTimer|registerNamedTimer]] instead. The below still holds true if you want to use the anonymous functions directly and is left for that reason. | ||
+ | : One of the more common issues people come into the help channel with is stacking tempTimers and old anonymous event handlers which have been edited not being cleared out and still firing. The solution for these issues is the same. The functions which create temporary or anonymous items in Mudlet return an ID. You need to save this ID to a variable and use it to remove the existing item before reregistering it. So for examples | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- incorrect, can stack timers | ||
+ | tempTimer(0.5, function() | ||
+ | send("And another one") | ||
+ | end) | ||
+ | |||
+ | -- Ensures the timer only fires once, .5 seconds after the first time it is called | ||
+ | if not myTempTimerID then -- only do something if we haven't already | ||
+ | myTempTimerID = tempTimer(0.5, function() | ||
+ | send("Only one") | ||
+ | myTempTimerID = nil -- unset it so it can fire again | ||
+ | end) | ||
+ | end | ||
+ | |||
+ | -- Resets the tempTimer each time it is run, so it runs | ||
+ | if myTempTimerID then | ||
+ | killTimer(myTempTimerID) -- can run killTimer on an already fired timer without causing an issue. | ||
+ | end | ||
+ | myTempTimerID = tempTimer(0.5, function() | ||
+ | send("Only one") | ||
+ | end) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Package and Module best practices === | ||
+ | |||
+ | : This is a list of things we have discovered over the years leads to the best user experience with Mudlet packages. They aren't all necessary, but you will find that the more of them you do, the easier the lives of your users and ultimately yourself. | ||
+ | |||
+ | * utilize the Mudlet Package Repository for updates | ||
+ | ** so players stay up to date, which will not be the case with manual updates) | ||
+ | * modular and extendable - use Mudlet modules for this | ||
+ | ** so people can disable ones not needed) | ||
+ | * hook into the sysInstallPackage event to give some introductory text about the package they just installed | ||
+ | * event based - raise events of your own for others to hook into | ||
+ | ** so the order of scripts installed in the players profile doesn't matter | ||
+ | * make sure aliases only call functions | ||
+ | ** so people can make their own aliases or keybindings to customize as needed without using expandAlias | ||
+ | * don't pollute the global namespace, keep everything in a table | ||
+ | ** so you don't get bugs from people overwriting it or vice versa) | ||
+ | ** touched on above but bears repeating | ||
+ | * undo any UI changes on uninstall: set borders back, hide the UI, etc | ||
+ | ** so people have a good experience even if they didn't like the package | ||
+ | * if you're specifying any fonts, package them | ||
+ | ** while it might be available on your computer, not guaranteed to be available on every computer | ||
+ | * if you're a game admin, install it automatically via GMCP or via the Mudlet Package Repository | ||
+ | ** less overhead for players to get a good experience | ||
+ | * if you're a game admin, provide GMCP data for your game | ||
+ | ** so people don't have to waste time trying to capture data, and can work with it instead | ||
+ | * don't use \ in paths, use / - even for Windows | ||
+ | ** it'll work on Windows - and work on macOS and Linux too | ||
− | == | + | == Generic Mapper == |
− | + | * when adding customisation triggers to the generic mapper, add them outside of the <code>generic_mapper</code> folder | |
+ | ** this is so when the mapper updates, you keep your changes |
Latest revision as of 13:48, 25 September 2024
Best Practices
- The hope is that this page will provide a place to collect a list of Mudlet 'best practices' in an effort to help people keep their Mudlet scripts and packages clean, efficient, and running smoothly. Another way to think of it is a collection of tips and tricks for making Mudlet run its best. They largely fall under these categories.
- Lua best practices
- Since Lua is the scripting language Mudlet makes use of, the majority of the best practices for Lua will apply when writing Mudlet scripts.
- We do not aim to replace the internet as a source of Lua knowledge, but will try to highlight the most beneficial ones
- Potential exceptions will be noted with their entries
- GUI best practices
- keeping your UI looking it best in as many situations as possible
- Mudlet best practices
- Optimizations and items specific to Mudlet's triggers, aliases, API, etc
- Generic mapper best practices
Lua
Use local variables
You should really be using local variables wherever you can. If a variable is only needed for the duration of a function, alias, or trigger, then make it a local every time. If you will need to access it repeatedly in your script, make it local first. Always be asking yourself, "Could I be using a local instead?"
-- bad
function myEcho(msg)
transformed_msg = "<cyan>(<yellow>Highlighter<cyan>)<reset>" .. msg
cecho(transformed_msg)
end
-- better
function myEcho(msg)
local transformed_msg = "<cyan>(<yellow>Highlighter<cyan>)<reset>" .. msg
cecho(transformed_msg)
end
There are three main reasons for doing so:
- Making things local keeps them from colliding with variables in use by other package developers. You are probably not the first person to use "tbl" for a temporary table name.
- You can make the code easier to both read and write.
self.mc[tabName]
is a bit much to type and remember, but if you first dolocal console = self.mc[tabName]
then it's obvious you're referring to a console where you use it in future
- It's faster:
- local variables are inherently faster in Lua, as they exist in the virtual machine registers and are a simple index lookup to access, whereas globals reside in a table and are therefore a hash lookup
- this means you can increase the speed of intensive processes just by making functions and variables local before using them.
- For instance, if you are going to cycle through a large table and cecho items from within it, adding
local cecho = cecho
above the loop can improve performance.
- local variables are inherently faster in Lua, as they exist in the virtual machine registers and are a simple index lookup to access, whereas globals reside in a table and are therefore a hash lookup
Conversely, avoid _G when possible
It's not only good practice to use local variables where possible, but you should avoid referencing the global table, _G. The very thing which makes _G useful (being able to reference elements if you don't know their name before hand) is what makes it dangerous, as it can be easy to overwrite things you shouldn't and leave yourself in a broken state. For instance, all it takes is accidentally writing to _G["send"] and suddenly all of your scripts which send commands to the game are broken.
Get into a habit of using a namespace for your scripts instead - see below.
Avoid overwriting your variables accidentally
Often times you need to initialize your tables at the top of your scripts. But if you click on that script again later and back off it will save and overwrite your current data. You can use 'or' to avoid this as shown below.
-- have to make a table before you can add things to it
-- risks overwriting the entire table if I resave the script
demonnic = {}
-- if the variable has already been defined, it is just set back to itself. If it has not, then it's set to a new table
demonnic = demonnic or {}
If you wish for parts of your script to not be executed when clicking on a script, you may use a guard variable to only code execute once during some initialization step.
-- Include at the top of your script.
-- If MyScript exists, isInitialized will remain true (as set at the bottom), otherwise MyScript will be initialized as
-- a table with the variable isInitialized set to false
MyScript = MyScript or {
isInitialized = false
}
if not MyScript.isInitialized then
-- Execute code that you do *not* want to be executed each time the script is clicked on, edited, or saved
end
-- Include at the bottom of your script to mark your script as initialized
MyScript.isInitialized = true
Ternary statements in Lua (set a variable based on a condition)
Lua does not have a ternary operator as such, but you can construct a statement in such a way it functions as one, allowing for shorter and more readable code. For example:
-- you can rewrite this if statement
if something then
myVariable = "x"
else
myVariable = "y"
end
-- as
myVariable = something and "x" or "y"
Group your globals together in a table
Sometimes you have to use globals, to pass values easily between items or make functions and values available to others. Lots of package makers may have a reason to track health and want to be able to pass it easily between triggers and functions without having to make it a parameter for every function they write. Obviously they can't all use the 'health' variable though, especially if one is using percentages and the other direct values. So it's best if you keep the variables for your package grouped together within a table. "Demonnic.health" is a lot less likely to collide than just 'health' There is a forum post with more information.
Declaring functions in tables: myTable:function() vs myTable.function()
Lua does not have as much syntactic sugar as some other languages, but using a : to call a function within a table is one of them. The following declarations are functionally identical
function myTable:coolFunction(parameter)
self.thing = parameter
end
function myTable.coolFunction(self, parameter)
self.thing = parameter
end
myTable.coolFunction = function(self, parameter)
self.thing = parameter
end
And these calls are equivalent
myTable:coolFunction("Test")
myTable.coolFunction(myTable, "Test")
You can use this to make it easier to keep your code self-contained and grouped together. You can combine it with setmetatable() to allow a sort of Object Oriented like programming pattern. Which leads us to
Create objects and 'classes' using metatables and:
Geyser makes extensive use of this pattern and it allows you to create classes and objects within Lua's procedural environment. The chapter in Programming in Lua on Object Oriented Programming does a good job of explaining it.
GUI
Give your Geyser elements unique names
While Geyser will generate a name for you if you do not provide one, if you do provide one it gives you some measure of protection against accidental duplication, as miniconsoles and labels with the same name are reused. Likewise, you should make sure you use names which are unlikely to be used by anyone else. "healthgauge" or "container" are maybe a little generic. Try "my_package_name_health_gauge" or similar. It's more typing up front, but you shouldn't need to type it again and it will help a lot in ensuring your UI behaves as expected.
Use percentages
A lot of your users will be on different resolution monitors, or may not want to have Mudlet covering their entire screen. If you hardcode your sizes as direct pixel values for your Geyser objects, they will not scale as well when the window is resized due to resolution restrictions or user preference. If you're personally working on a 1080p resolution UI but you want it to scale up or down, rather than using "640px" as the width to cover a third of the screen use "33%" for the width, and it will resize to cover a third of the window no matter the size.
Adjustable Containers put the power back in your user's hands
If you use Adjustable.Container for groupings of your UI it allows your users to resize and move them around if they don't like the way you've arranged things. This means more people using your package in the end.
Mudlet
Shield your complicated regular expression triggers with a substring trigger
- Regular expressions are very useful, but are one of the slower trigger types to process for Mudlet. Using a substring trigger gate or a multiline trigger with the substring trigger ahead of the regular expression and a line delta of 0 you can reduce the processing time of your triggers overall by quite a bit. There has been benchmarking done
Avoid expandAlias()
where possible
- There's a whole wiki page on this at Functions_vs_expandAlias
feedTriggers is for testing, not production
- the c/d/h/feedTriggers functions are great for testing your triggers without having to make something actually happen in your game. But you should not be using it as a functional part of your system. Much like with expandAlias, you should be calling a function from the trigger and the place you are calling feedTriggers to do the same thing, rather than getting the trigger engine itself involved.
Capture the IDs returned by your temp items and anonymous event handlers
- You can now skip the following by using registerNamedEventHandler or registerNamedTimer instead. The below still holds true if you want to use the anonymous functions directly and is left for that reason.
- One of the more common issues people come into the help channel with is stacking tempTimers and old anonymous event handlers which have been edited not being cleared out and still firing. The solution for these issues is the same. The functions which create temporary or anonymous items in Mudlet return an ID. You need to save this ID to a variable and use it to remove the existing item before reregistering it. So for examples
-- incorrect, can stack timers
tempTimer(0.5, function()
send("And another one")
end)
-- Ensures the timer only fires once, .5 seconds after the first time it is called
if not myTempTimerID then -- only do something if we haven't already
myTempTimerID = tempTimer(0.5, function()
send("Only one")
myTempTimerID = nil -- unset it so it can fire again
end)
end
-- Resets the tempTimer each time it is run, so it runs
if myTempTimerID then
killTimer(myTempTimerID) -- can run killTimer on an already fired timer without causing an issue.
end
myTempTimerID = tempTimer(0.5, function()
send("Only one")
end)
Package and Module best practices
- This is a list of things we have discovered over the years leads to the best user experience with Mudlet packages. They aren't all necessary, but you will find that the more of them you do, the easier the lives of your users and ultimately yourself.
- utilize the Mudlet Package Repository for updates
- so players stay up to date, which will not be the case with manual updates)
- modular and extendable - use Mudlet modules for this
- so people can disable ones not needed)
- hook into the sysInstallPackage event to give some introductory text about the package they just installed
- event based - raise events of your own for others to hook into
- so the order of scripts installed in the players profile doesn't matter
- make sure aliases only call functions
- so people can make their own aliases or keybindings to customize as needed without using expandAlias
- don't pollute the global namespace, keep everything in a table
- so you don't get bugs from people overwriting it or vice versa)
- touched on above but bears repeating
- undo any UI changes on uninstall: set borders back, hide the UI, etc
- so people have a good experience even if they didn't like the package
- if you're specifying any fonts, package them
- while it might be available on your computer, not guaranteed to be available on every computer
- if you're a game admin, install it automatically via GMCP or via the Mudlet Package Repository
- less overhead for players to get a good experience
- if you're a game admin, provide GMCP data for your game
- so people don't have to waste time trying to capture data, and can work with it instead
- don't use \ in paths, use / - even for Windows
- it'll work on Windows - and work on macOS and Linux too
Generic Mapper
- when adding customisation triggers to the generic mapper, add them outside of the
generic_mapper
folder- this is so when the mapper updates, you keep your changes