Your First BeamNG Mod
Step-by-step guide to creating your first BeamNG.drive Lua mod — file structure, extensions, hooks, and UI communication.
Every BeamNG mod starts with the same building blocks: an extension file, a mod script, and hooks into the game's event system. This guide walks you through each piece so you can go from an empty folder to a working mod in minutes.
Mod File Structure
Your mod is a ZIP file (or folder during development) that overlays the game's virtual filesystem:
mods/
mymod.zip (or mods/unpacked/mymod/)
lua/
ge/
extensions/
mymod/
myFeature.lua -- GE extension
ui/
modules/
apps/
myApp/
app.js -- UI app
app.json -- UI app configFiles inside your mod shadow (override) base game files at the same path. Your extensions in lua/ge/extensions/ are discovered automatically.
Creating a GE Extension
Extensions are the building blocks of GE Lua modding. Each file in lua/ge/extensions/ becomes a loadable module.
File Path = Extension Name
lua/ge/extensions/mymod/myFeature.lua
↓
Extension name: mymod_myFeaturePath separators (/) become underscores (_).
Basic Extension Template
local M = {}
local function onExtensionLoaded()
log('I', 'mymod', 'My extension loaded!')
end
local function onExtensionUnloaded()
log('I', 'mymod', 'My extension unloaded!')
end
M.onExtensionLoaded = onExtensionLoaded
M.onExtensionUnloaded = onExtensionUnloaded
return MEvery extension must return a table (conventionally M). Functions on that table become the extension's public API and hook handlers.
Loading Extensions Correctly
This is the most important thing to get right. There are two load modes:
Auto-loaded Extensions
By default, extensions are auto-loaded when the game discovers them and unloaded on level changes. This is fine for simple mods.
Manual Load Mode (Persistent Extensions)
If you want your extension to stay loaded across level changes (map switches, career transitions), you register it in your mod script - not inside the extension itself.
Create scripts/mymod/modScript.lua:
-- scripts/mymod/modScript.lua
setExtensionUnloadMode("mymod_myFeature", "manual")
loadManualUnloadExtensions()This tells the engine to load your extension early and keep it loaded when switching levels. See the Mod Scripts guide for full details.
Your extension stays clean - just the M table and functions:
local M = {}
local function onExtensionLoaded()
log('I', 'mymod', 'Loaded and will persist across levels!')
end
M.onExtensionLoaded = onExtensionLoaded
return MLoading Your Extension
To load an extension from Lua code:
-- Load by extension name
extensions.load('mymod_myFeature')
-- Load multiple at once
extensions.load('mymod_myFeature', 'mymod_otherFeature')The extensions.load() function:
- Finds the file at the corresponding path
- Executes it and stores the returned table
- Calls
onExtensionLoaded()if defined - Resolves any dependencies
Loading from UI/JavaScript
To load a GE extension from UI code:
bngApi.engineLua("extensions.load('mymod_myFeature')")Hooks - Responding to Game Events
Extensions receive game events through hooks. Define a function on your M table matching the hook name:
local M = {}
local function onExtensionLoaded()
log('I', 'mymod', 'Extension loaded!')
end
local function onUpdate(dtReal, dtSim, dtRaw)
-- dtReal: real-world delta time
-- dtSim: simulation delta time (affected by slow-mo)
end
local function onVehicleSpawned(vehicleId)
local veh = be:getObjectByID(vehicleId)
log('I', 'mymod', 'Vehicle spawned: ' .. tostring(veh:getName()))
end
local function onVehicleSwitched(oldId, newId, player)
log('I', 'mymod', 'Switched from ' .. tostring(oldId) .. ' to ' .. tostring(newId))
end
M.onExtensionLoaded = onExtensionLoaded
M.onUpdate = onUpdate
M.onVehicleSpawned = onVehicleSpawned
M.onVehicleSwitched = onVehicleSwitched
return MCommon hooks:
onExtensionLoaded- Extension just loadedonUpdate(dtReal, dtSim, dtRaw)- Every frameonVehicleSpawned(vehicleId)- Vehicle createdonVehicleSwitched(oldId, newId, player)- Player switched vehicleonClientStartMission(levelPath)- Level loadingonClientEndMission(levelPath)- Level unloading
See the Hook Catalog for the full list.
Communicating with UI
GE Lua to UI (JavaScript)
-- Send data to UI
guihooks.trigger('MyModEvent', {score = 100, name = "test"})UI (JavaScript) to GE Lua
// Call a function on your extension
bngApi.engineLua("extensions.mymod_myFeature.doSomething()")
// With arguments
bngApi.engineLua(`extensions.mymod_myFeature.setScore(${score})`)UI Listening for Events
// In your app.js
angular.module('beamng.apps')
.directive('myModApp', function() {
return {
template: '<div>{{ data.score }}</div>',
link: function(scope) {
scope.data = { score: 0 }
scope.$on('MyModEvent', function(event, data) {
scope.$apply(function() {
scope.data = data
})
})
}
}
})Key APIs
| API | What It Does | Example |
|---|---|---|
extensions.load() | Load an extension | extensions.load('mymod_feature') |
extensions.hook() | Call a hook on all extensions | extensions.hook('onMyEvent', data) |
guihooks.trigger() | Send event to UI | guihooks.trigger('Event', data) |
be:getObjectByID() | Get vehicle by ID | be:getObjectByID(vehicleId) |
be:getPlayerVehicle(0) | Get player's vehicle | local veh = be:getPlayerVehicle(0) |
log(level, tag, msg) | Log a message | log('I', 'mymod', 'hello') |
setExtensionUnloadMode() | Set persistence (use in modScript.lua) | setExtensionUnloadMode("mymod_feature", "manual") |
scenetree.findObject() | Find scene object | scenetree.findObject("thePlayer") |
Common Mistakes
These will waste hours of debugging
Every mistake below has tripped up experienced modders. Read them before you start.
-
Not using a mod script - Your extension disappears on level change. Register it in
scripts/mymod/modScript.luawithsetExtensionUnloadMode. See Mod Scripts. -
Forgetting to return M - Your extension won't load. Always end with
return M. -
Using
registerCoreModule- This is deprecated. UsesetExtensionUnloadModein your modScript.lua instead. -
Calling extension functions before they're loaded - Use
extensions.load()first, or check withextensions.isExtensionLoaded('name'). -
Not handling nil vehicles - Always check
be:getPlayerVehicle(0)for nil before using it.
See Also
- Mod Scripts - How to set up your mod's entry point (read this next)
- Creating Extensions - Detailed extension reference
- Module Loading - Loading entire folders of extensions
- Vehicle Control - Spawning, getting, and controlling vehicles
- UI Apps - Creating HUD widgets
- UI Bridge - Lua to UI communication
- GUI Hooks - Sending events to the UI
- Saving Data - Saving and loading mod data
- Debugging - Logging, inspecting data, common errors
- Hook Catalog - All available hooks
- Common Patterns - Patterns you'll use frequently
- Anti-Patterns - Mistakes to avoid
- Performance - Keeping your mod fast
- Architecture - How everything fits together
- FAQ - Frequently asked questions