RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Your First BeamNG ModMod Scripts (modScript.lua)Creating a GE ExtensionLoading Extension FoldersTesting & Debugging Your ModPublishing Your Mod

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

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 config

Files 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_myFeature

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

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

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

  1. Finds the file at the corresponding path
  2. Executes it and stores the returned table
  3. Calls onExtensionLoaded() if defined
  4. 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 M

Common hooks:

  • onExtensionLoaded - Extension just loaded
  • onUpdate(dtReal, dtSim, dtRaw) - Every frame
  • onVehicleSpawned(vehicleId) - Vehicle created
  • onVehicleSwitched(oldId, newId, player) - Player switched vehicle
  • onClientStartMission(levelPath) - Level loading
  • onClientEndMission(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

APIWhat It DoesExample
extensions.load()Load an extensionextensions.load('mymod_feature')
extensions.hook()Call a hook on all extensionsextensions.hook('onMyEvent', data)
guihooks.trigger()Send event to UIguihooks.trigger('Event', data)
be:getObjectByID()Get vehicle by IDbe:getObjectByID(vehicleId)
be:getPlayerVehicle(0)Get player's vehiclelocal veh = be:getPlayerVehicle(0)
log(level, tag, msg)Log a messagelog('I', 'mymod', 'hello')
setExtensionUnloadMode()Set persistence (use in modScript.lua)setExtensionUnloadMode("mymod_feature", "manual")
scenetree.findObject()Find scene objectscenetree.findObject("thePlayer")

Common Mistakes

These will waste hours of debugging

Every mistake below has tripped up experienced modders. Read them before you start.

  1. Not using a mod script - Your extension disappears on level change. Register it in scripts/mymod/modScript.lua with setExtensionUnloadMode. See Mod Scripts.

  2. Forgetting to return M - Your extension won't load. Always end with return M.

  3. Using registerCoreModule - This is deprecated. Use setExtensionUnloadMode in your modScript.lua instead.

  4. Calling extension functions before they're loaded - Use extensions.load() first, or check with extensions.isExtensionLoaded('name').

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

BeamNG Modding Docs

Comprehensive documentation for BeamNG.drive Lua modding - GE extensions, VE extensions, UI framework, and more.

Mod Scripts (modScript.lua)

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

On this page

Mod File StructureCreating a GE ExtensionFile Path = Extension NameBasic Extension TemplateLoading Extensions CorrectlyAuto-loaded ExtensionsManual Load Mode (Persistent Extensions)Loading Your ExtensionLoading from UI/JavaScriptHooks - Responding to Game EventsCommunicating with UIGE Lua to UI (JavaScript)UI (JavaScript) to GE LuaUI Listening for EventsKey APIsCommon MistakesSee Also