Overriding Lua Modules
How to safely override and extend existing Lua module functions without breaking other mods.
When modding BeamNG, you often need to change or extend the behavior of an existing module. The golden rule: never replace the original file. Instead, override individual functions at runtime so your changes stack with other mods.
This guide covers the function wrapping technique — the standard way to extend existing modules while keeping compatibility with the base game and other mods.
Why Not Just Replace the File?
If your mod includes a file at the same path as a vanilla file (e.g. lua/ge/extensions/core/something.lua), your version completely replaces the original. This causes problems:
- Other mods break — if two mods both replace the same file, only one wins
- Game updates break your mod — when BeamNG updates that file, your old copy is missing the changes
- You lose all original functionality unless you manually copy it into your version
The repository will also reject mods that overwrite default game files.
The Function Wrapping Pattern
Instead of replacing a file, create your own extension that wraps the target function:
-- lua/ge/extensions/auto/myMod_overrides.lua
local M = {}
local originalFunc = nil
local function onExtensionLoaded()
-- Get a reference to the module you want to modify
local targetModule = extensions.gameplay_traffic
-- Store the original function
originalFunc = targetModule.setupTraffic
-- Replace with your wrapper
targetModule.setupTraffic = function(...)
-- Your logic BEFORE the original runs
log('I', 'myMod', 'setupTraffic called with custom wrapper')
-- Call the original function with all its arguments
local result = originalFunc(...)
-- Your logic AFTER the original runs
-- e.g. modify the result, add extra behavior, etc.
return result
end
end
M.onExtensionLoaded = onExtensionLoaded
return MWhy This Works
- You store a reference to the original function before overwriting it
- Your wrapper calls the original, so all vanilla behavior is preserved
- If another mod wraps the same function, it wraps your wrapper — the calls chain together
- The original file is never modified
Wrapping Multiple Functions
For modules where you need to override several functions:
local M = {}
local origSetup = nil
local origCleanup = nil
local function onExtensionLoaded()
local target = extensions.gameplay_traffic
origSetup = target.setupTraffic
target.setupTraffic = function(...)
log('I', 'myMod', 'Before setup')
local result = origSetup(...)
log('I', 'myMod', 'After setup')
return result
end
origCleanup = target.cleanup
target.cleanup = function(...)
log('I', 'myMod', 'Before cleanup')
local result = origCleanup(...)
-- Add your cleanup here
return result
end
end
M.onExtensionLoaded = onExtensionLoaded
return MChoosing When to Call the Original
Where you place the call to the original function matters — it depends on what you're trying to achieve:
Run your logic after the original
Call the original first, then do your thing. Useful when you need the original to set things up before you modify the result or add behavior on top:
target.setupTraffic = function(...)
local result = origFunc(...) -- Original runs first
-- Now modify result or do additional work
return result
endRun your logic before the original
Do your thing first, then let the original run. Useful for validation, logging, or modifying arguments before they reach the original:
target.setupTraffic = function(arg1, ...)
-- Modify arguments or set up state
arg1 = sanitize(arg1)
return origFunc(arg1, ...) -- Original runs after
endRun your logic around the original
Wrap both sides — set something up before, react to the result after:
target.setupTraffic = function(...)
prepareMyStuff() -- Before
local result = origFunc(...) -- Original
adjustBasedOn(result) -- After
return result
endWhich approach you choose depends entirely on why you need the override. There's no single right answer.
Limitations
Function wrapping gives you access to a module's public interface — the functions and values it exposes on its module table (M). However, you cannot access local variables or local functions inside the original module. Anything declared with local in the original file is invisible to your wrapper.
If the behavior you need to change is driven by a local variable or a local helper function, wrapping won't help. In those cases, your options are limited — you may need to request an API change from the module author, or find a different approach entirely.
Conditionally Overriding
Sometimes you only want to modify behavior under certain conditions. The wrapper pattern handles this naturally:
local origFunc = nil
local function onExtensionLoaded()
local target = extensions.some_module
origFunc = target.doSomething
target.doSomething = function(arg1, arg2, ...)
-- Only modify behavior when your condition is met
if someCondition then
-- Your custom behavior
return myCustomResult
end
-- Otherwise, fall through to the original
return origFunc(arg1, arg2, ...)
end
endTiming: When to Apply Overrides
Use onExtensionLoaded to apply your wraps. This hook fires after your extension is loaded, and the target modules will already be available through the extensions table.
If you need to target a module that loads later, you can wrap in a different hook:
local applied = false
local function onUpdate(dtReal, dtSim, dtRaw)
if not applied and extensions.some_late_module then
-- Apply wraps here
applied = true
end
endMultiple Mods Stacking
The beauty of this pattern is that it chains. If three mods all wrap gameplay_traffic.setupTraffic:
- Mod A stores the original, replaces with Wrapper A
- Mod B stores Wrapper A (thinks it's the original), replaces with Wrapper B
- Mod C stores Wrapper B, replaces with Wrapper C
When setupTraffic is called: Wrapper C → Wrapper B → Wrapper A → Original
Each mod's additions run without knowing about the others. No conflicts, no overwrites.
Common Mistakes
Forgetting to call the original
-- BAD: Original behavior is completely lost
target.doSomething = function(...)
return myNewBehavior()
end
-- GOOD: Original behavior preserved
target.doSomething = function(...)
local result = origFunc(...)
-- Add your stuff
return result
endWrapping too early
If you wrap in your module's top-level scope (outside a function), the target module might not be loaded yet. Always wrap inside onExtensionLoaded or later.
Not passing arguments through
-- BAD: Drops any arguments the original expects
target.doSomething = function()
return origFunc()
end
-- GOOD: Passes all arguments through
target.doSomething = function(...)
return origFunc(...)
endSee Also
- Extension Patterns — Common Lua patterns in BeamNG
- Creating a GE Extension — How to build your own extension
- Publishing Your Mod — Repository rules including the no-overwrite policy
- Module System — How BeamNG's module system works
Anti-Patterns to Avoid
Common BeamNG modding mistakes that cause crashes, data loss, and silent failures — with correct alternatives for each.
Cleaning Up AI-Generated Code
How to identify and fix common AI coding patterns — excessive guards, restating comments, unnecessary boilerplate, and other slop that hurts readability.