RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Your First BeamNG ModMod Scripts (modScript.lua)Creating a GE ExtensionLoading Extension Folders

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

GuidesGetting Started

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_myfeature

Naming 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('#', '_')
end

Examples:

File PathExtension NameAccess Via
career/career.luacareer_careercareer_career (global)
core/settings/settings.luacore_settings_settingscore_settings_settings
ui/fadeScreen.luaui_fadeScreenui_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 M

Step 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')
end

Step 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
end

Note: 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):

  1. File loaded - require() executes your file, M table is created
  2. onPreLoad() - Called during refresh (rarely used)
  3. Dependencies loaded - All M.dependencies are loaded first
  4. Registered - Extension added to global scope (_G[extName] = M)
  5. onExtensionLoaded(deserializedData) - Your init hook. Return false to abort.
  6. 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__])
end

Step 6: Communicating with Other Extensions

Call another extension directly

if career_career then  -- Check it's loaded
  career_career.isActive()
end

Broadcast a hook to all extensions

extensions.hook('myCustomHookName', arg1, arg2)
-- Every loaded extension with M.myCustomHookName will be called

Check 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
end

Send data to UI

guihooks.trigger('MyCustomEvent', {data = "value"})

Deprecated Patterns

The extension system auto-patches old function names (from extensions.lua:20-27):

Old NameNew Name
onLoadonExtensionLoaded
initonExtensionLoaded
onRaceWaypointonRaceWaypointReached

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 M

Load it with: extensions.load('mymod_helloworld')


Checklist

  • File is in lua/ge/extensions/ with correct path
  • Returns M table
  • Uses onExtensionLoaded (not deprecated onLoad/init)
  • Dependencies declared if needed
  • setExtensionUnloadMode in modScript.lua if it should persist across levels
  • Hooks exported on M (e.g., M.onUpdate = onUpdate)
  • Cleanup in onExtensionUnloaded if 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

Mod Scripts (modScript.lua)

How modScript.lua works — the entry point for your mod that controls which extensions persist across level changes.

Loading Extension Folders

How to automatically load all extensions from a folder with proper namespacing — essential for mods with multiple features.

On this page

Step 1: Create the FileStep 2: Write the ExtensionStep 3: Loading Your ExtensionAuto-load (on level start)Manual unload mode (persist across levels)Unload when doneStep 4: DependenciesStep 5: Extension Load OrderStep 6: Communicating with Other ExtensionsCall another extension directlyBroadcast a hook to all extensionsCheck if another extension is availableSend data to UIDeprecated PatternsComplete Minimal ExampleChecklistSee Also