RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Reference

UI

UI Apps - HUD WidgetsBridge/API Layer - Lua ↔ UI CommunicationBeamNG UI Development Cheat SheetCommon Components - Bng* UI ElementsLua ↔ UI Integration - Full Round-Trip PatternsServices - Shared UtilitiesVue Modules - Full-Page Screens

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

UI Framework

Lua ↔ UI Integration - Full Round-Trip Patterns

Complete patterns for bidirectional communication between Lua game engine and Vue UI.

Complete patterns for bidirectional communication between Lua game engine and Vue UI.


Overview

Communication flows in two directions:

┌─────────────────────────────────────────────────────────────┐
│                   Lua → UI (Push)                           │
│  • guihooks.trigger() - One-time events                     │
│  • guihooks.queueStream() - High-frequency data             │
│  • guihooks.triggerRequest() - Request with callback        │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                   UI → Lua (Call)                           │
│  • lua.extension.function() - Typed function call           │
│  • runRaw() - Arbitrary Lua execution                       │
│  • api.engineLua() - Low-level with callback                │
└─────────────────────────────────────────────────────────────┘

Pattern 1: Event-Based Communication

Lua → UI: Fire Event

-- Lua (GE extension)
local function onSomethingHappened()
  guihooks.trigger('MyEventName', {
    value = 42,
    name = "Example",
    items = {"a", "b", "c"}
  })
end

UI: Listen for Event

// Vue component
import { useEvents } from '@/services/events'

const events = useEvents()

events.on('MyEventName', (data) => {
  console.log('Value:', data.value)      // 42
  console.log('Name:', data.name)        // "Example"
  console.log('Items:', data.items)      // ["a", "b", "c"]
})

Complete Example: State Change

-- Lua: career_myFeature.lua
local M = {}
local isActive = false

function M.setActive(active)
  isActive = active
  guihooks.trigger('MyFeatureStateChanged', {active = isActive})
end

function M.toggle()
  M.setActive(not isActive)
end

return M
<!-- Vue: MyFeatureWidget.vue -->
<template>
  <div :class="{ active: isActive }">
    {{ isActive ? 'Active' : 'Inactive' }}
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useEvents } from '@/services/events'

const events = useEvents()
const isActive = ref(false)

events.on('MyFeatureStateChanged', (data) => {
  isActive.value = data.active
})
</script>

Pattern 2: Stream-Based Communication

For high-frequency data (every frame or nearly).

Lua: Send Stream Data

-- Lua: In updateGFX hook (runs every frame)
local function updateGFX(dtReal, dtSim, dtRaw)
  -- Queue stream data
  guihooks.queueStream('myFeatureData', {
    speed = currentSpeed,
    position = {x = pos.x, y = pos.y, z = pos.z},
    progress = currentProgress
  })
  
  -- Flush all queued streams to UI
  guihooks.sendStreams()
end

M.updateGFX = updateGFX

UI: Subscribe to Stream

<template>
  <div class="my-widget">
    <span>Speed: {{ speed.toFixed(1) }} m/s</span>
    <span>Progress: {{ (progress * 100).toFixed(0) }}%</span>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useStreams } from '@/services/events'

const speed = ref(0)
const progress = ref(0)

useStreams(['myFeatureData'], (streams) => {
  if (streams.myFeatureData) {
    speed.value = streams.myFeatureData.speed || 0
    progress.value = streams.myFeatureData.progress || 0
  }
})
</script>

Pattern 3: UI → Lua Function Calls

Define Lua Function Signature

// bridge/LuaFunctionSignatures.js
export default {
  // ... existing signatures ...
  
  myMod_myExtension: {
    getData: () => {},                    // No params
    setConfig: (key, value) => [String, Any],  // Typed params
    processItems: (items) => Array,       // Array param
    calculate: (a, b) => [Number, Number] // Number params
  }
}

UI: Call Lua Function

import { lua } from '@/bridge'

// Call and await result
const data = await lua.myMod_myExtension.getData()

// Call with parameters
await lua.myMod_myExtension.setConfig('volume', 0.8)

// Call and use result
const result = await lua.myMod_myExtension.calculate(10, 20)
console.log('Result:', result)

Lua: Handle Function Call

-- Lua: myMod/myExtension.lua
local M = {}

function M.getData()
  return {
    items = {"a", "b", "c"},
    count = 3,
    active = true
  }
end

function M.setConfig(key, value)
  settings[key] = value
  -- Update state
end

function M.calculate(a, b)
  return a + b
end

return M

Pattern 4: Request-Response (Async)

UI requests data, Lua responds via event.

UI: Request Data

