Phone UI System
Phone overlay system - toggleable in-game phone with apps, minimap, time display, and state management.
Phone overlay system - toggleable in-game phone with apps, minimap, time display, and state management.
Overview
The phone is a GE-side overlay controlled by gameplay_phone. It opens/closes via togglePhone(), enforces a speed check (must be ≤5 mph unless in a cab), and communicates state to the CEF/UI layer through guihooks.trigger.
Extension path: lua/ge/extensions/gameplay/phone.lua
Sub-modules: ui/phone/time.lua, ui/phone/minimap.lua
Phone API
| Function | Signature | Description |
|---|---|---|
togglePhone | togglePhone(reason?) | Open/close the phone. Optional reason string shown if blocked. |
isPhoneOpen | isPhoneOpen() → bool | Returns current open state. |
Toggle Logic
-- From gameplay_phone
local function togglePhone(reason)
if isPhoneOpen then
isPhoneOpen = false
guihooks.trigger('closePhone')
else
local playerSpeed = math.abs(be:getObjectVelocityXYZ(be:getPlayerVehicleID(0))) * speedUnit
if playerSpeed > 5 then
ui_message(reason or "You must be stationary to open the phone.", 3, "info", "info")
return
end
isPhoneOpen = true
if gameplay_taxi.isTaxiJobActive() then
guihooks.trigger('ChangeState', {state = 'phone-taxi'})
else
guihooks.trigger('ChangeState', {state = 'phone-main'})
end
end
endAuto-Close on Movement
The phone polls every 2.5 seconds. If speed exceeds 5 mph while open (and not in a cab), it auto-closes:
local function onUpdate(dt)
updateTimer = updateTimer + dt
if updateTimer > updateInterval then
updateTimer = 0
if isPhoneOpen then
local playerSpeed = math.abs(be:getObjectVelocityXYZ(be:getPlayerVehicleID(0))) * speedUnit
if playerSpeed > 5 then
isPhoneOpen = false
guihooks.trigger('closePhone')
end
end
end
endUI Events
| Event | Data | Direction | Purpose |
|---|---|---|---|
ChangeState | {state = 'phone-main'} | GE → UI | Open phone main screen |
ChangeState | {state = 'phone-taxi'} | GE → UI | Open phone taxi mode |
closePhone | nil | GE → UI | Close phone overlay |
phone_time_update | string (e.g. "3:45 PM") | GE → UI | Update time display |
PhoneMinimapData | {nodes, terrainTiles} | GE → UI | Initial minimap data |
PhoneMinimapUpdate | {controlID, objects, ...} | GE → UI | Per-frame minimap positions |
Time Display
Extension: ui/phone/time.lua
Formats the in-game time-of-day into 12-hour clock strings and pushes updates to the UI.
local function formatTime(time)
local total_minutes = time * 1440 -- time is 0..1 (fraction of day)
local hours = math.floor(total_minutes / 60)
local minutes = math.floor(total_minutes % 60)
local period = hours < 12 and "PM" or "AM"
local twelve_hour = hours % 12
twelve_hour = twelve_hour == 0 and 12 or twelve_hour
return string.format("%d:%02d %s", twelve_hour, minutes, period)
end| Function | Description |
|---|---|
onUpdate() | Checks scenetree.tod.time, triggers phone_time_update on change |
onExtensionLoaded() | Sends initial time value |
clearTime() | Resets cached time (used on phone close) |
Key detail: scenetree.tod.time is a float 0–1 representing fraction of a 24-hour day.
Phone Minimap
Extension: ui/phone/minimap.lua
Provides road-network data and per-frame vehicle positions to the phone's minimap view (separate from the HUD minimap app).
Data Flow
requestPhoneMap() → PhoneMinimapData (once, on load)
onGuiUpdate() → PhoneMinimapUpdate (every frame)Road Network Data (PhoneMinimapData)
{
nodes = {
[nodeId] = {
pos = {x, y}, -- 2D world position
radius = number,
links = {
[targetNodeId] = {
drivability = 0..1,
oneWay = bool
}
}
}
},
terrainTiles = {
{
size = {w, h}, -- world block size
offset = {x, y}, -- terrain position
file = "path/to/minimap.png"
}
}
}Per-Frame Update (PhoneMinimapUpdate)
{
controlID = vehicleId,
isFreeCam = bool,
camPosition = {x = num, y = num},
camRotationZ = degrees,
objects = {
[vehId] = {
posX = number, -- note: negated X
posY = number,
rot = degrees,
speed = number,
isPlayer = bool
}
}
}Lifecycle Hooks
| Hook | Behavior |
|---|---|
onExtensionLoaded | Phone resets isPhoneOpen = false |
onUIPlayStateChanged | Closes phone on play state change |
onUpdate(dt) | Speed-check polling every 2.5s |
Quick Reference
-- Toggle the phone
gameplay_phone.togglePhone()
-- Check if phone is open
if gameplay_phone.isPhoneOpen() then ... end
-- Get formatted in-game time
-- (internally: scenetree.tod.time → 12hr string)
-- Request minimap data refresh
ui_phone_minimap.requestPhoneMap()
-- UI listens for:
-- 'ChangeState' {state='phone-main'|'phone-taxi'}
-- 'closePhone'
-- 'phone_time_update' → time string
-- 'PhoneMinimapData' → road network + terrain
-- 'PhoneMinimapUpdate' → vehicle positions each frameCommon Patterns
Custom Phone State
-- Open phone to a custom state
guihooks.trigger('ChangeState', {state = 'phone-myapp'})Speed Gate Pattern
-- The phone uses mph internally (speedUnit = 2.2369362921)
local speedUnit = 2.2369362921
local speed = math.abs(be:getObjectVelocityXYZ(be:getPlayerVehicleID(0))) * speedUnit
if speed <= 5 then
-- Safe to open phone
endSee Also
- Apps System - HUD minimap app and custom app lifecycle
- GUIHooks - UI communication bridge
- Communication Guide - Cross-context patterns
Documentation Tag Index
Complete reference for all documentation tags across GE and VE contexts — use these tags to find relevant files via grep or IDE search.
Angular Overlay Pattern (Mod UI)
How to create a full-screen overlay UI (splash screen, popup, modal) from a mod that renders on top of everything in BeamNG's CEF UI.