Vehicle Modding Guide
How to add custom logic to vehicles safely — extensions vs controllers, the electrics data bus, powertrain API, safe overrides, and UI visibility.
Adding custom logic to vehicles is where BeamNG modding gets powerful — and where things can go wrong fast. This guide shows you the right way to modify vehicle behavior without breaking other mods or the base game.
1. Extension vs. Controller
- Extension: Best for modular features that should work on many vehicles (e.g., a "drift mode" script or "telemetry logger"). They load automatically if placed in
lua/vehicle/extensions/auto/. - Controller: Best for vehicle-specific hardware logic (e.g., a specific engine management system or complex animation). They are defined in the JBeam.
2. Creating a Basic Extension
Create a file at lua/vehicle/extensions/myAwesomeMod.lua:
local M = {}
-- Called once when vehicle is loaded
local function onInit()
print("My Awesome Mod Initialized!")
end
-- Called every graphics frame (UI rate)
local function updateGFX(dt)
-- UI logic here
end
-- Called every physics step (2000Hz)
-- Use this for torque and forces!
local function updateFixedStep(dt)
-- Physics logic here
end
-- Hook exports
M.onInit = onInit
M.updateGFX = updateGFX
M.updateFixedStep = updateFixedStep
return M3. Modifying Existing Systems (The Safe Way)
When modding, you should avoid modifying the core BeamNG files directly. Instead, build your logic in an extension and interact with other systems using the Data Bus or the Powertrain API.
A. The Data Bus (electrics.values)
The electrics.values table is the "shared memory" of the vehicle. It's where the engine, dashboard, and lights all store and read data.
How to use it:
- Reading Data: To check the vehicle's state, just read from the table.
local currentRPM = electrics.values.rpm or 0 if currentRPM > 5000 then -- Do something at high RPM end - Injecting/Overriding Data: You can write your own values into this table to trigger other systems (like dash lights or props) or override existing ones.
local function updateGFX(dt) -- Force the 'Check Engine' light on electrics.values.checkengine = 1 -- Create a custom signal for a JBeam prop to use electrics.values.my_mod_needle = math.sin(os.clock()) end - Why use it? It decouples your script from other systems. Your script doesn't need to know how the dashboard works; it just needs to set a value that the dashboard is programmed to watch.
B. The Powertrain API
If you want to change how the vehicle moves (add power, lock wheels, change gears), you use the powertrain module to find and control physical devices.
Adding Power (Architectural Solution)
Don't hijack temporary properties or other systems. Instead, modify the engine's base torque curve during initialization. This is standalone, non-destructive, and visible in UI tools.
local function onInit()
local eng = powertrain.getDevice("mainEngine")
if eng and eng.torqueCurve then
-- Add +20% boost to the base curve permanently for this spawn
for rpm, torque in pairs(eng.torqueCurve) do
if type(rpm) == "number" then
eng.torqueCurve[rpm] = torque * 1.2
end
end
-- Re-calculate internal engine data so UI and logic stays synced
eng.torqueData = eng:getTorqueData()
eng.maxTorque = eng.torqueData.maxTorque
eng.maxPower = eng.torqueData.maxPower
end
endMaster Multipliers (Restrictors)
If you want to create a "Restrictor Plate" or a dynamic power multiplier (e.g., for a "Valet Mode"), use the outputTorqueState property. This is a master multiplier for the final calculated torque.
local function updateGFX(dt)
local eng = powertrain.getDevice("mainEngine")
if eng then
-- 0.8 = -20% power across the board
eng.outputTorqueState = 0.8
end
endC. Reacting to Events (Hooks)
Instead of "hijacking" a function by overriding it, listen for the events that the game already triggers. This ensures your mod works alongside other mods.
Essential Hooks:
onInit(): Setup your mod's variables.onVehicleReset(): Reset your state when the player presses 'R' or 'I'.onBeamBroke(id, energy): Detect when a specific part of the car breaks.updateGFX(dt): Use for visual/UI logic (runs at frame rate).updateFixedStep(dt): Use for physics/torque logic (runs at 2000Hz).
4. Advanced Modding Patterns
Overriding vs. Hooking
Rule: Hooks are almost always better than Overrides. An Override replaces a function entirely, which can break other mods. If you must override (e.g., to change how a core module calculates a specific value), always store and call the original function.
The Safe Override Pattern
The order in which you call the original function is critical:
- Original FIRST (Recommended): Call the original function at the beginning of your wrapper. This ensures that the core game logic finishes all its calculations first, allowing your mod to have the "final word" and preventing your changes from being overwritten by the original function.
- Original LAST: Call the original function at the end. Use this only if your logic is meant to prepare data for the original function to process.
-- How to safely override a function
local originalFunc = someModule.importantFunction
someModule.importantFunction = function(arg1, arg2)
-- 1. Call the original function FIRST
-- This lets the game do its standard work
local result = originalFunc(arg1, arg2)
-- 2. Apply your custom logic AFTER
-- Now you can modify the 'result' or set variables
-- knowing the game won't overwrite them this frame.
print("Game logic finished, now applying mod logic...")
return result
endAdding a Turbo Boost Button
This example shows how to combine input detection with powertrain manipulation.
local boostActive = false
local function updateGFX(dt)
-- Check if throttle is fully pressed and clutch is released
boostActive = electrics.values.throttle > 0.9 and (electrics.values.clutch or 0) < 0.1
end
local function updateFixedStep(dt)
if boostActive then
local eng = powertrain.getDevice("mainEngine")
if eng then
-- Apply 1.5x power multiplier (50% boost)
eng.outputTorqueState = 1.5
-- Visually show it on the dash
electrics.values.boost_active = 1
end
else
local eng = powertrain.getDevice("mainEngine")
if eng then eng.outputTorqueState = 1.0 end
electrics.values.boost_active = 0
end
end5. UI Visibility (Architectural Overrides)
If you want your modifications (like a 20% power boost) to show up in the Torque Curve UI App, you must safely override the engine's data reporting function.
A. Avoiding "Ghost Values"
A Ghost Value occurs when your mod changes the physics of the vehicle (e.g., adding torque via outputTorqueState) but fails to update the UI data. This leads to a confusing user experience where the vehicle feels stronger or weaker than the gauges and graphs indicate.
- Rule: If you change how much power a wheel gets or how hot an engine gets, you MUST ensure that
electrics.valuesand reporting functions likegetTorqueData()reflect those changes.
B. The getTorqueData Override
The Torque Curve app calls eng:getTorqueData(). You can wrap this function to modify the results before the UI sees them.
local function onInit()
local eng = powertrain.getDevice("mainEngine")
if not eng then return end
-- 1. Store the original function
local orgGetTorqueData = eng.getTorqueData
-- 2. Define the wrapper
eng.getTorqueData = function(device)
-- 3. Call the original to get the base curves
local data = orgGetTorqueData(device)
-- 4. Apply your multiplier to every point in the curves
local multiplier = 1.2 -- 20% boost
for _, curve in ipairs(data.curves) do
for rpm, torque in pairs(curve.torque) do
curve.torque[rpm] = torque * multiplier
end
for rpm, power in pairs(curve.power) do
curve.power[rpm] = power * multiplier
end
end
-- 5. Update the max values so the graph scales correctly
data.maxTorque = data.maxTorque * multiplier
data.maxPower = data.maxPower * multiplier
return data
end
end6. Physical Consequences of Adding Power
When you add power via curve modification or outputTorqueState, the rest of the vehicle physics will react naturally to that force.
- Torque Converter: It will handle the extra torque, but it will slip more. Since a torque converter uses fluid coupling, more torque results in more internal shear, which generates extra heat. If you add 200% more power, you may need to upgrade the torque converter's thermal capacity in JBeam or it will overheat and fail.
- Drivetrain Stress: Every component downstream (Gearbox, Shafts, Differentials) has a physical limit defined in JBeam (e.g.,
beamStrength). Adding too much torque can cause these parts to snap or lock up if they aren't rated for the new load. - Thermals: The engine itself will generate more heat. If you only add power via Lua and don't touch the cooling system, the engine will overheat faster under load.
7. How to properly do common tasks
A. Adding a new Mode (e.g., Drift Mode)
Don't modify the existing driveModes.lua. Instead, create an extension that monitors a toggle and modifies steering or torque logic.
- Toggle: Use a key bind or UI button to set a flag in your extension.
- Logic: In
updateFixedStep, if the flag is active, modify steering coefficients inelectrics.values.steeringOverrideor add torque to the wheels.
B. Creating a Telemetry Logger
- Capture: In
updateGFX, collect values fromelectrics.valuesorobj:getPosition(). - Storage: Use a local table to buffer data.
- Output: Use
dumpToFileorprintto export data periodically.
8. Avoiding Interferences
- Namespace Your Electrics: If you create a custom signal, use a unique name (e.g.,
electrics.values.myMod_active = 1). - Use auto Loading: Placing extensions in
extensions/auto/ensures they load without needing to modify the vehicle'smain.lua. - Check for Existence: Always check if a device exists before calling methods on it:
local diff = powertrain.getDevice("rearDiff") if diff then diff:lockUp() end
See Also
- Controller Template: Reference for controller structure.
- Developer Recipes: Snippets for common tasks.
- Cheat Sheet: One-liners for quick modding.
- Powertrain Base Device: Methods available for torque manipulation.
62 Vehicle Scripting Recipes
Copy-paste solutions for powertrain control, JBeam manipulation, input handling, UI communication, and niche modding tasks.
Understanding Vehicle Damage Systems
How BeamNG's three damage systems work together — beamstate for physics, damageTracker for gameplay, and partCondition for persistence.