import { onMounted } from 'vue'
import { lua } from '@/bridge'
import { useEvents } from '@/services/events'

const events = useEvents()
const data = ref(null)

onMounted(() => {
  // Listen for response
  events.on('MyDataResponse', (responseData) => {
    data.value = responseData
  })
  
  // Request data
  lua.myMod_myExtension.requestData()
})

Lua: Handle Request, Send Response

-- Lua: myMod/myExtension.lua
local M = {}

function M.requestData()
  -- Do async work or gather data
  local data = gatherComplexData()
  
  -- Send response via event
  guihooks.trigger('MyDataResponse', data)
end

return M

Pattern 5: UI Opens Lua Menu/State

UI: Navigate via Lua

import { lua } from '@/bridge'

// Open career menu
await lua.career_career.closeAllMenus()

// Open vehicle shopping
await lua.career_modules_vehicleShopping.openShop(null, computerId)

// Start tuning mode
await lua.extensions.gameplay_garageMode.setGarageMenuState('tuning')

Lua: Trigger UI Navigation

-- Navigate to UI route
guihooks.trigger('ChangeState', {state = 'career/vehicleInventory'})

-- Open menu module
guihooks.trigger('MenuOpenModule', 'vehicleselect')

-- Navigate back
guihooks.trigger('UINavigation', 'back', 1)

-- Hide menu
guihooks.trigger('MenuHide')

Pattern 6: Notifications

Toast Messages

-- Lua: Show toast notification
guihooks.trigger("toastrMsg", {
  type = "success",   -- "info", "warning", "error", "success"
  title = "Saved!",
  msg = "Your vehicle has been saved."
})
// UI: Programmatic toast (if needed)
import { lua } from '@/bridge'

lua.guihooks.trigger('toastrMsg', JSON.stringify({
  type: 'info',
  title: 'Notice',
  msg: 'Something happened'
}))

HUD Messages

-- Lua: Show HUD message
guihooks.trigger('Message', {
  category = 'myFeature',
  text = 'Mission started!',
  icon = 'raceFlag',
  ttl = 5  -- seconds
})

-- Clear message
guihooks.trigger('Message', {
  category = 'myFeature',
  clear = true
})

Pattern 7: Confirmation Flow

UI: Show Confirmation, Execute Lua

<script setup>
import { openConfirmation } from '@/services/popup'
import { $translate } from '@/services'
import { lua } from '@/bridge'

async function confirmPurchase() {
  const confirmed = await openConfirmation(
    $translate.instant('ui.career.confirmPurchase'),
    $translate.instant('ui.career.purchaseMessage'),
    [
      { label: $translate.instant('ui.common.yes'), value: true },
      { label: $translate.instant('ui.common.no'), value: false }
    ]
  )
  
  if (confirmed) {
    await lua.career_modules_vehicleShopping.buyFromPurchaseMenu('vehicle', options)
  }
}
</script>

Lua: Trigger Confirmation

-- Lua can request UI confirmation
guihooks.trigger('ConfirmDialog', {
  title = "Confirm Action",
  message = "Are you sure?",
  confirmLabel = "Yes",
  cancelLabel = "No",
  onConfirm = "myExtension.doAction()",
  onCancel = "myExtension.cancelAction()"
})

Pattern 8: Complex Data Round-Trip

Full Example: Shopping Cart

-- Lua: career_modules_shopping.lua
local M = {}
local cart = {}

function M.getCartData()
  return {
    items = cart,
    total = calculateTotal(),
    canCheckout = #cart > 0
  }
end

function M.addItem(itemId)
  table.insert(cart, itemId)
  M.sendCartUpdate()
end

function M.removeItem(itemId)
  -- Remove from cart
  M.sendCartUpdate()
end

function M.sendCartUpdate()
  guihooks.trigger('ShoppingCartUpdated', M.getCartData())
end

function M.checkout()
  local success = processPayment()
  if success then
    cart = {}
    guihooks.trigger('CheckoutComplete', {success = true})
  else
    guihooks.trigger('CheckoutComplete', {success = false, error = "Payment failed"})
  end
end

return M
<!-- Vue: ShoppingCart.vue -->
<template>
  <div class="cart">
    <div v-for="item in items" :key="item.id" class="cart-item">
      {{ item.name }} - ${{ item.price }}
      <BngButton @click="removeItem(item.id)">Remove</BngButton>
    </div>
    
    <div class="cart-total">
      Total: ${{ total }}
    </div>
    
    <BngButton 
      :disabled="!canCheckout" 
      @click="checkout"
    >
      Checkout
    </BngButton>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useEvents } from '@/services/events'
