RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Lua Classes & MetatablesLua Coding StandardsProven GE PatternsAnti-Patterns to AvoidOverriding Lua ModulesCleaning Up AI-Generated CodeError Handling & Defensive CodingPerformance OptimizationLua Modding FAQ

Reference

UI

Resources

BeamNG Game Engine Lua Cheat SheetGE Developer RecipesMCP Server Setup

// RLS.STUDIOS=true

Premium Mods for BeamNG.drive. Career systems, custom vehicles, and immersive gameplay experiences.

Index

HomeProjectsPatreon

Socials

DiscordPatreon (RLS)Patreon (Vehicles)

© 2026 RLS Studios. All rights reserved.

Modding since 2024

GuidesLua

Lua Coding Standards

Write clean, readable, maintainable Lua code. Covers naming, structure, comments, defensive coding, and Lua-specific idioms.

Good code is read 10x more than it's written. These standards help you write Lua that's easy to understand, modify, and debug — whether you're building career modules, vehicle extensions, or UI integrations.


Naming Conventions

Consistent naming makes code scannable. Follow these Lua-community conventions:

WhatConventionExample
Local variablessnake_casevehicle_count, save_path
Module methodscamelCaseM.getData, M.onVehicleSpawned
ConstantsUPPER_CASEMAX_VEHICLES, DEFAULT_FUEL
Classes/ModulesCamelCaseVehicleManager
Private/internal_underscore prefix_cachedData, _buildIndex
Throwaway vars_for _, item in ipairs(items)
-- ✅ Good
local MAX_RETRIES = 3
local save_path = currentSavePath .. "/career/myMod"
local function onVehicleSpawned(vehicleId) end

-- ❌ Bad
local MAXRETRIES = 3  -- no separation
local s = currentSavePath .. "/career/myMod"  -- cryptic
local function vehiclespawned(vid) end  -- no convention

Scope rule: Variables with larger scope need more descriptive names. Single-letter names are fine for loop counters and tiny scopes — nowhere else.

-- Fine: small scope, obvious meaning
for i = 1, #vehicles do
  local v = vehicles[i]
end

-- Not fine: large scope, cryptic name
local d = {}  -- what is d? data? directions? damage?

Function Design

Functions should do one thing. If you have to read the body to understand what a function does, the name is wrong or the function is too big.

Name functions after what they do

-- ✅ Name tells you everything
local function calculateRepairCost(vehicle, damage)
  local base_cost = damage.severity * vehicle.repairRate
  return base_cost * getDifficultyMultiplier()
end

-- ❌ Name is vague, you have to read the body
local function process(v, d)
  local c = d.severity * v.repairRate
  return c * getDifficultyMultiplier()
end

Use early returns instead of deep nesting

-- ✅ Guard clauses at the top, main logic at normal indentation
local function startDelivery(vehicleId, destination)
  if not career_career.isActive() then return end
  local veh = be:getObjectByID(vehicleId)
  if not veh then return end
  if not destination then return end

  local route = calculateRoute(veh, destination)
  beginTracking(veh, route)
end

-- ❌ Pyramid of doom
local function startDelivery(vehicleId, destination)
  if career_career.isActive() then
    local veh = be:getObjectByID(vehicleId)
    if veh then
      if destination then
        local route = calculateRoute(veh, destination)
        beginTracking(veh, route)
      end
    end
  end
end

Extract when there's a gap between intention and implementation

Don't extract just to hit a line count. Extract when a block of code needs a comment to explain what it's doing — the function name replaces the comment.

-- ✅ The function name IS the documentation
local function hasEnoughFuel(vehicle)
  local fuel = vehicle.electrics.fuel or 0
  return fuel > 0.05
end

local function canStartRace(vehicle)
  return hasEnoughFuel(vehicle) and not vehicle.damaged
end

Comment Quality

Comments should explain why, never what. If code needs a comment to explain what it does, fix the code.

Comments that add value

-- ✅ Explains WHY — not obvious from the code
local function onVehicleSpawned(_, veh)
  -- Delay setup by one frame because VE extensions aren't loaded yet on spawn
  core_jobsystem.create(function(job)
    job.sleep(0.1)
    initializeVehicle(veh:getId())
  end)
end

-- ✅ Documents a non-obvious business rule
local repair_cost = math.max(base_cost, 50)  -- minimum $50 to prevent free exploit via tiny damage

-- ✅ Links to external context
-- See: https://wiki.beamng.com/Modding/Lua_Electrics
local fuel = electrics.values.fuel or 0

Comments that are noise

-- ❌ Restates the code
local careerData = {}  -- Initialize the career data table
careerData.money = startingMoney  -- Set the player's money
return careerData  -- Return the career data

-- ❌ Restates the function name
--- Gets the vehicle data
local function getVehicleData(vehicleId)

-- ❌ Obvious from context
local veh = be:getPlayerVehicle(0)  -- Get the player's vehicle
if not veh then return end  -- Return if no vehicle

The rule: If deleting a comment loses zero information, delete it.


Module Structure

Every GE extension follows the same pattern. Use it consistently.

local M = {}
M.dependencies = {'career_career', 'career_saveSystem'}

local logTag = 'myModule'

