Developer Guidance
Developer Guidance
While not every tip here will be useful in every situation, this document provides an overview of good practices, which will make your developer experience easier, your products less likely to break and your customers happier.
Utilise Lua Refresh
Your code being compatible with Lua refresh means that configuration changes won’t require a server restart, and you can develop new features and fixes without constantly restarting your environment. While not every server will use Lua refresh, being compatible with it make it easier for both customers and development.
Third Party Libraries
3rd-party libraries are made available by numerous developers, aimed at making the process of creating addons easier by providing access to helper functions. If you wish to use a 3rd-party library; read through the documentation for that library to see what it gives you the ability to do.
Gmodstore does not provide support or any warranty for addons that may utilise a 3rd-party library. If you decide to use a library; it is at your sole discretion. Please ensure that you follow the licences attached libraries you decide to use.
Some third party libraries include:
JSON Compression
If you use compressed JSON to send a large amount of data over the net library, it is important to be aware of an exploit with this method.
Please see the relevant links: https://wiki.facepunch.com/gmod/Net_Library_Usage#compression
Using pairs vs ipairs
When iterating through a table, in some cases you can use ipairs, if the table is numerically keyed (only using numbers) and sequential (keys start with 1, 2, 3 without gaps) you can use ipairs, which is 4 times faster than pairs. You could also use a for loop, which is about the same speed as ipairs is. This is especially important in commonly called functions, such as think or paint hooks.
Using a for loop:
local myTable = {"gmodstore", "addon", "guidelines"} for i = 1, #myTable do print(myTable[i]) end
Using ipairs:
local myTable = {"gmodstore", "addon", "guidelines"} for i, word in ipairs(myTable) do print(word) end
Global Table Structure
Your global table should be structured in order to facilitate easier development. For example, for a product named forge, written by Internet, the table could be named internet_forge.
Inside that table should be entries for configuration (internet_forge.cfg, for example). This allows all relevant product data to be contained in a single table, and is less likely to conflict with others.
internet_forge = internet_forge or {} internet_forge.cfg = internet_forge.cfg or {} internet_forge.cfg.server_name = "Cool Server” function internet_forge:PrintMessage() print(“Welcome to “ .. self.cfg.server_name) end
Avoid Nesting
Deeply nested ifs and loops become harder to read and understand, utilising sub-functions, returns and continues can be used to avoid deeply nesting your statements.
Comments
Commenting isn’t required, but can be very useful for developers. Docblocks can be used to automatically create documentation, inline comments can be used to explain why actions are taken or complex bits of code. Using comments as a tool can be easier for you, if you return to add features or fixes, for end users if they make their own modifications and for curators.
Table and Network Sizes
Tables which have no size limit / infinitely grow can take infinite amounts of memory; Garry’s Mod cannot use infinite memory, so ensuring that your tables have a size limit is important
Profiling Tools
Profiling tools / benchmarks can be used to identify poorly performing code, and create alternatives.
- FProfiler identifies bottlenecks and can be run on specific functions.
- DBugR can also profile network activity.
- The Lua Guide provides benchmarking information, providing a before / after comparison on code.
- InterBenchmark can also be used to provide benchmarks.
Constant / Pre-existing Global Variables
A number of constant global variables are created by the game, which can be used by your products. You can view the list of constant variables here. This includes built-in Vector, Angle and Color variables. You should consider using them instead of creating your own duplicated variable.
surface.SetDrawColor Optimisation
surface.SetDrawColor is a function in the surface library whose objective is to set the color for any future shape to be drawn. This is often used for UI stuff, in functions launched every frame.
As written in the wiki page, the function can be used in two ways :
- By providing a Color object
- By providing four numbers
It appears that using four numbers is faster (by a power of 10) than providing a Color object. However, providing four numbers is less generic, makes the code harder when you want to add a theme feature to your addon (as it's not a Color object). We give you the choice between:
- Using four numbers as you prefer performance over readability/portability
- Unpacking / destructing the Color object in Lua (surface.SetDrawColor(color.r, color.g, color.b))
- Using a Color object - in this case the Color object needs to be cached!
Table Structure
Tables in Lua serve multiple purposes, similar to PHP's arrays; serving as dictionaries, maps and lists. Because of this, inefficiently accessing tables is common, hence the reason for the ipairs development guideline.
When searching tables for values, using a numeric table with table.HasValue ({"admin", "superadmin"}
) will be half the speed of a key lookup table ({["admin"] = true, ["superadmin"] = true}
).
Because of this, where you have the option to, you should use a lookup, such as below:
local allowedRanks = { admin = true, superadmin = true, owner = true } hook.Add("DoSomething", "OnlyAllowAdminsToDoStuff", function(ply) if allowedRanks[ply:GetRank()] then print("Player is allowed to do something.") return true else print("Player is not allowed to do something!") return false end end)
Network Optimisation
Use net.WriteUInt / net.WriteInt with the correct numberOfBits argument if possible.
Use the correct networking function for your datatype:
- Positive-only integers should use WriteUInt.
- Positive-or-negative integers should use WriteInt
- Decimal values should use WriteFloat
Using the incorrect function could use more networking traffic than is required.
Avoid sending strings that the client already knows, ie. localisation data:
util.AddNetworkString('NotifyMe') local lang = { ["UserError"] = "There was a error, please try again!", ["UserErrorTimeout"] = "There was an error, please try again in %s seconds!" } // Bad example if SERVER then net.Start('NotifyMe') net.WriteString(lang.UserError) net.Broadcast() net.Start('NotifyMe') net.WriteString(string.format(lang.UserErrorTimeout, 10)) net.Broadcast() else net.Receive('NotifyMe', function() local str = net.ReadString() print(str) end) end // Better Example if SERVER then net.Start('NotifyMe') net.WriteString("UserErrorTimeout") net.WriteString("10") net.Broadcast() else net.Receive('NotifyMe', function() local key = net.ReadString() print(string.format(lang[key], net.ReadString())) end) end
There are multiple ways of networking the error, without just sending the full text across the network. Combined with more optimised networking functions, you could also make the keys a lookup table, so that you send a numeric ID across, instead of a string.