import { lua } from '@/bridge'
import { openMessage } from '@/services/popup'

const events = useEvents()
const items = ref([])
const total = ref(0)
const canCheckout = ref(false)

onMounted(async () => {
  // Listen for cart updates
  events.on('ShoppingCartUpdated', updateCart)
  events.on('CheckoutComplete', onCheckoutComplete)
  
  // Get initial cart state
  const data = await lua.career_modules_shopping.getCartData()
  updateCart(data)
})

function updateCart(data) {
  items.value = data.items
  total.value = data.total
  canCheckout.value = data.canCheckout
}

async function removeItem(itemId) {
  await lua.career_modules_shopping.removeItem(itemId)
}

async function checkout() {
  await lua.career_modules_shopping.checkout()
}

function onCheckoutComplete(data) {
  if (data.success) {
    openMessage('Success', 'Purchase complete!')
  } else {
    openMessage('Error', data.error)
  }
}
</script>

Best Practices

1. Always Use Events for State Changes

-- ✅ Good: Trigger event when state changes
function M.setActive(active)
  M.isActive = active
  guihooks.trigger('MyStateChanged', {active = active})
end

-- ❌ Bad: UI might be out of sync
function M.setActive(active)
  M.isActive = active
  -- UI never knows!
end

2. Guard Stream Data

// ✅ Good: Check for undefined
useStreams(['myData'], (streams) => {
  if (streams.myData) {
    value.value = streams.myData.field || 0
  }
})

// ❌ Bad: Crash on undefined
useStreams(['myData'], (streams) => {
  value.value = streams.myData.field  // Error if undefined!
})

3. Cleanup Listeners

// ✅ Good: Use useEvents (auto-cleanup)
const events = useEvents()
events.on('MyEvent', handler)

// ⚠️ Manual: Must clean up yourself
onMounted(() => {
  $game.events.on('MyEvent', handler)
})
onUnmounted(() => {
  $game.events.off('MyEvent', handler)
})

4. Handle Async Properly

// ✅ Good: Await Lua calls
const data = await lua.myExtension.getData()
processData(data)

// ❌ Bad: Data undefined when used
const data = lua.myExtension.getData()
processData(data)  // data is a Promise, not the result!

5. Throttle High-Frequency Updates

-- Lua: Throttle UI updates
local uiUpdateTimer = 0
function M.updateGFX(dt)
  uiUpdateTimer = uiUpdateTimer + dt
  if uiUpdateTimer > 0.1 then  -- 10 Hz
    uiUpdateTimer = 0
    guihooks.trigger('MyUpdate', M.getState())
  end
end

Debug Tips

Log All Events (UI)

// Temporary debug: log all events
const originalEmit = window.vueEventBus.emit
window.vueEventBus.emit = function(name, ...args) {
  console.log('Event:', name, args)
  return originalEmit.apply(this, arguments)
}

Log Stream Data (UI)

useStreams(['electrics'], (streams) => {
  console.log('Streams:', JSON.stringify(streams, null, 2))
})

Verify Lua Function Exists

// Check if function is callable
if (lua.myExtension?.myFunction) {
  await lua.myExtension.myFunction()
} else {
  console.error('Function not found in signatures')
}

See Also

  • Bridge API - Low-level bridge details
  • GUIHooks Reference - Lua side documentation
  • Services - useEvents, useStreams composables

Common Components - Bng* UI Elements

Reusable Vue components and directives for building BeamNG UI.

Services - Shared Utilities

Services provide shared functionality across the UI: event handling, popups, translations, storage, and navigation.

On this page

OverviewPattern 1: Event-Based CommunicationLua → UI: Fire EventUI: Listen for EventComplete Example: State ChangePattern 2: Stream-Based CommunicationLua: Send Stream DataUI: Subscribe to StreamPattern 3: UI → Lua Function CallsDefine Lua Function SignatureUI: Call Lua FunctionLua: Handle Function CallPattern 4: Request-Response (Async)UI: Request DataLua: Handle Request, Send ResponsePattern 5: UI Opens Lua Menu/StateUI: Navigate via LuaLua: Trigger UI NavigationPattern 6: NotificationsToast MessagesHUD MessagesPattern 7: Confirmation FlowUI: Show Confirmation, Execute LuaLua: Trigger ConfirmationPattern 8: Complex Data Round-TripFull Example: Shopping CartBest Practices1. Always Use Events for State Changes2. Guard Stream Data3. Cleanup Listeners4. Handle Async Properly5. Throttle High-Frequency UpdatesDebug TipsLog All Events (UI)Log Stream Data (UI)Verify Lua Function ExistsSee Also