GE Boot Sequence
Complete documentation of the GE boot sequence — when extensions load, hook execution order, and how to initialize correctly.
Timing bugs are the #1 source of "it works sometimes" problems in BeamNG mods. If your extension tries to access the map before it's loaded, or calls another extension before it exists, you'll get silent failures. This guide shows exactly when each hook fires so you can initialize at the right moment.
Overview
The GE Lua VM follows a strict initialization sequence that ensures core systems are ready before extensions load. Understanding this sequence is critical for writing extensions that initialize correctly and interoperate safely.
Boot Sequence
Phase 1: Lua VM Initialization
[Engine Core] → Create Lua VM → Load lua/ge/main.luaThe engine creates a fresh Lua VM and executes lua/ge/main.lua as the entry point. This happens:
- On game startup
- On Lua VM reload (F11 in dev mode)
- When switching levels (full reload)
Phase 2: Core System Bootstrap (main.lua)
main.lua executes in strict order:
| Step | Action | Purpose |
|---|---|---|
| 1 | require('luaCore'), require('common/cdefs') | Core Lua infrastructure |
| 2 | Load mathlib, utils, devUtils, ge_utils, luaProfiler | Math, utilities, profiling |
| 3 | Load json, guihooks, screenshot, simTimeAuthority | Core modules |
| 4 | extensions = require("extensions") | Extension system |
| 5 | extensions.addModulePath("lua/ge/extensions/") | Register extension paths |
| 6 | Load map, spawn, serverConnection, server, commands | Game systems |
| 7 | Set globals: settings = extensions.core_settings_settings | Aliases |
| 8 | Define C++ callbacks: vehicleSpawned, update, luaPreRender, etc. | Engine hooks |
Phase 3: Level Loading (Deferred)
After boot completes, level loading triggers additional hooks:
Level Load Request → Unload Current Level → Load New Level → Trigger onClientPostStartMissionExtension Loading Order
Directory Structure
lua/ge/extensions/
├── core/ # Core extensions
│ ├── settings/settings.lua # Settings system (aliased as global `settings`)
│ ├── input/actionFilter.lua # Input filtering
│ ├── camera.lua # Camera system
│ ├── modmanager.lua # Mod manager
│ └── ...
├── gameplay/ # Gameplay extensions
│ ├── missions/ # Mission system
│ └── traffic.lua # Traffic system
├── career/ # Career extensions
│ ├── career.lua # Career main module
│ └── modules/ # Career sub-modules
└── freeroam/ # Freeroam extensions
└── freeroam.lua # Freeroam game modeLoading Priority
Extensions load in this priority order:
-
Startup Extensions (defined in
main.lua'sstartupExtensionstable)- ~60 extensions loaded on boot, set to
manualunload mode - Includes
core_audio,core_camera,core_gamestate,core_modmanager,core_settings_settings,career_career,freeroam_freeroam, etc.
- ~60 extensions loaded on boot, set to
-
Preset Extensions (loaded per game mode via
loadPresetExtensions())- Additional extensions loaded when entering freeroam, career, etc.
- Includes
gameplay_traffic,gameplay_missions_missionManager,freeroam_bigMapMode, etc.
-
On-Demand Extensions
- Loaded via
extensions.load('name')at runtime - Trigger
onExtensionLoadedon self, then on all other loaded extensions
- Loaded via
Loading Mechanics
-- When extensions.load('myMod') is called:
-- 1. Check if already loaded (return if true - idempotent)
-- 2. Convert name to path: extNameToLuaPath('myMod') → 'myMod'
-- 3. Resolve dependencies - load any M.dependencies first
-- 4. Execute lua/ge/extensions/myMod.lua (via require)
-- 5. Register module as global: _G['myMod'] = M
-- 6. Call M.onExtensionLoaded() if it exists (self-init, no args)
-- 7. Call M.onInit() if it exists (GE second-phase init)
-- 8. Trigger onExtensionLoaded('myMod') hook on ALL other loaded extensionsLifecycle Hooks
Initialization Hooks
| Hook | Trigger | Called On |
|---|---|---|
onExtensionLoaded | Extension loaded | Single extension (self), receives deserialized data |
onInit | After all batch extensions call onExtensionLoaded (GE only) | Single extension (self) |
onFirstUpdate | First GFX frame after boot | All extensions |
onExtensionUnloaded | Extension being unloaded | Single extension (self, no broadcast) |
Level/Mission Hooks
| Hook | Trigger | Use Case |
|---|---|---|
onClientPreStartMission(levelPath) | Before mission resources load | Early level setup |
onClientPostStartMission(levelPath) | Level Lua loaded, objects available | Mission logic initialization |
onClientStartMission(levelPath) | Level items loaded (hookNotify) | Level setup |
onWorldReadyState(state) | World ready (state=2) | Post-spawn setup |
onClientEndMission(levelPath) | Leaving level (hookNotify) | Level cleanup |
onResetGameplay(playerID) | Gameplay reset | Clean state, reset timers |
Runtime Hooks
| Hook | Frequency | Use Case |
|---|---|---|
onUpdate(dtReal, dtSim, dtRaw) | Every frame (before physics) | Game logic, timers |
onPreRender(dtReal, dtSim, dtRaw) | Every frame (after physics) | Visual updates, debug draw |
onGuiUpdate(dtReal, dtSim, dtRaw) | Every frame (UI) | UI-specific per-frame |
onVehicleSpawned(vid, vehObj) | Vehicle spawn | Track vehicles |
onVehicleDestroyed(vid) | Vehicle removal | Cleanup vehicle data |
Hook Execution Order
C++ Engine → main.lua loaded
↓
main.lua: require core modules (mathlib, utils, json, guihooks, extensions, etc.)
↓
C++ calls onGameEngineStartup()
→ clientCore.initializeCore()
→ client_init.initClient()
↓
C++ calls init()
→ settings.initSettings()
→ detectGlobalWrites()
→ extensions.load(startupExtensions) [~60 extensions loaded]
→ Each ext: onExtensionLoaded(deserializedData) (self only), then onInit(deserializedData) (GE only)
→ importPersistentData()
→ core_modmanager.onUiReady()
↓
C++ calls updateFirstFrame()
→ extensions.hook('onFirstUpdate')
→ settings.finalizeInit()
↓
C++ calls uiReady()
→ extensions.hook('onUiReady')
↓
Level Load Request
→ clientPreStartMission(levelPath) → hook('onClientPreStartMission')
→ clientPostStartMission(levelPath) → hook('onClientPostStartMission')
→ clientStartMission(levelPath) → hookNotify('onClientStartMission')
↓
luaPreRender detects worldReadyState == 1 → materials done → worldReadyState = 2
→ hook('onWorldReadyState', 2)
↓
Per-frame: update() → hook('onUpdate'), hook('onGuiUpdate')
Per-frame: luaPreRender() → hook('onPreRender'), hook('onDrawDebug')Understanding onExtensionLoaded
onExtensionLoaded is called only on the extension itself when it loads. It receives deserialized state data from a previous VM session (e.g., after Ctrl+L reload). It does NOT broadcast to other extensions.
local state = {}
local function onExtensionLoaded(deserializedData)
state = deserializedData or {}
log('I', 'myMod', 'I have loaded!')
end
M.onExtensionLoaded = onExtensionLoadedIf your onExtensionLoaded returns false, the extension will be unloaded immediately.
After all batch-loaded extensions call onExtensionLoaded, the system calls onInit(deserializedData) on each (GE only). Use onInit for second-phase initialization that depends on other extensions being ready.
Hook Registration Patterns
Standard Pattern
local M = {}
M.dependencies = {'core_settings', 'gameplay_sites_sitesManager'}
local hasCareer = false
local function onExtensionLoaded(deserializedData)
log('I', 'myExtension', 'Extension initialized')
end
local function onInit(deserializedData)
if career_career then
hasCareer = true
end
end
local function onExtensionUnloaded()
-- Cleanup resources here
end
M.onExtensionLoaded = onExtensionLoaded
M.onInit = onInit
M.onExtensionUnloaded = onExtensionUnloaded
return MConditional Initialization
local ready = false
local function onExtensionLoaded()
if not extensions.isExtensionLoaded('requiredMod') then
log('W', 'myExtension', 'Required mod not found, disabling')
return false -- abort loading
end
ready = true
end
M.onExtensionLoaded = onExtensionLoadedDeferred Setup (After All Extensions Load)
local pendingSetup = false
local function onExtensionLoaded()
pendingSetup = true
end
local function onInit()
if pendingSetup and freeroam_freeroam then
doFullSetup()
pendingSetup = false
end
end
M.onExtensionLoaded = onExtensionLoaded
M.onInit = onInitExtension Unload Modes
Extensions have different unload behaviors based on their role:
| Mode | Behavior | Use For |
|---|---|---|
manual | Survives level changes, must be explicitly unloaded | Core systems, settings, mod infrastructure |
auto | Unloaded on level change (default) | Level-specific gameplay, missions, temporary systems |
-- Set unload mode in your modScript.lua (NOT inside the extension)
-- scripts/mymod/modScript.lua
setExtensionUnloadMode("myExtension", "manual")
loadManualUnloadExtensions()See Mod Scripts for full details.
When extensions unload:
autoextensions: Unloaded when level changes orunloadAutoExtensions()is calledmanualextensions: Only unloaded by explicitextensions.unload()call
onExtensionUnloaded() is called on the extension being unloaded (no broadcast to others).
Extension Dependencies
Soft Dependencies (Recommended)
-- Check at runtime if optional extension is available
local function someFunction()
if optionalMod and optionalMod.doSomething then
optionalMod.doSomething()
else
-- Fallback behavior
end
end
M.someFunction = someFunctionHard Dependencies
local function onExtensionLoaded()
if not extensions.isExtensionLoaded('requiredMod') then
log('E', 'myExtension', 'Missing required extension: requiredMod')
return false -- abort loading
end
end
M.onExtensionLoaded = onExtensionLoadedBest Practices
Initialization Do's and Don'ts
| Do | Don't |
|---|---|
Keep onExtensionLoaded lightweight | Don't do heavy file I/O during init |
Use onExtensionLoaded for cross-mod setup | Don't assume load order - use hooks |
Check extensions.isExtensionLoaded() before calls | Don't call other extensions during init |
Defer level-dependent setup to onClientPostStartMission | Don't query map or scenetree at boot |
| Set state flags to track readiness | Don't leave half-initialized state |
Common Pitfalls
Pitfall: Assuming Load Order
-- BAD: May fail if career loads after myMod
local function onExtensionLoaded()
career_career.setSomething() -- May not exist yet!
end
-- GOOD: Use onInit for second-phase init (other extensions are ready)
local function onInit()
if career_career then
career_career.setSomething()
end
endPitfall: Heavy Initialization
-- BAD: Blocks boot
local function onExtensionLoaded()
for i = 1, 10000 do
heavyComputation()
end
end
-- GOOD: Defer to first update
local function onFirstUpdate()
-- Heavy work here
endPitfall: Level-Dependent Setup at Boot
-- BAD: map not ready during boot
local function onExtensionLoaded()
local nodes = map.getMap().nodes -- Nil!
end
-- GOOD: Wait for level loaded
local function onClientPostStartMission()
local nodes = map.getMap().nodes
endDebugging Boot Issues
Enable Verbose Logging
-- In console or early in main.lua
log('I', 'Boot', 'Starting extension load')Check Extension Load Status
-- In console (F11)
extensions.isExtensionLoaded('myExtension') -- true/false
dump(extensions.getLoadedExtensionsNames()) -- List all loaded extensionsTrace Hook Execution
-- Add to your extension for debugging
local function onExtensionLoaded()
log('I', 'myMod', 'onExtensionLoaded called')
debug.traceback() -- Print stack trace
end
M.onExtensionLoaded = onExtensionLoadedVM Reload Behavior
When Lua VM reloads (F11):
- Current VM destroyed
- New VM created
- Full boot sequence executes
- All extensions reload
- Level state preserved (if possible)
onClientPostStartMissionmay not re-trigger if level already loaded
Handling Reloads
local state = nil
local function onExtensionLoaded(deserializedData)
if deserializedData then
log('I', 'myMod', 'Reload detected, restoring state')
state = deserializedData
else
state = {}
end
end
M.onExtensionLoaded = onExtensionLoadedReference: Extension API
-- Load/unload
extensions.load('name') -- Load extension
extensions.unload('name') -- Unload extension
extensions.reload('name') -- Reload extension
extensions.isExtensionLoaded('name') -- Check if loaded
-- Hooks
extensions.hook('eventName', arg1, arg2) -- Trigger hook
-- (no hookExists API available)
-- Registry
extensions.getLoadedExtensionsNames() -- List of loaded extension namesSee Also
- Architecture - Full extension system reference
- Quick Reference - Fast rules refresher
- Mod Scripts - Extension persistence
- Hook Catalog - All available hooks