Files
Elite-Gaming-FiveM/resources/radio/client/screen.lua
T
2026-03-28 17:31:40 -07:00

408 lines
16 KiB
Lua

-- ┌──────────────────────────────────────────────────────────────┐
-- │ Tommy's Radio - 3D Vehicle Radio Screen │
-- │ Renders the AFX-1500 screen on the vehicle radio prop │
-- │ using AddReplaceTexture to swap the prop's screen material │
-- │ with a DUI browser. Zero jitter — the DUI content renders │
-- │ as part of the 3D model geometry itself. │
-- │ Active only when the player owns a vehicle that has an │
-- │ attached radio prop (see positions.lua). │
-- │ Documentation: https://docs.timmygstudios.com/docs/tommys-radio │
-- └──────────────────────────────────────────────────────────────┘
-- Server has nothing to do here
if IsDuplicityVersion() then return end
-- ══════════════════════════════════════════════════════════════════
-- CONFIGURATION
-- ══════════════════════════════════════════════════════════════════
-- The original texture dictionary and texture name on the AFX1500 prop model.
-- AddReplaceTexture swaps this material with the DUI content at runtime.
local ORIG_TXD = "afx1500"
local ORIG_TEX = "C_Screen"
local DUI_RES_W = 480 -- DUI browser resolution (width)
local DUI_RES_H = 240 -- DUI browser resolution (height)
-- LED texture names on the AFX1500 prop and their lit colors (RGBA 0-255).
local LED_TEXTURES = {
power = { tex = "C_Red", onR = 255, onG = 0, onB = 0 },
connected = { tex = "C_Green", onR = 0, onG = 255, onB = 0 },
transmit = { tex = "C_Orange", onR = 255, onG = 153, onB = 0 },
}
-- Dim color when LED is off
local LED_OFF_R, LED_OFF_G, LED_OFF_B = 20, 20, 20
-- ══════════════════════════════════════════════════════════════════
-- DUI STATE
-- ══════════════════════════════════════════════════════════════════
local screenDui = nil
local screenTxdName = "radio_scr_txd"
local screenTexName = "radio_scr_tex"
local textureActive = false
local screenActive = false
local screenVehicle = nil
local screenProp = nil
-- LED runtime texture state: { [key] = { txdName, texName, currentlyOn } }
local ledState = {}
-- Cache of the last message sent for each action type, so we can
-- replay the full display state when the DUI is first created.
local lastScreenState = {}
-- ══════════════════════════════════════════════════════════════════
-- LED TEXTURE HELPERS
-- Creates a 4x4 solid-color runtime texture for each LED and
-- uses AddReplaceTexture to swap the prop's LED materials.
-- ══════════════════════════════════════════════════════════════════
--- Fills a runtime texture with a solid color by setting every pixel.
local function fillTexture(tex, w, h, r, g, b, a)
for y = 0, h - 1 do
for x = 0, w - 1 do
SetRuntimeTexturePixel(tex, x, y, r, g, b, a)
end
end
CommitRuntimeTexture(tex)
end
--- Creates the runtime textures for all LEDs and sets them to the off color.
local function createLedTextures()
for key, cfg in pairs(LED_TEXTURES) do
local txdName = "radio_led_" .. key .. "_txd"
local texName = "radio_led_" .. key .. "_tex"
local txd = CreateRuntimeTxd(txdName)
local tex = CreateRuntimeTexture(txd, texName, 4, 4)
fillTexture(tex, 4, 4, LED_OFF_R, LED_OFF_G, LED_OFF_B, 255)
AddReplaceTexture(ORIG_TXD, cfg.tex, txdName, texName)
ledState[key] = {
txdName = txdName,
texName = texName,
texHandle = tex,
currentlyOn = false,
}
end
end
--- Removes all LED texture replacements and clears state.
local function destroyLedTextures()
for key, st in pairs(ledState) do
local cfg = LED_TEXTURES[key]
if cfg then
RemoveReplaceTexture(ORIG_TXD, cfg.tex)
end
end
ledState = {}
end
--- Sets a single LED to its on or off color.
local function setLedColor(key, on)
local st = ledState[key]
local cfg = LED_TEXTURES[key]
if not st or not cfg then return end
if st.currentlyOn == on then return end -- no change
if on then
fillTexture(st.texHandle, 4, 4, cfg.onR, cfg.onG, cfg.onB, 255)
else
fillTexture(st.texHandle, 4, 4, LED_OFF_R, LED_OFF_G, LED_OFF_B, 255)
end
st.currentlyOn = on
end
-- ══════════════════════════════════════════════════════════════════
-- DUI LIFECYCLE
-- ══════════════════════════════════════════════════════════════════
local function createScreenDui()
if screenDui then return end
local resName = GetCurrentResourceName()
local url = "https://cfx-nui-" .. resName .. "/client/screen.html"
screenDui = CreateDui(url, DUI_RES_W, DUI_RES_H)
local handle = GetDuiHandle(screenDui)
local txd = CreateRuntimeTxd(screenTxdName)
CreateRuntimeTextureFromDuiHandle(txd, screenTexName, handle)
-- Swap the prop's screen texture with the DUI content
AddReplaceTexture(ORIG_TXD, ORIG_TEX, screenTxdName, screenTexName)
textureActive = true
-- Create LED textures and sync to cached state
createLedTextures()
for key, _ in pairs(LED_TEXTURES) do
local cached = lastScreenState['setLED_' .. key]
if cached and cached.mode then
setLedColor(key, true)
end
end
-- Give the browser a moment to load, then replay cached state.
-- Skip 'alert' — alerts are transient (3s auto-clear) and should
-- not be replayed on screen activation or power-on.
Citizen.SetTimeout(300, function()
if screenDui then
for key, msg in pairs(lastScreenState) do
if key ~= 'alert' then
SendDuiMessage(screenDui, json.encode(msg))
end
end
end
end)
end
local function destroyScreenDui()
destroyLedTextures()
if textureActive then
RemoveReplaceTexture(ORIG_TXD, ORIG_TEX)
textureActive = false
end
if screenDui then
DestroyDui(screenDui)
screenDui = nil
end
end
--- Sends a JSON message to the DUI browser (no-op if DUI is inactive).
local function sendToScreen(msg)
if screenDui then
SendDuiMessage(screenDui, json.encode(msg))
end
end
-- ══════════════════════════════════════════════════════════════════
-- DISPLAY FUNCTION WRAPPING
-- Intercepts the global display functions defined in shared.lua
-- to mirror every NUI update into the 3D DUI screen.
-- Cached so the full state can be replayed on DUI creation.
-- ══════════════════════════════════════════════════════════════════
-- dispUpdate — main display text, zone, channel
local _origDispUpdate = dispUpdate
function dispUpdate(btn01, btn02, btn03, btn04, btn05, ln01, ln02, zone, channel)
_origDispUpdate(btn01, btn02, btn03, btn04, btn05, ln01, ln02, zone, channel)
local msg = {
action = 'dispUpdate',
display = {
btn01 = btn01, btn02 = btn02, btn03 = btn03,
btn04 = btn04, btn05 = btn05,
ln01 = ln01, ln02 = ln02,
zone = zone, channel = channel,
}
}
lastScreenState['dispUpdate'] = msg
sendToScreen(msg)
end
-- setBatt
local _origSetBatt = setBatt
function setBatt(status)
_origSetBatt(status)
local msg = { action = 'setBatt', status = status }
lastScreenState['setBatt'] = msg
sendToScreen(msg)
end
-- setSignal
local _origSetSignal = setSignal
function setSignal(status)
_origSetSignal(status)
local msg = { action = 'setSignal', status = status }
lastScreenState['setSignal'] = msg
sendToScreen(msg)
end
-- setGPS
local _origSetGPS = setGPS
function setGPS(status)
_origSetGPS(status)
local msg = { action = 'setGPS', status = status }
lastScreenState['setGPS'] = msg
sendToScreen(msg)
end
-- setTrunk
local _origSetTrunk = setTrunk
function setTrunk(status)
_origSetTrunk(status)
local msg = { action = 'setTrunk', status = status }
lastScreenState['setTrunk'] = msg
sendToScreen(msg)
end
-- setScan
local _origSetScan = setScan
function setScan(status)
_origSetScan(status)
local msg = { action = 'setScan', status = status }
lastScreenState['setScan'] = msg
sendToScreen(msg)
end
-- setWarn
local _origSetWarn = setWarn
function setWarn(status)
_origSetWarn(status)
local msg = { action = 'setWarn', status = status }
lastScreenState['setWarn'] = msg
sendToScreen(msg)
end
-- setLED — also drives the 3D prop LED textures
local _origSetLED = setLED
function setLED(key, state)
_origSetLED(key, state)
local msg = { action = 'setLED', key = key, mode = state }
lastScreenState['setLED_' .. tostring(key)] = msg
sendToScreen(msg)
-- Update the prop's LED material color
setLedColor(key, state and true or false)
end
-- updateTime — recompute the time string (mirrors shared.lua logic)
local _origUpdateTime = updateTime
function updateTime()
_origUpdateTime()
local msg
if RadioState and RadioState.power then
local hours = GetClockHours()
local minutes = GetClockMinutes()
local period = "AM"
if hours >= 12 then
period = "PM"
if hours > 12 then hours = hours - 12 end
elseif hours == 0 then
hours = 12
end
msg = { action = 'setTime', time = string.format("%02d:%02d %s", hours, minutes, period) }
else
msg = { action = 'setTime', time = "" }
end
lastScreenState['setTime'] = msg
sendToScreen(msg)
end
-- setTheme — sync dark/light mode to the 3D screen
-- resolveTheme is local in shared.lua, so we inline the Auto logic here.
local _origSetTheme = setTheme
function setTheme(theme)
_origSetTheme(theme)
local resolved = theme
if theme == "Auto" then
local h = GetClockHours()
resolved = (h >= 20 or h < 6) and "Dark" or "Light"
end
local msg = { action = 'setTheme', theme = resolved }
lastScreenState['setTheme'] = msg
sendToScreen(msg)
end
-- setRadioConfig — forward model name so the DUI can load useAlertLineTS
local _origSetRadioConfig = setRadioConfig
function setRadioConfig(model)
_origSetRadioConfig(model)
local msg = { action = 'setRadioConfig', model = model }
lastScreenState['setRadioConfig'] = msg
sendToScreen(msg)
end
-- showAlert
local _origShowAlert = showAlert
function showAlert(message, mode, color)
_origShowAlert(message, mode, color)
if not mode then mode = 'none' end
if not color and mode == 'none' then color = 'black' end
if not color then color = 'white' end
local msg = { action = 'alert', message = message, mode = mode, textColor = color }
lastScreenState['alert'] = msg
sendToScreen(msg)
end
-- ══════════════════════════════════════════════════════════════════
-- VEHICLE OWNERSHIP LIFECYCLE
-- Watches playerOwnedVehicle (defined in shared.lua) and
-- activates the screen when the owned vehicle has a radio prop.
-- ══════════════════════════════════════════════════════════════════
Citizen.CreateThread(function()
while true do
Citizen.Wait(1000)
local vehicle = playerOwnedVehicle
if vehicle and DoesEntityExist(vehicle) then
-- Check if this vehicle has an attached radio prop
local prop = getAttachedRadioProp and getAttachedRadioProp(vehicle) or nil
if prop and DoesEntityExist(prop) then
if not screenActive or screenVehicle ~= vehicle then
screenVehicle = vehicle
screenProp = prop
screenActive = true
createScreenDui()
end
elseif screenActive then
screenActive = false
screenVehicle = nil
screenProp = nil
destroyScreenDui()
end
elseif screenActive then
screenActive = false
screenVehicle = nil
screenProp = nil
destroyScreenDui()
end
end
end)
-- ══════════════════════════════════════════════════════════════════
-- EVENT HOOKS
-- Catch LED changes that bypass the global setLED() function.
-- shared.lua line 9914: radioClient:setLED sends directly to NUI.
-- ══════════════════════════════════════════════════════════════════
AddEventHandler("radioClient:setLED", function(mode)
-- This event doesn't include a key — it's a general LED mode toggle.
-- Forward to the DUI and sync all prop LEDs based on the mode value.
if type(mode) == "table" then
-- mode might be a table of { key = state } pairs
for key, state in pairs(mode) do
setLedColor(key, state and true or false)
local msg = { action = 'setLED', key = key, mode = state }
lastScreenState['setLED_' .. tostring(key)] = msg
sendToScreen(msg)
end
elseif type(mode) == "boolean" then
-- Boolean: toggle all LEDs on/off
for key, _ in pairs(LED_TEXTURES) do
setLedColor(key, mode)
end
end
end)
-- ══════════════════════════════════════════════════════════════════
-- CLEANUP
-- ══════════════════════════════════════════════════════════════════
AddEventHandler('onResourceStop', function(resourceName)
if GetCurrentResourceName() == resourceName then
destroyScreenDui()
end
end)