GE Developer Recipes
A comprehensive collection of 50+ practical solutions for common Game Engine (GE) development tasks. Each recipe is copy-paste ready with explanations.
A comprehensive collection of 50+ practical solutions for common Game Engine (GE) development tasks. Each recipe is copy-paste ready with explanations.
Table of Contents
- Extension Fundamentals
- Custom Missions
- Career Integration
- UI Popups & Notifications
- Flowgraph Scripting
- Vehicle Management
- World & Environment
- Traffic & AI
- Save & Persistence
- Utility Patterns
1. Extension Fundamentals
Recipe 1.1: Minimal GE Extension
local M = {}
local logTag = 'myMod'
local function onExtensionLoaded()
log('I', logTag, 'Extension loaded')
end
M.onExtensionLoaded = onExtensionLoaded
return MRecipe 1.2: Extension with Dependencies
local M = {}
M.dependencies = {'career_career', 'career_saveSystem', 'freeroam_facilities'}
-- Dependencies are loaded before this extension's hooks fire
return MRecipe 1.3: Hot-Reload Extension During Development
-- Load an extension
extensions.load('gameplay_taxi')
-- Unload when done
extensions.unload('gameplay_taxi')
-- Hot-reload (unload + load) for rapid iteration
extensions.reload('gameplay_taxi')Recipe 1.4: Persist Extension Across Level Changes
-- By default, extensions unload on level change
-- Use manual mode to persist
setExtensionUnloadMode("gameplay_taxi", "manual")
extensions.load("gameplay_taxi")Recipe 1.5: Fire Custom Hook to All Extensions
-- Trigger a custom hook that any extension can listen to
extensions.hook("onMyCustomEvent", {data = 42, source = "myMod"})
-- In another extension, receive the event:
local function onMyCustomEvent(data)
log('I', 'otherMod', 'Received event with data: ' .. dumps(data))
end
M.onMyCustomEvent = onMyCustomEvent2. Custom Missions
Recipe 2.1: Start a Mission Programmatically
-- Start a mission by its ID
local missionId = "mission_timeTrial_01"
gameplay_missions_missionManager.start(missionId)
-- Start with attempt data
local attemptData = {
vehicle = "etk800",
difficulty = "hard"
}
gameplay_missions_missionManager.start(missionId, attemptData)Recipe 2.2: Check Mission Status
-- Check if any mission is active
local activeMissionId = gameplay_missions_missionManager.getForegroundMissionId()
if activeMissionId then
log('I', 'myMod', 'Active mission: ' .. activeMissionId)
else
log('I', 'myMod', 'No mission active')
end
-- Get mission state
local state = gameplay_missions_missionManager.getMissionState()
-- state can be: "none", "started", "stopped", etc.Recipe 2.3: Stop Current Mission
-- Stop the foreground mission
local missionId = gameplay_missions_missionManager.getForegroundMissionId()
if missionId then
gameplay_missions_missionManager.stop(missionId)
endRecipe 2.4: Create Mission Attempt Data
-- Structure for tracking a mission attempt
local attempt = {
type = "passed", -- "completed", "passed", "attempted", "abandoned", "failed"
date = os.time(),
humanDate = os.date("!%Y-%m-%dT%TZ"),
data = {
points = 1500,
time = 45.32,
penalties = 0
},
unlockedStars = {
defaultStar0 = true,
bonusStar0 = true
}
}
-- Aggregate the attempt into progress
gameplay_missions_progress.aggregateAttempt(missionId, attempt, "default")Recipe 2.5: Get Mission Progress
-- Get mission by ID
local mission = gameplay_missions_missions.getMissionById(missionId)
-- Check if mission is unlocked
local isUnlocked = gameplay_missions_unlocks.getSimpleUnlockedStatus(missionId)
-- Format save data for UI display
local uiData = gameplay_missions_progress.formatSaveDataForUi(missionId)Recipe 2.6: Mission with Star Rewards
-- Define star rewards in mission configuration
local missionConfig = {
careerSetup = {
starRewards = {
defaultStar0 = {
{ attributeKey = "money", rewardAmount = 500 },
{ attributeKey = "beamXP", rewardAmount = 10 },
},
bonusStar0 = {
{ attributeKey = "money", rewardAmount = 200 },
{ attributeKey = "voucher", rewardAmount = 1 },
}
}
}
}3. Career Integration
Recipe 3.1: Guard Code Behind Career Check
-- Always check if career is active before using career modules
if not career_career or not career_career.isActive() then
return
end
-- Safe to use career modules here
local money = career_modules_playerAttributes.getAttributeValue("money")Recipe 3.2: Pay Money from Player
-- Check if player has enough money first
local price = { money = { amount = 5000, canBeNegative = false } }
local currentMoney = career_modules_playerAttributes.getAttributeValue("money")
if currentMoney >= 5000 then
career_modules_payment.pay(price, { label = "Purchased garage upgrade" })
return true
else
-- Show insufficient funds notification
guihooks.trigger("toastrMsg", {
type = "error",
title = "Insufficient Funds",
msg = "You need $5,000 for this purchase."
})
return false
endRecipe 3.3: Reward Player
-- Give money/XP reward via addAttributes
local change = { money = 1500, beamXP = 50 }
local reason = { label = "Delivery completed" }
career_modules_playerAttributes.addAttributes(change, reason)
-- Show toast notification manually
guihooks.trigger("toastrMsg", {
type = "success",
title = "Reward!",
msg = "You earned $1,500 and 50 XP"
})Recipe 3.4: Read Player Attributes
-- Get various player attributes
local money = career_modules_playerAttributes.getAttributeValue("money")
local beamXP = career_modules_playerAttributes.getAttributeValue("beamXP")
local vouchers = career_modules_playerAttributes.getAttributeValue("voucher")
-- Check if player has specific amount
if money >= 10000 then
-- Unlock premium feature
endRecipe 3.5: Reputation Management
-- Add/update reputation data for an organization object
-- Note: addReputationToOrg takes an organization table, not a string
career_modules_reputation.addReputationToOrg(organizationTable)
-- Get value for a specific event type
local val = career_modules_reputation.getValueForEvent("returnLoanerDamaged")
-- Get label for a reputation level
local label = career_modules_reputation.getLabel(levelNumber)Recipe 3.6: Career Save/Load Integration
-- Save custom data when career saves
local function onSaveCurrentSaveSlot(currentSavePath)
local dirPath = currentSavePath .. "/career/myMod"
if not FS:directoryExists(dirPath) then
FS:directoryCreate(dirPath)
end
career_saveSystem.jsonWriteFileSafe(dirPath .. "/state.json", myState, true)
end
M.onSaveCurrentSaveSlot = onSaveCurrentSaveSlot
-- Load data when career modules activate
local function onCareerModulesActivated(alreadyInLevel)
if not career_career.isActive() then return end
local _, path = career_saveSystem.getCurrentSaveSlot()
if not path then return end
myState = jsonReadFile(path .. "/career/myMod/state.json") or {}
end
M.onCareerModulesActivated = onCareerModulesActivatedRecipe 3.7: Inventory Management
-- Get player's owned vehicles
local inventory = career_modules_inventory
local vehicles = inventory.getVehicles()
-- Get specific vehicle data
local vehicleData = inventory.getVehicle(inventoryId)
-- Spawn a vehicle from inventory
inventory.spawnVehicle(inventoryId)
-- Get current vehicle inventory ID
local currentId = inventory.getCurrentVehicleId()Recipe 3.8: Trigger Manual Career Save
-- Force a career save (use sparingly)
career_saveSystem.saveCurrent()
-- With callback when complete
career_saveSystem.saveCurrent(function(success)
if success then
guihooks.trigger("toastrMsg", {
type = "success",
title = "Game Saved",
msg = "Your progress has been saved."
})
end
end)4. UI Popups & Notifications
Recipe 4.1: Toast Notification
-- Simple toast notification (non-blocking, auto-dismiss)
guihooks.trigger("toastrMsg", {
type = "success", -- "success", "error", "warning", "info"
title = "Game Saved",
msg = "Your progress has been saved."
})
-- Error toast
guihooks.trigger("toastrMsg", {
type = "error",
title = "Update Required",
msg = "Please update to the latest version."
})
-- Warning with label (for grouping)
guihooks.trigger("toastrMsg", {
type = "warning",
label = "vehStored",
title = "Vehicle stored",
msg = "Damaged vehicles have been moved to storage."
})Recipe 4.2: Full Notification Dialog
-- Full notification with more prominence
guihooks.trigger("Notification", {
type = "info", -- "info", "warning", "error", "success"
title = "Mission Complete",
message = "You have successfully delivered all packages."
})Recipe 4.3: Confirmation Dialog
-- Show confirmation then execute callback
guihooks.trigger("ConfirmDialog", {
title = "Are you sure?",
message = "This will delete your save file permanently.",
confirmLabel = "Delete",
cancelLabel = "Cancel",
onConfirm = "myExtension.confirmDelete()",
onCancel = "myExtension.cancelDelete()"
})
-- Handler functions
local function confirmDelete()
-- Perform deletion
log('I', 'myMod', 'User confirmed deletion')
end
M.confirmDelete = confirmDelete
local function cancelDelete()
-- Cleanup if needed
log('I', 'myMod', 'User cancelled deletion')
end
M.cancelDelete = cancelDeleteRecipe 4.4: UI State Navigation
-- Navigate to a UI state
guihooks.trigger("ChangeState", {state = "play", params = {}})
guihooks.trigger("ChangeState", {state = "menu"})
guihooks.trigger("ChangeState", {state = "vehicleInventory"})
guihooks.trigger("ChangeState", {state = "purchase-garage"})
-- Open specific UI module
guihooks.trigger("MenuOpenModule", "vehicleselect")
-- Navigate back
guihooks.trigger("UINavigation", "back", 1)
-- Hide menu
guihooks.trigger("MenuHide")Recipe 4.5: Send Data to UI
-- Send one-time data update
guihooks.trigger("vehicleInventoryData", {
vehicles = getVehicleList(),
balance = getMoney(),
garageName = "Main Garage"
})
-- Simple message popup
guihooks.message("Purchase complete!", 5, "info")Recipe 4.6: Stream High-Frequency Data
-- For telemetry or live dashboards
function M.onUpdate(dtReal, dtSim, dtRaw)
guihooks.queueStream("myModSpeed", currentSpeed)
guihooks.queueStream("myModFuel", fuelPercent)
guihooks.queueStream("myModRPM", engineRPM)
-- Streams are flushed automatically by main.lua's render loop
end
-- In UI: bngApi.engineLua('myMod.getStreamData()', callback)Recipe 4.7: Screen Fade Effect
-- Fade to black over 0.5 seconds
ui_fadeScreen.start(0.5)
-- React to fade state changes
local function onScreenFadeState(state)
if state == 1 then
-- Fully black - do transition work
performLevelTransition()
ui_fadeScreen.stop(0.5) -- Fade back in
elseif state == 3 then
-- Fully visible again - cleanup
cleanupTransition()
end
end
M.onScreenFadeState = onScreenFadeState5. Flowgraph Scripting
Recipe 5.1: Access Flowgraph Module
-- Get a flowgraph module
local missionModule = self.mgr:getModule("mission")
-- Process vehicles at mission start
missionModule:processVehicles({ keepPlayer = true, keepTraffic = false })
-- Get original player vehicle ID
local origId = missionModule:getOriginalPlayerId()
-- Fire custom mission event
missionModule:missionHook("checkpointReached", { index = 3 })Recipe 5.2: Create Custom Flowgraph Node
-- Template for custom node
local C = {}
C.name = "My Custom Node"
C.type = "node"
C.category = "Gameplay"
C.pinSchema = {
{dir = "in", type = "flow", name = "flow", description = "Input flow"},
{dir = "in", type = "number", name = "value", description = "Input value"},
{dir = "out", type = "flow", name = "flow", description = "Output flow"},
{dir = "out", type = "number", name = "result", description = "Result"}
}
function C:init()
-- Initialize node state
self.count = 0
end
function C:_executionStarted()
-- Called when execution starts
self.count = 0
end
function C:work(args)
-- Process input and set output
self.count = self.count + 1
self.pinOut.result.value = self.pinIn.value.value * 2
self.pinOut.flow.value = self.pinIn.flow.value
end
function C:_executionStopped()
-- Cleanup when execution stops
end
return _flowgraph_createNode(C)Recipe 5.3: Flowgraph Timer Node
-- Create a countdown timer node
local C = {}
C.name = "Countdown Timer"
C.type = "node"
C.category = "Logic"
C.pinSchema = {
{dir = "in", type = "flow", name = "start", description = "Start timer"},
{dir = "in", type = "number", name = "duration", default = 10, description = "Duration in seconds"},
{dir = "out", type = "flow", name = "finished", description = "Timer finished"},
{dir = "out", type = "number", name = "remaining", description = "Seconds remaining"}
}
function C:init()
self.running = false
self.endTime = 0
end
function C:work(args)
if self.pinIn.start.value then
self.running = true
self.endTime = os.clock() + self.pinIn.duration.value
end
if self.running then
local remaining = self.endTime - os.clock()
self.pinOut.remaining.value = math.max(0, remaining)
self.pinOut.finished.value = remaining <= 0
if remaining <= 0 then
self.running = false
end
end
end
return _flowgraph_createNode(C)Recipe 5.4: Vehicle Visibility Control
-- Hide/show player vehicle in flowgraph
local missionModule = self.mgr:getModule("mission")
-- Hide player vehicle, freeze traffic
missionModule:processVehicles({ keepPlayer = false, keepTraffic = false })
-- Show player vehicle, unfreeze traffic
missionModule:processVehicles({ keepPlayer = true, keepTraffic = true })
-- Prepare vehicle (headlights based on time of day)
missionModule:prepareVehicle(vehId)Recipe 5.5: Flowgraph Variable Access
-- Get/set flowgraph variables
local value = self.mgr:getVariable("myVariable")
self.mgr:setVariable("myVariable", newValue)
-- Check if variable exists
local exists = self.mgr:hasVariable("myVariable")
-- Get all variables as table
local vars = self.mgr:getVariables()6. Vehicle Management
Recipe 6.1: Get Player Vehicle
-- Get the player vehicle (index 0 = current player)
local veh = be:getPlayerVehicle(0)
if not veh then return end
local vehId = veh:getId()
local jbeamFile = veh:getJBeamFilename()Recipe 6.2: Spawn Vehicle
-- spawn.spawnVehicle(model, partConfig, pos, rot, options)
local pos = vec3(0, 0, 100)
local rot = quat(0, 0, 0, 1)
local options = {
licenseText = "AB-123-CD"
}
local veh = spawn.spawnVehicle("etk800", "vehicles/etk800/sport.pc", pos, rot, options)
-- Get spawned vehicle ID
local vehId = veh:getId()Recipe 6.3: Send Command to Vehicle
-- Send Lua command to vehicle's VE context
local veh = be:getPlayerVehicle(0)
if veh then
veh:queueLuaCommand("electrics.values.hazard = 1")
veh:queueLuaCommand("extensions.load('myVeExtension')")
end
-- Send to vehicle by ID
be:getObjectByID(vehId):queueLuaCommand("electrics.set_motor_boost(1)")Recipe 6.4: VE-to-GE Callback Pattern
-- In GE: spawn vehicle, then run VE code that calls back
veh:queueLuaCommand(string.format(
"partCondition.initConditions(nil, %d, nil, %f) obj:queueGameEngineLua('myMod.onSpawnComplete(%d)')",
mileage, visualCondition, vehId
))
-- Callback handler
local function onSpawnComplete(vehId)
log('I', 'myMod', 'Vehicle ' .. vehId .. ' spawn complete')
end
M.onSpawnComplete = onSpawnCompleteRecipe 6.5: Iterate All Vehicles
-- Loop through all vehicles in scene
for i = 0, be:getObjectCount() - 1 do
local veh = be:getObject(i)
local id = veh:getId()
local pos = veh:getPosition()
-- Process each vehicle
end
-- Delete all vehicles (iterate backwards)
for i = be:getObjectCount() - 1, 0, -1 do
be:getObject(i):delete()
end
-- Delete specific vehicle
veh:delete()Recipe 6.6: Teleport Vehicle
local veh = be:getPlayerVehicle(0)
if veh then
-- Set position
veh:setPosition(Point3F(x, y, z))
-- Set rotation
veh:setRotation(QuatF(0, 0, 0, 1))
-- Combined position + rotation
veh:setPositionRotation(x, y, z, rx, ry, rz, rw)
-- Reset physics (recommended after teleport)
veh:reset()
end7. World & Environment
Recipe 7.1: Time of Day Control
-- Set time via core_environment (0.0-1.0, where 0.5 = noon)
core_environment.setTimeOfDay({time = 0.5})
-- Or via scenetree TimeOfDay object
local tod = scenetree.findObject("TimeOfDay")
if tod then
tod.time = 0.5
tod.play = false -- Stop day/night cycle
endRecipe 7.2: Physics Control
-- Pause/resume via simTimeAuthority
simTimeAuthority.pause(true)
simTimeAuthority.pause(false)
-- Set physics speed factor
be:setPhysicsSpeedFactor(0.5) -- Half speed
be:setPhysicsSpeedFactor(1.0) -- NormalRecipe 7.3: Scene Object Access
-- Find object by name
local obj = scenetree.findObject("myTrigger")
if obj then
local pos = obj:getPosition()
local className = obj:getClassName()
end
-- Hide/show object
obj:setHidden(true)
obj:setHidden(false)
-- Get object by ID
local obj = scenetree.findObjectById(id)Recipe 7.4: Facility Access
-- Get all facilities of a type
local garages = freeroam_facilities.getFacilitiesByType("garage")
local gasStations = freeroam_facilities.getFacilitiesByType("gasStation")
-- Get specific facility
local garage = freeroam_facilities.getFacility("garage", garageId)
-- Get garage position and rotation
local pos, rot = freeroam_facilities.getGaragePosRot(garageId)Recipe 7.5: Map/Level Info
-- Get current level identifier
local levelName = getCurrentLevelIdentifier()
-- Get map data
local mapData = map.getMap()
local mapName = map.getMapName()
local mapPath = map.getMapPath()
-- Access road nodes
local nodes = map.getMap().nodes
local nodePos = nodes["nodename"].pos8. Traffic & AI
Recipe 8.1: Get Traffic Vehicles
-- Get list of all traffic vehicles
local trafficVehicles = gameplay_traffic.getTrafficList()
for _, tVeh in ipairs(trafficVehicles) do
local id = tVeh.id
local role = tVeh.role -- "police", "civilian", etc.
-- Process traffic vehicle
end
-- Get traffic data table
local trafficData = gameplay_traffic.getTrafficData()Recipe 8.2: Set AI Role
-- Assign police role to traffic vehicle
local vehicle = gameplay_traffic.getTrafficData()[vehId]
if vehicle then
vehicle:setRole("police")
end
-- Available roles: "police", "civilian", "ambulance", "fire", "highwayPatrol"Recipe 8.3: AI Driving Commands
local obj = be:getObjectByID(vehId)
-- Drive in lane
obj:queueLuaCommand('ai.driveInLane("on")')
-- Set speed mode
obj:queueLuaCommand('ai.setSpeedMode("off")') -- No speed limit
obj:queueLuaCommand('ai.setSpeedMode("limit")') -- Obey speed limits
obj:queueLuaCommand('ai.setSpeed(20)') -- m/s
-- Set target
obj:queueLuaCommand(string.format('ai.setTarget(%f, %f, %f)', x, y, z))Recipe 8.4: Spawn Traffic
-- Add traffic vehicle
gameplay_traffic.addTrafficVehicle(vehId)
-- Remove traffic vehicle
gameplay_traffic.removeTrafficVehicle(vehId)
-- Setup parked cars
gameplay_traffic.setupParkedCars(config)9. Save & Persistence
Recipe 9.1: Read/Write JSON Settings
local settingsPath = 'settings/myMod/config.json'
local function loadSettings()
return jsonReadFile(settingsPath) or {
volume = 1.0,
enabled = true,
difficulty = "normal"
}
end
local function saveSettings(data)
if not FS:directoryExists('settings/myMod') then
FS:directoryCreate('settings/myMod')
end
jsonWriteFile(settingsPath, data, true)
endRecipe 9.2: Safe File Writing
-- Use safe write for critical save data
-- Writes to temp file first, then moves to prevent corruption
career_saveSystem.jsonWriteFileSafe(
"path/to/data.json", -- file path
myTable, -- data to save
true -- pretty print
)Recipe 9.3: File System Operations
-- Check existence
if FS:fileExists("/path/to/file.json") then ... end
if FS:directoryExists("/path/to/dir") then ... end
-- Create directory (recursive)
FS:directoryCreate("/path/to/deep/dir", true)
-- Find files
local luaFiles = FS:findFiles("/lua/ge/extensions/", "*.lua", -1, true, false)
-- Read/Write raw files
local content = readFile("/path/to/file.txt")
writeFile("/path/to/output.txt", content)Recipe 9.4: Mission Progress Save Slot
-- Set save slot for mission progress
gameplay_missions_progress.setSaveSlot("career_slot1")
-- Get current slot
local slotName, savePath = gameplay_missions_progress.getSaveSlot()10. Utility Patterns
Recipe 10.1: Input Action Filtering
-- Block specific editor actions
local blocked = {"editorToggle", "editorSafeModeToggle", "vehicleEditorToggle"}
core_input_actionFilter.setGroup("MY_MOD_BLOCK", blocked)
-- Enable blocking
core_input_actionFilter.addAction(0, "MY_MOD_BLOCK", true)
-- Disable blocking
core_input_actionFilter.addAction(0, "MY_MOD_BLOCK", false)Recipe 10.2: Async Job System
-- Create non-blocking delayed work
core_jobsystem.create(function(job)
job.sleep(3) -- Wait 3 seconds
log('I', 'myMod', 'Delayed work done')
-- Can chain more work
job.sleep(1)
doMoreWork()
end)Recipe 10.3: World Ready State
-- Run code when world is fully loaded
local function onWorldReadyState(state)
if state == 2 then -- 2 = fully ready
initializeMyMod()
end
end
M.onWorldReadyState = onWorldReadyStateRecipe 10.4: Career Activation Hook
-- React to career activation/deactivation
local function onCareerActive(active)
if active then
startCareerFeatures()
else
cleanupCareerFeatures()
end
end
M.onCareerActive = onCareerActiveRecipe 10.5: Vehicle Spawn Hook
-- React to vehicle spawn
local function onVehicleSpawned(objID, veh)
veh:queueLuaCommand("extensions.load('myVeExtension')")
end
M.onVehicleSpawned = onVehicleSpawnedRecipe 10.6: Mod Deactivation Cleanup
-- Cleanup when mod is deactivated
local function onModDeactivated(modData)
if modData.modname == "my_mod_name" then
cleanup()
saveState()
end
end
M.onModDeactivated = onModDeactivatedRecipe 10.7: Throttled UI Updates
-- Don't update UI every frame - throttle to 10Hz
function M.onUpdate(dtReal, dtSim, dtRaw)
if not M.isActive then return end
M.uiTimer = (M.uiTimer or 0) + dtReal
if M.uiTimer > 0.1 then -- 10Hz update rate
M.uiTimer = 0
guihooks.trigger('MyModUpdate', M.getState())
end
endRecipe 10.8: State + UI Sync Pattern
-- Always keep UI in sync with state (No Ghost Values!)
local function setActive(active)
M.isActive = active
-- Immediately notify UI
guihooks.trigger('MyModStateChanged', {active = M.isActive})
end
-- DON'T do this (creates ghost value):
-- M.isActive = true -- UI doesn't know about this!Quick Reference
| Category | Recipes | Key Modules |
|---|---|---|
| Extensions | 5 | extensions |
| Missions | 6 | gameplay_missions_* |
| Career | 8 | career_modules_*, career_saveSystem |
| UI | 7 | guihooks, ui_fadeScreen |
| Flowgraph | 5 | _flowgraph_* |
| Vehicles | 6 | be, spawn |
| World | 5 | be, scenetree, map |
| Traffic | 4 | gameplay_traffic |
| Save/Persistence | 4 | FS, json* |
| Utilities | 8 | Various |
| Total | 58 | - |
See Also
- GE Cheat Sheet - 50+ common one-liners
- GE TL;DR Architect - Extension lifecycle rules
- GE SKILL.md - Complete GE documentation hub
- VE Recipes - Vehicle Engine recipes