Manual:Best Practices

From Mudlet
Jump to navigation Jump to search


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

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

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.
  • built-in auto-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)
  • 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 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
    • 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