A set of guidelines to help keep your GLua code Clean and Cool π
π· For CFC Members and Contributors
Please adhere to these rules in our repositories. Your PR will be automatically blocked if it does not meet the style guidelines.
π€ For Non-CFC Members
Feel free to use these guidelines as you see fit! We believe they help maintain and share code effectively.
If you disagree with some guidelines, that's okay! The most important principle is consistency. If you choose to follow or ignore certain rules, ensure you do so consistently throughout your code. π
Use GLuaLint (GLuaFixer) as your linter. There are plugins available for most major editors.
GLuaFixer: https://github.com/FPtje/GLuaFixer
Use CFC's GLuaFixer config, found here: https://cfc.gg/configs/gluafixer/glualint.json
Good
local x = a * b + cBad
local x = a* b+cGood
local x = ( 3 * myFunc() ) + 5
local data = { 5, {} }Bad
local x = (3 * myFunc( )) + 5
local data = {5, { }}Good
myFunc( 10, { 3, 5 } )Bad
myFunc( 10,{ 3,5 } )Good
if cond then
myFunc()
endBad
if cond then
myFunc()
endGood
local val = tab[5] + tab[3]
local val2 = tab[5 * 3]Bad
local val = tab[ 5 ] + tab[ 3 ]
local val2 = tab[ 5 * 3 ]Good
-- This is a good comment
local a = 3 -- This is also goodBad
--This comment doesn't have a space before it
local a = 3-- This comment starts too close to the 3Good
local config = GM.Config
function GM:Think()
-- do thing
endBad
local config = GM.Config
function GM:Think()
-- do thing
endGood
local config = GM.Config
function GM:Think()
-- do thing
endBad
local config = GM.Config
function GM:Think()
-- do thing
endGood
function test()
local a = 3
print( a )
endBad
function test()
local a = 3
print( a )
endGood
function test()
local a = 3
return a
end
function test2()
return 3
endBad
function test()
local a = 3
return a
endGood
function CFCNotifications.resolveFilter( filter )
if type( filter ) == "Player" then
filter = { filter }
end
if type( filter ) == "table" then
filter = fWrap( filter )
end
filter = filter or player.GetAll
local players = filter()
if type( players ) == "Player" then
players = { players }
end
if not players then players = player.GetAll() end
return players
endBad, far too dense and difficult to read at a glance
function CFCNotifications.resolveFilter( filter )
if type( filter ) == "Player" then
filter = { filter }
end
if type( filter ) == "table" then
filter = fWrap( filter )
end
filter = filter or player.GetAll
local players = filter()
if type( players ) == "Player" then
players = { players }
end
if not players then players = player.GetAll() end
return players
endGood
if a ~= b and not b then endBad, Garry's operators aren't recognized by standard Lua highlighting
if a != b && !b then endGood
--[[
This line does stuff
]]
do( stuff ) -- Stuff being doneBad, gmod comments aren't recognized by standard Lua highlighting
/*
This line does stuff
*/
do( stuff ) // Stuff being doneGood
for k, v in pairs( tab ) do
if IsValid( v ) then
v:Remove()
end
endBad, garry's continue is a flawed implementation that is prone to errors when used in repeat-until loops
for k, v in pairs( tab ) do
if not IsValid( v ) then continue end
v:Remove()
endIf your condition is more complicated, consider deferring the logic to another function
local function processItem( item )
if not IsValid( item ) then return end
if not item:IsReady() then return end
item:Process()
end
for _, item in ipairs( tab ) do
processItem( item )
endGood
local myVariable = 10Bad
local MyVariable = 10
local my_variable = 20Good
CATEGORY_NAME = "nothing"
local MAXIMUM_VALUE = 25Bad
CategoryName = "nothing"
categoryName = "nothing"
category_name = "nothing"
local maximumValue = 25Good
GlobalVariable = 10Bad
globalVariable = 20
global_variable = 20Good
function classTable:SetHealth( amount )
endBad
function classTable:setHealth( amount )
endGood
myTable.myValue = 4Bad, accessing the table value requires brackets and quotes because of the hyphen
myTable["my-value"] = 4Good
for _, ply in pairs( player.GetAll() ) do
local _, shouldKill = ply:GetKillData()
if shouldKill then
ply:Kill()
end
endBad, k isn't used
for k, ply in pairs( player.GetAll() ) do
local canKill, shouldKill = ply:GetKillData()
if shouldKill then
ply:Kill()
end
end- Hook identifiers should be named as such
Organization_AddonName_HookPurpose - The hook event name should not be included in the identifier.
For example, you should not do
ORG_MyAddon_OnDisconnector evenORG_MyAddon_CleanupPropsOnDisconnect. ButORG_MyAddon_CleanupPropswould be appropriate. The "HookPurpose" should state what a function does without restating information provided by the event name. - Hook event names should be named as such
Organization_EventName
Good, quote usage is consistent
myFunc( "hello ", "world!" )Bad, different quotes are used interchangeably
myFunc( "hello ", 'world!' )Good
if x == y thenBad, these parenthesis are not necessary
if ( x == y ) thenRedundant parenthesis may be used if it makes the order of operations clear. The author should use their best judgement
It's important for the author to remember who their target audience is. Not every reader will be good with maths, and it can be helpful to make equations easy to follow
Acceptable
local amount = ( quantity * modifier ) + baseAmountUnsavory, but fine
local amount = quantity * modifier + baseAmountElements should begin on the next line and the last line should contain only a closing bracket. Elements inside should be indented once
Good
tbl = {
v1 = c,
v2 = d,
v3 = k,
v4 = j,
v5 = y
}Fine, if both the keys and values are short, it's acceptable to keep them in one line
tbl = { v1 = c, v2 = d, v3 = k, v4 = j, v5 = y }Bad, the indentation is inconsistent
tbl = { v1 = v, v2 = d,
v3 = k, v4 = j }Good
myFunc(
"First arg",
secondArg,
{ third, arg }
)Bad, this indentation is inconsistent (and objectively wrong)
myFunc( "First arg",
secondArg, { third, arg } )Good
function test()
if not myThing then return end
-- Do a bunch of real complicated things
endBad, adds an extra level of indentation for no reason
function test()
if myThing then
-- Do a bunch of real complicated things
end
endGood
local maxX = 25
function checkX( x )
return x > maxX
endBad, the significance of 25 is unknown without a meaningful variable name
function checkX( x )
return x > 25
endGood, each step of the equation is named and done individually. The math is easy to follow
local widthModifier = amount * damageMult
local age = 1 - lifetime / duration
local width = widthModifier * ageBad, this math is difficult to figure out from a glance
local width = ( amount * 5 ) * ( 1 - lifetime / duration )Semicolons provide no functional value in Lua. While they can be used to delimit table items, a comma is preferred
Good
local a = 3
print( a )
return aBad, this exclusively makes the code harder to read
local a = 3; print( a );
return a;Good
x = y * math.pi
radians = math.rad( deg )Bad, the meaning of 3.142 may not be immediately clear. It also suffers a loss in precision compared to math.pi
x = y * 3.142
radians = deg * ( 3.142 / 180 )Good, each check is clear and it's easy to follow the reasoning
local entValid = IsValid( ent )
local entOwner = ent:CPPIGetOwner()
local ownerVehicleIsVisible = entOwner:GetVehicle():GetColor().a > 0
local ownerCapable = entOwner:IsAdmin() or entOwner.canDoThing
if entValid and ownerVehicleIsVisible and ownerCapable then
-- doThing
endBad, the conditions are difficult to follow
if IsValid( ent ) and ent:GetCPPIOwner():GetVehicle():GetColor().a > 0
and ( ent:CPPIGetOwner():IsAdmin() or ent:GetCPPIOwner().canDoThing ) then
-- do thing
endGood
if IsValid( ent ) and ent:IsPlayer() and ent:IsAdmin() then
local hue = ( CurTime() * 10 ) % 360
local color = HSVToColor( hue, 1, 1 ) )
ent:SetColor( color )
endBad, this line is too long and could easily be split into multiple, easy-to-follow lines
if IsValid( ent ) and ent:IsPlayer() and ent:IsAdmin() then ent:SetColor( HSVToColor( ( CurTime() * 10 ) % 360, 1, 1 ) ) ) endWhile you can omit a zero in decimals in the range of 0-1, it's an antipattern and a one-off exception when compared to decimals outside of said range
Good
local num = 0.69Bad, while the 0 is implied, it can make it harder to parse at a glance
local num = .69Lua numbers can technically be prefixed with as many 0s as you'd like, but in most cases it's completely unnecessary
Good
local factor = 420Bad, just why?
local factor = 00420Except in cases where the trailing 0s make it easier to understand the values (e.g. when you have a column of configurable decimals with varying precision), there is no reason to include them and could confuse the reader
Good
local decimal = 0.2Bad
local decimal = 0.200Good variable and function names can make comments unecessary. Strive for self commenting code. Save comments for complicated code that may not be clear on its own
Good
for _, ply in pairs( players ) do
print( ply )
if ply:Alive() then ply:Kill() end
endBad, the code explains itself without comments
for _, v in pairs( stuff ) do -- loop through players
print( v ) -- print the player
if v:Alive() then v:Kill() end -- kill player if player is alive
endWhile these functions are relatively low-cost on their own, they still add overhead in places where it may not be necessary
Developers have been conditioned to pepper these checks anywhere-and-everywhere to avoid Attempt to index NULL errors.
If you know an Entity can become NULL, first try to remedy the underlying issue that causes it. Only rely on IsValid if the situation is outside of your control (i.e. delayed timer actions).
Good, IsValid is only used in pieces of code that we can't avoid, and isn't used in places that add no value
net.Receive( "my_net_message", function( _, ply )
-- No IsValid check is needed here, Players cannot become invalid at this point
ply:ChatPrint( "Starting the process now..." )
local steamID64 = ply:SteamID64()
timer.Create( "delayed_action_" .. steamID64, 5, 1, function()
-- IsValid check is required here because the Player may have disconnected before this timer triggers
if not IsValid( ply ) then return end
ply:DoAction()
timer.Create( "delayed_action_step2_" .. steamID64, 5, 1, function()
-- No IsValid check is needed here because we already saved the data we need from their Player object
Database:UpdatePlayer( steamID64, true )
end )
end )
end )Bad, IsValid is used in a hook where it's impossible for the Player to have become invalid
hook.Add( "PlayerCanSuicide", "check_suicide", function( ply )
if not IsValid( ply ) then return end
if ply:IsAdminFrozen() then return false end
end )print accepts any number of varargs, and the values will be automatically spaced out and coerced into strings
Bad, uses a lot of string concatenation and many tostring calls, difficult to read at a glance
local function botDebugInfo( bot )
print( tostring( bot ) .. " Health: " .. bot:Health() .. " Pos: " .. tostring( bot:GetPos() ) .. " Alive: " .. tostring( bot:Alive() ) .. " Weapon: " .. tostring( bot:GetActiveWeapon() ) .. " Value: " .. tostring( bot:GetNW2Var( "test", nil ) ) )
endGood, uses print's vararg feature with string casting and auto-spacing
As an added benefit, it's also able to be split by newlines for easier reading
local function botDebugInfo( bot )
print(
bot,
"Health:", bot:Health(),
"Pos:", bot:GetPos(),
"Alive:", bot:Alive(),
"Weapon:", bot:GetActiveWeapon(),
"Value:", bot:GetNW2Var( "test" )
)
endWarning
The only downside to printing this way is that a tab character is inserted between each entry. This is fine if you're only printing a couple of values, but can get difficult to follow if you're printing multiple values.
You can mitigate this issue by using spacer characters:
local function botDebugInfo( bot )
print(
bot,
"Health:", bot:Health(), "|",
"Pos:", bot:GetPos(), "|",
"Alive:", bot:Alive(), "|",
"Weapon:", bot:GetActiveWeapon(), "|"
"Value:", bot:GetNW2Var( "test" )
)
endWith spacers:
Player [1][Bot01] Health: 100 | Pos: -642.174744 445.843323 -151.074295 | Alive: true | Weapon: Weapon [193][weapon_crowbar] | Value: nil
Without spacers:
Player [1][Bot01] Health: 100 Pos: -5225.150391 1630.958984 -95.968750 Alive: true Weapon: Weapon [193][weapon_crowbar] Value: nil
Good, relies on the built-in feature of Lua string concatenation
for i = 1, #lines do
local line = lines[i]
print( "Line #" .. i, line )
endBad, tostring simply isn't necessary when you're 100% sure that the value is a number
for i = 1, #lines do
local line = lines[i]
print( "Line #" .. tostring( i ), line )
end