Bridge/API Layer - Lua ↔ UI Communication
The bridge layer connects JavaScript UI code to the Lua game engine. It provides event handling, stream subscriptions, and typed Lua function calls.
The bridge layer connects JavaScript UI code to the Lua game engine. It provides event handling, stream subscriptions, and typed Lua function calls.
Overview
The bridge provides:
- Events - Fire-and-forget messages from Lua to UI
- Streams - High-frequency data channels (electrics, sensors, etc.)
- Lua calls - Invoke Lua functions from JavaScript
- Hooks - Global event dispatch system
Using the Bridge
Import Methods
// Method 1: useBridge composable
import { useBridge } from '@/bridge'
const { lua, api, events, streams } = useBridge()
// Method 2: Direct imports
import { lua } from '@/bridge'
// Method 3: Via useLibStore (in apps)
import { useLibStore } from '@/services'
const { $game } = useLibStore()
// $game.lua, $game.events, $game.streams, $game.apiEvents
Events are one-way messages from Lua → UI.
Listening for Events
import { useEvents } from '@/services/events'
// Auto-cleanup on component unmount
const events = useEvents()
events.on('MyEvent', (data) => {
console.log('Received:', data)
})
events.once('OneTimeEvent', (data) => {
// Only fires once
})
// Manual cleanup (if needed)
events.off('MyEvent', handler)Common Events
| Event | Data | Description |
|---|---|---|
VehicleChange | - | Player vehicle changed |
VehicleFocusChanged | {mode} | Camera focus changed |
ChangeState | {state, params} | Navigate to UI state |
Message | {category, text, icon, ttl} | Show HUD message |
toastrMsg | {type, title, msg} | Toast notification |
MenuHide | - | Hide menu overlay |
ClearTasklist | - | Clear task list |
Emitting Events (UI → UI)
events.emit('MyCustomEvent', { data: 'value' })Streams
Streams are high-frequency data channels, typically updated every frame.
Using useStreams Composable
import { useStreams } from '@/services/events'
// Subscribe to streams (auto-cleanup on unmount)
useStreams(['electrics', 'sensors'], (streams) => {
if (streams.electrics) {
const speed = streams.electrics.wheelspeed
const rpm = streams.electrics.rpm
}
if (streams.sensors) {
const yaw = streams.sensors.yaw
}
})Manual Stream Management
import { useBridge } from '@/bridge'
const { streams, events } = useBridge()
// Subscribe
streams.add(['electrics', 'engineInfo'])
// Listen for updates
events.on('onStreamsUpdate', (data) => {
console.log('Speed:', data.electrics?.wheelspeed)
})
// Unsubscribe
streams.remove(['electrics', 'engineInfo'])Common Streams
| Stream | Contents | Update Rate |
|---|---|---|
electrics | Speed, RPM, gear, lights, fuel | Every frame |
sensors | Yaw, pitch, roll, g-forces | Every frame |
engineInfo | Max RPM, idle RPM, shift points | On vehicle load |
damageData | Part damage values | On damage |
minimap | Location name, distance to target | Varies |
Stream Data Examples
// electrics stream
{
wheelspeed: 15.5, // m/s
rpm: 3500,
gear: 3,
gearIndex: 4, // Including neutral/reverse
fuel: 0.75,
lowbeam: true,
highbeam: false,
signal_L: false,
signal_R: false,
hazard: false,
ignitionLevel: 2, // 0=off, 1=acc, 2=on
parkingbrake: 0,
abs: false,
esc: false
}
// sensors stream
{
yaw: 1.57, // radians
pitch: 0.02,
roll: 0.01,
gravity: { x: 0, y: 0, z: -9.81 },
gx: 0.1, // G-forces
gy: 0.0,
gz: 1.0
}
// engineInfo stream
{
maxRPM: 7000,
idleRPM: 800,
shiftUpRPM: 6500,
shiftDownRPM: 2000
}Calling Lua Functions
Using Typed Signatures
import { lua } from '@/bridge'
// Call extension function (returns Promise)
const result = await lua.extensions.myExtension.myFunction(arg1, arg2)
// Call career module
await lua.career_modules_fuel.requestRefuelingTransactionData()
// Call core function
const details = await lua.core_vehicles.getCurrentVehicleDetails()
// Check extension state
const isActive = await lua.career_career.isActive()Function Signature Types
Signatures in LuaFunctionSignatures.js define parameter types:
// bridge/LuaFunctionSignatures.js
export default {
career_career: {
isActive: () => {}, // No params
createOrLoadCareerAndStart: (id, auto, tut) => // Typed params
[String, Any, Boolean]
},
guihooks: {
trigger: hookName => String // String param
},
core_vehicles: {
getCurrentVehicleDetails: () => {} // No params, returns object
}
}Raw Lua Execution
For functions not in signatures:
import { runRaw } from '@/bridge/libs/Lua.js'
// Execute arbitrary Lua code
const result = await runRaw('extensions.myMod.customFunction("arg")')
// Without promise (fire-and-forget)
runRaw('print("Hello from JS")', false)Using API for Vehicle Lua
import { useBridge } from '@/bridge'
const { api } = useBridge()
// Execute on active vehicle (VE context)
api.activeObjectLua('electrics.setIgnitionLevel(2)')
// Execute on game engine
api.engineLua('print("Hello")', (result) => {
console.log('Result:', result)
})Hooks System
The Hooks module handles global event dispatch.
How Hooks Work
- Lua triggers hook -
guihooks.trigger('EventName', data) - CEF receives -
window.multihookUpdate(hooksData) - Hooks dispatches - To both Angular and Vue event buses
- Components receive - Via
events.on('EventName', handler)
Internal Hook Handler
// bridge/libs/Hooks.js
const mainHookHandler = function(hookName, args) {
// Dispatch to Vue event bus
window.vueEventBus?.emit(hookName, ...args)
// Dispatch to Angular (legacy)
window.globalAngularRootScope?.$broadcast(hookName, ...args)
}Stream Update Flow
- Lua queues stream data -
guihooks.queueStream('electrics', data) - Lua flushes -
guihooks.sendStreams()(typically in updateGFX) - CEF receives -
window.streamUpdate(data) - StreamManager processes - Merges vehicle/global streams
- Event emitted -
vueEventBus.emit('onStreamsUpdate', data) - Coordinator syncs - Waits for Vue/Angular updates, signals completion
Stream Coordinator
The coordinator ensures UI updates complete before signaling the game engine.
// bridge/coordinator/lite.js
// Before broadcasting streams
coordinator.beforeBroadcast()
// Emit stream update
vueEventBus.emit('onStreamsUpdate', data)
// After broadcast - wait for Vue.nextTick and Angular $timeout
coordinator.afterBroadcast(() => {
// Signal game engine that UI frame is complete
window.beamng?.uiFrameCallback?.()
})Coordinator Features
- Vue sync - Waits for
Vue.nextTick() - Angular sync - Waits for
$timeout(0) - Safety timeout - Forces completion after 2000ms
- Skip detection - Handles overlapping updates
Game Blurrer
Region-based blur system for UI elements. Each blur region is tracked by ID and synced to Lua.
import { gameBlurrer } from '@/bridge'
// Register a blur region (returns an ID string)
const id = gameBlurrer.register({ x: 0, y: 0, width: 100, height: 100 })
// Update a registered blur region
gameBlurrer.update(id, { x: 10, y: 10, width: 200, height: 200 })
// Unregister a blur region
gameBlurrer.unregister(id)The blurrer lazily loads/unloads the ui_gameBlur Lua extension as needed. When the region list is empty, it unloads the extension automatically.
UI Units
Handle unit conversions (metric/imperial). The UIUnits class manages settings for length, speed, temperature, weight, torque, power, volume, pressure, etc.
import { useBridge } from '@/bridge'
const { units } = useBridge()
// Unit settings are keyed by category
// e.g. 'uiUnitLength', 'uiUnitTemperature', 'uiUnitWeight', etc.
// Categories: length, speed, temperature, weight, consumptionRate,
// torque, energy, date, power, volume, pressure
// The units object provides conversion methods per category
// Settings can be 'metric'/'imperial' or specific unit strings like 'bhp', 'psi', 'gal'Common Patterns
Fetch Data on Mount
import { onMounted, ref } from 'vue'
import { lua } from '@/bridge'
const data = ref(null)
onMounted(async () => {
data.value = await lua.myExtension.getData()
})React to Events
import { ref, onMounted, onUnmounted } from 'vue'
import { useBridge } from '@/bridge'
const { events } = useBridge()
const isActive = ref(false)
function onStateChange(data) {
isActive.value = data.active
}
onMounted(() => {
events.on('MyStateChanged', onStateChange)
})
onUnmounted(() => {
events.off('MyStateChanged', onStateChange)
})Stream + Event Combo
import { ref, onMounted, onUnmounted } from 'vue'
import { useLibStore } from '@/services'
const { $game } = useLibStore()
const speed = ref(0)
const vehicleName = ref('')
onMounted(() => {
// Stream for continuous data
$game.streams.add(['electrics'])
$game.events.on('onStreamsUpdate', onStreams)
// Event for discrete changes
$game.events.on('VehicleChange', onVehicleChange)
})
onUnmounted(() => {
$game.streams.remove(['electrics'])
$game.events.off('onStreamsUpdate', onStreams)
$game.events.off('VehicleChange', onVehicleChange)
})
function onStreams(streams) {
speed.value = streams.electrics?.wheelspeed || 0
}
async function onVehicleChange() {
const details = await $game.lua.core_vehicles.getCurrentVehicleDetails()
vehicleName.value = details?.configs?.Name || 'Unknown'
}Gotchas
- Lua calls return Promises - Always use
awaitor.then() - Stream data may be undefined - Check before accessing properties
- Event names are case-sensitive - Match Lua
guihooks.triggerexactly - Clean up subscriptions - Memory leaks if you don't
- Streams are reference-counted - Multiple subscribers OK, but each must unsubscribe
See Also
- GUIHooks Reference - Lua side of communication
- Services - useEvents, useStreams wrappers
- Lua Integration - Full round-trip patterns