Creating a GE Extension
Step-by-step guide to creating a GE extension — file naming, lifecycle hooks, dependencies, and the full loading sequence.
Extensions are the fundamental building blocks of BeamNG modding. Every gameplay system — career, missions, traffic, UI — is an extension. Understanding how to create one correctly is the single most important skill for any BeamNG modder.
Step 1: Create the File
Extensions live in lua/ge/extensions/. The file path determines the extension name:
lua/ge/extensions/mymod/myfeature.lua
↓
Extension name: mymod_myfeatureNaming rule: Path separators (/) become underscores (_). Literal underscores in filenames become double underscores (__).
-- From extensions.lua:52-59
local function luaPathToExtName(filepath)
return (filepath:gsub('_', '__'):gsub('/', '_'))
end
local function extNameToLuaPath(extName)
return extName:gsub('__', '#'):gsub('_', '/'):gsub('#', '_')
endExamples:
| File Path | Extension Name | Access Via |
|---|---|---|
career/career.lua | career_career | career_career (global) |
core/settings/settings.lua | core_settings_settings | core_settings_settings |
ui/fadeScreen.lua | ui_fadeScreen | ui_fadeScreen |
Step 2: Write the Extension
Every extension must return a module table M:
-- lua/ge/extensions/mymod/myfeature.lua
local M = {}
-- OPTIONAL: Declare dependencies (loaded before your extension)
M.dependencies = {'core_settings_settings', 'career_career'}
-- OPTIONAL: Called when YOUR extension loads (or VM reloads)
-- Receives deserialized state from previous VM session (Ctrl+L reload)
local function onExtensionLoaded(deserializedData)
-- Initialize state, register callbacks
-- Return false to abort loading (extension will be unloaded)
end
-- OPTIONAL: Called AFTER onExtensionLoaded, GE only
-- Good for second-phase init when all batch-loaded extensions are ready
local function onInit(deserializedData)
end
-- OPTIONAL: Per-frame update (called every frame, before physics)
local function onUpdate(dtReal, dtSim, dtRaw)
-- dtReal = wall-clock delta, dtSim = simulation delta (affected by slow-mo), dtRaw = unscaled
end
-- OPTIONAL: Called when level finishes loading and objects are available
local function onClientPostStartMission(levelPath)
end
-- OPTIONAL: Called when world is fully ready (state=2 means all done)
local function onWorldReadyState(state)
if state == 2 then
-- Safe to query entities, spawn things, etc.
end
end
-- OPTIONAL: Called when your extension is unloaded
local function onExtensionUnloaded()
-- Cleanup
end
-- Export hooks by assigning to M
M.onExtensionLoaded = onExtensionLoaded
M.onInit = onInit
M.onUpdate = onUpdate
M.onClientPostStartMission = onClientPostStartMission
M.onWorldReadyState = onWorldReadyState
M.onExtensionUnloaded = onExtensionUnloaded
-- Export public API functions
-- M.myPublicFunction = myPublicFunction
return MStep 3: Loading Your Extension
Auto-load (on level start)
Add your extension path to the level's preset extension list, or load it from another extension:
extensions.load('mymod_myfeature')Manual unload mode (persist across levels)
By default, extensions are unloaded when the level changes. To keep yours loaded, register it in your mod script:
-- scripts/mymod/modScript.lua
setExtensionUnloadMode("mymod_myfeature", "manual")
loadManualUnloadExtensions()Do NOT use setExtensionUnloadMode inside the extension itself. It belongs in modScript.lua.
Unload when done
local function cleanup()
extensions.unload('mymod_myfeature')
endStep 4: Dependencies
Declare dependencies to ensure other extensions load first:
M.dependencies = {'core_settings_settings', 'career_career'}From extensions.lua:411-420:
if m.dependencies then
for _, depName in ipairs(m.dependencies) do
if not extRequested[depName] then
extRequested[depName] = true
local dependencyLoaded = extensionLoadInternal(depName, nil, extRequested)
if not dependencyLoaded and not luaMods[depName] then
log('W', logTag, 'Failed to load dependency: '..depName)
end
end
end
endNote: Failed dependencies log a warning but don't prevent your extension from loading. Check availability at runtime if needed.
Step 5: Extension Load Order
The actual initialization sequence (from extensions.lua:587-632):
- File loaded -
require()executes your file,Mtable is created onPreLoad()- Called during refresh (rarely used)- Dependencies loaded - All
M.dependenciesare loaded first - Registered - Extension added to global scope (
_G[extName] = M) onExtensionLoaded(deserializedData)- Your init hook. Returnfalseto abort.onInit(deserializedData)- Second-phase init (GE only, after all batch-loaded extensions finish step 5)
-- From extensions.lua:601-620 (processLoadedFreshList)
if m.onExtensionLoaded then
res = m.onExtensionLoaded(deserializedData[m.__extensionName__])
if res == false then
-- Extension will be unloaded
end
end
-- ... later, for GE only:
if m and type(m.onInit) == 'function' then
m.onInit(deserializedData[m.__extensionName__])
endStep 6: Communicating with Other Extensions
Call another extension directly
if career_career then -- Check it's loaded
career_career.isActive()
endBroadcast a hook to all extensions
extensions.hook('myCustomHookName', arg1, arg2)
-- Every loaded extension with M.myCustomHookName will be calledCheck if another extension is available
-- In onInit or later, check if an extension is loaded
local function onInit()
if career_career then
-- Career extension is available
end
endSend data to UI
guihooks.trigger('MyCustomEvent', {data = "value"})Deprecated Patterns
The extension system auto-patches old function names (from extensions.lua:20-27):
| Old Name | New Name |
|---|---|
onLoad | onExtensionLoaded |
init | onExtensionLoaded |
onRaceWaypoint | onRaceWaypointReached |
Always use the new names. The old ones trigger deprecation warnings.
Complete Minimal Example
-- lua/ge/extensions/mymod/helloworld.lua
local M = {}
local initialized = false
local function onExtensionLoaded()
log('I', 'mymod', 'Hello World extension loaded!')
initialized = true
end
local function onWorldReadyState(state)
if state == 2 and initialized then
log('I', 'mymod', 'World is ready!')
guihooks.trigger('MyModReady', {message = 'Hello from mymod!'})
end
end
local function onExtensionUnloaded()
log('I', 'mymod', 'Goodbye!')
initialized = false
end
M.onExtensionLoaded = onExtensionLoaded
M.onWorldReadyState = onWorldReadyState
M.onExtensionUnloaded = onExtensionUnloaded
return MLoad it with: extensions.load('mymod_helloworld')
Checklist
- File is in
lua/ge/extensions/with correct path - Returns
Mtable - Uses
onExtensionLoaded(not deprecatedonLoad/init) - Dependencies declared if needed
-
setExtensionUnloadModein modScript.lua if it should persist across levels - Hooks exported on
M(e.g.,M.onUpdate = onUpdate) - Cleanup in
onExtensionUnloadedif needed
See Also
- Getting Started - First mod walkthrough
- Mod Scripts - Registering extensions for persistence
- Module Loading - Loading folders of extensions
- Hook Catalog - All available hooks
- Common Patterns - Proven extension patterns