RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

GE Extension SystemGE Architecture Quick ReferenceGE Boot SequenceGE Cross-VM CommunicationHow Mods Are LoadedInter-VM Communication (GE ↔ VE ↔ UI)Vehicle Boot Sequence & Lifecycle

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

GuidesArchitecture

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.lua

The 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:

StepActionPurpose
1require('luaCore'), require('common/cdefs')Core Lua infrastructure
2Load mathlib, utils, devUtils, ge_utils, luaProfilerMath, utilities, profiling
3Load json, guihooks, screenshot, simTimeAuthorityCore modules
4extensions = require("extensions")Extension system
5extensions.addModulePath("lua/ge/extensions/")Register extension paths
6Load map, spawn, serverConnection, server, commandsGame systems
7Set globals: settings = extensions.core_settings_settingsAliases
8Define 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 onClientPostStartMission

Extension 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 mode

Loading Priority

Extensions load in this priority order:

  1. Startup Extensions (defined in main.lua's startupExtensions table)

    • ~60 extensions loaded on boot, set to manual unload mode
    • Includes core_audio, core_camera, core_gamestate, core_modmanager, core_settings_settings, career_career, freeroam_freeroam, etc.
  2. 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.
  3. On-Demand Extensions

    • Loaded via extensions.load('name') at runtime
    • Trigger onExtensionLoaded on self, then on all other loaded extensions

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 extensions

Lifecycle Hooks

Initialization Hooks

HookTriggerCalled On
onExtensionLoadedExtension loadedSingle extension (self), receives deserialized data
onInitAfter all batch extensions call onExtensionLoaded (GE only)Single extension (self)
onFirstUpdateFirst GFX frame after bootAll extensions
onExtensionUnloadedExtension being unloadedSingle extension (self, no broadcast)

Level/Mission Hooks

HookTriggerUse Case
onClientPreStartMission(levelPath)Before mission resources loadEarly level setup
onClientPostStartMission(levelPath)Level Lua loaded, objects availableMission 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 resetClean state, reset timers

Runtime Hooks

HookFrequencyUse 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 spawnTrack vehicles
onVehicleDestroyed(vid)Vehicle removalCleanup 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 = onExtensionLoaded

If 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 M

Conditional 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 = onExtensionLoaded

Deferred 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 = onInit

Extension Unload Modes

Extensions have different unload behaviors based on their role:

ModeBehaviorUse For
manualSurvives level changes, must be explicitly unloadedCore systems, settings, mod infrastructure
autoUnloaded 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:

  • auto extensions: Unloaded when level changes or unloadAutoExtensions() is called
  • manual extensions: Only unloaded by explicit extensions.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 = someFunction

Hard 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 = onExtensionLoaded

Best Practices

Initialization Do's and Don'ts

DoDon't
Keep onExtensionLoaded lightweightDon't do heavy file I/O during init
Use onExtensionLoaded for cross-mod setupDon't assume load order - use hooks
Check extensions.isExtensionLoaded() before callsDon't call other extensions during init
Defer level-dependent setup to onClientPostStartMissionDon't query map or scenetree at boot
Set state flags to track readinessDon'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
end

Pitfall: 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
end

Pitfall: 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
end

Debugging 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 extensions

Trace Hook Execution

-- Add to your extension for debugging
local function onExtensionLoaded()
    log('I', 'myMod', 'onExtensionLoaded called')
    debug.traceback()  -- Print stack trace
end
M.onExtensionLoaded = onExtensionLoaded

VM Reload Behavior

When Lua VM reloads (F11):

  1. Current VM destroyed
  2. New VM created
  3. Full boot sequence executes
  4. All extensions reload
  5. Level state preserved (if possible)
  6. onClientPostStartMission may 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 = onExtensionLoaded

Reference: 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 names

See Also

  • Architecture - Full extension system reference
  • Quick Reference - Fast rules refresher
  • Mod Scripts - Extension persistence
  • Hook Catalog - All available hooks

GE Architecture Quick Reference

10-second refresher on Game Engine extension lifecycle, hooks, and core rules. Pin this page while coding.

GE Cross-VM Communication

How GE communicates with Vehicle Engines and the UI — bridge methods, hooks, and patterns for cross-context data flow.

On this page

OverviewBoot SequencePhase 1: Lua VM InitializationPhase 2: Core System Bootstrap (main.lua)Phase 3: Level Loading (Deferred)Extension Loading OrderDirectory StructureLoading PriorityLoading MechanicsLifecycle HooksInitialization HooksLevel/Mission HooksRuntime HooksHook Execution OrderUnderstanding onExtensionLoadedHook Registration PatternsStandard PatternConditional InitializationDeferred Setup (After All Extensions Load)Extension Unload ModesExtension DependenciesSoft Dependencies (Recommended)Hard DependenciesBest PracticesInitialization Do's and Don'tsCommon PitfallsDebugging Boot IssuesEnable Verbose LoggingCheck Extension Load StatusTrace Hook ExecutionVM Reload BehaviorHandling ReloadsReference: Extension APISee Also