-- ================================
-- STATE
-- ================================
local state = {}

-- ================================
-- PRIVATE FUNCTIONS
-- ================================
local function resetState()
  state = {}
end

local function doInternalWork(data)
  -- no nil checks needed — callers validate before calling
  return data.value * 2
end

-- ================================
-- PUBLIC API
-- ================================
local function getData()
  return deepcopy(state)
end

-- ================================
-- LIFECYCLE HOOKS
-- ================================
local function onExtensionLoaded()
  resetState()
end

local function onCareerActive(active)
  if not active then resetState() end
end

-- ================================
-- EXPORTS
-- ================================
M.getData = getData
M.onExtensionLoaded = onExtensionLoaded
M.onCareerActive = onCareerActive

return M

Rules:

  • Never use the deprecated module() function or package.seeall
  • Return a table of explicitly defined exports
  • No side effects on require — only load dependencies and return the table
  • Name your require variable after the module: local bar = require("foo.bar")

Defensive Coding Done Right

Key principles:

  1. Validate at boundaries, trust internals — Public APIs validate inputs; private helpers trust their callers already validated.
  2. Only nil-check things that can actually be nil — be:getPlayerVehicle(0) can be nil (no vehicle), but core_gamestate is always loaded. Ask: "Can I name a scenario where this is nil?"
  3. Don't wrap internal code in pcall — Reserve pcall for trust boundaries (file I/O, user input, deserialization).

For the full treatment with examples, see Anti-Patterns (sections 1 and 3) and Error Handling.


Lua Idioms

Defaults with or

-- ✅ Idiomatic Lua
local name = config.name or "Unknown"
local count = options.count or 10
local enabled = options.enabled ~= false  -- default true (nil → true)

-- ❌ Verbose alternative
local name
if config.name then
  name = config.name
else
  name = "Unknown"
end

or default gotcha

x or default treats false the same as nil. If false is a valid value, use an explicit check:

local show_ui = (config.show_ui ~= nil) and config.show_ui or true  -- wrong if false is valid
local show_ui = config.show_ui == nil and true or config.show_ui     -- handles false correctly

The and/or ternary

-- ✅ Clean conditional assignment
local label = is_active and "Running" or "Stopped"
local color = fuel > 0.2 and green or red

Local is faster

In hot paths, cache global lookups in locals:

-- ✅ ~10x faster in tight loops
local sin = math.sin
local insert = table.insert
local format = string.format

for i = 1, 100000 do
  insert(results, sin(i))
end

For normal code, don't bother — readability first.

The # operator

-- ✅ Safe: contiguous arrays
local items = {"a", "b", "c"}
print(#items)  -- 3

-- ❌ Dangerous: sparse tables
local t = {1, 2, nil, 4, nil}
print(#t)  -- undefined! Could be 2 or 4 or 5

Rule: Never rely on # for tables that may have nil holes. Use explicit counting or table.maxn for sparse tables.


Table Best Practices

Use constructor syntax

-- ✅ Clean, readable, diff-friendly (trailing comma!)
local config = {
  enabled = true,
  name = "delivery",
  reward = 500,
}

-- ❌ Verbose
local config = {}
config.enabled = true
config.name = "delivery"
config.reward = 500

Return copies, not references

-- ✅ Caller can't corrupt your state
function M.getData()
  return deepcopy(state)
end

-- ❌ Caller can modify your internal state
function M.getData()
  return state  -- dangerous!
end

Check empty with next()

-- ✅ Correct
if not next(myTable) then
  log('I', logTag, 'Table is empty')
end

-- ❌ Wrong — always creates a new table, never equal
if myTable == {} then end

Collect then modify

-- ✅ Safe: collect keys first
local to_remove = {}
for k, v in pairs(vehicles) do
  if v.destroyed then
    table.insert(to_remove, k)
  end
end
for _, k in ipairs(to_remove) do
  vehicles[k] = nil
end

-- ❌ Dangerous: modifying during iteration
for k, v in pairs(vehicles) do
  if v.destroyed then
    vehicles[k] = nil  -- undefined behavior
  end
end

Error Handling

See Error Handling for pcall patterns and defensive error strategies.


See Also

  • Common Patterns — Battle-tested patterns for BeamNG mods
  • Anti-Patterns — Mistakes to avoid
  • Cleaning Up AI-Generated Code — Fix common AI coding slop
  • Error Handling — Defensive coding patterns
  • Performance — Keeping your mod fast

Lua Classes & Metatables

When and how to use Lua classes in BeamNG modding — metatables, constructors, inheritance, with practical examples like parcels, checkpoints, and NPC fleets.

Proven GE Patterns

12 battle-tested patterns for BeamNG GE mods — save/load, UI data flow, screen transitions, vehicle callbacks, settings, and more.

On this page

Naming ConventionsFunction DesignName functions after what they doUse early returns instead of deep nestingExtract when there's a gap between intention and implementationComment QualityComments that add valueComments that are noiseModule StructureDefensive Coding Done RightLua IdiomsDefaults with orThe and/or ternaryLocal is fasterThe # operatorTable Best PracticesUse constructor syntaxReturn copies, not referencesCheck empty with next()Collect then modifyError HandlingSee Also