From 1a1ba6c1d344c09b9226e010401a7fd0fa3ce028 Mon Sep 17 00:00:00 2001 From: bitpredator <67551273+bitpredator@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:46:11 +0200 Subject: [PATCH] feat: in-game TV support --- .../fxmanifest.lua | 4 + .../stream/generic_texture_renderer.gfx | Bin 0 -> 4830 bytes .../stream/generic_texture_renderer_2.gfx | Bin 0 -> 4830 bytes .../[esx_addons]/ptelevision/README.md | 27 ++ .../ptelevision/client/cursor.lua | 88 +++++++ .../[esx_addons]/ptelevision/client/dui.lua | 44 ++++ .../[esx_addons]/ptelevision/client/main.lua | 228 +++++++++++++++++ .../[esx_addons]/ptelevision/client/tv.lua | 240 ++++++++++++++++++ .../[esx_addons]/ptelevision/config.lua | 154 +++++++++++ .../[esx_addons]/ptelevision/fxmanifest.lua | 32 +++ .../ptelevision/html/VCR_OSD_MONO_1.001.ttf | Bin 0 -> 75864 bytes .../[esx_addons]/ptelevision/html/blank.html | 12 + .../[esx_addons]/ptelevision/html/index.html | 27 ++ .../[esx_addons]/ptelevision/html/main.js | 143 +++++++++++ .../[esx_addons]/ptelevision/html/style.css | 93 +++++++ .../[esx_addons]/ptelevision/server/main.lua | 87 +++++++ .../[esx_addons]/ptelevision/shared/main.lua | 31 +++ 17 files changed, 1210 insertions(+) create mode 100644 server-data/resources/[esx_addons]/generic_texture_renderer_gfx/fxmanifest.lua create mode 100644 server-data/resources/[esx_addons]/generic_texture_renderer_gfx/stream/generic_texture_renderer.gfx create mode 100644 server-data/resources/[esx_addons]/generic_texture_renderer_gfx/stream/generic_texture_renderer_2.gfx create mode 100644 server-data/resources/[esx_addons]/ptelevision/README.md create mode 100644 server-data/resources/[esx_addons]/ptelevision/client/cursor.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/client/dui.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/client/main.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/client/tv.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/config.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/fxmanifest.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/html/VCR_OSD_MONO_1.001.ttf create mode 100644 server-data/resources/[esx_addons]/ptelevision/html/blank.html create mode 100644 server-data/resources/[esx_addons]/ptelevision/html/index.html create mode 100644 server-data/resources/[esx_addons]/ptelevision/html/main.js create mode 100644 server-data/resources/[esx_addons]/ptelevision/html/style.css create mode 100644 server-data/resources/[esx_addons]/ptelevision/server/main.lua create mode 100644 server-data/resources/[esx_addons]/ptelevision/shared/main.lua diff --git a/server-data/resources/[esx_addons]/generic_texture_renderer_gfx/fxmanifest.lua b/server-data/resources/[esx_addons]/generic_texture_renderer_gfx/fxmanifest.lua new file mode 100644 index 000000000..242789006 --- /dev/null +++ b/server-data/resources/[esx_addons]/generic_texture_renderer_gfx/fxmanifest.lua @@ -0,0 +1,4 @@ +fx_version "cerulean" +game "gta5" +lua54 'yes' +version "1.0.2" \ No newline at end of file diff --git a/server-data/resources/[esx_addons]/generic_texture_renderer_gfx/stream/generic_texture_renderer.gfx b/server-data/resources/[esx_addons]/generic_texture_renderer_gfx/stream/generic_texture_renderer.gfx new file mode 100644 index 0000000000000000000000000000000000000000..8151eff5464afbb966da6f6ee9edd1459e931b74 GIT binary patch literal 4830 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 + + +## What is this? + +Basically, this serves as a free resource for servers that need a television script. + +With this resource, you will be able to do the following: + +- Watch Youtube & Twitch Videos / Streams. +- Broadcast your Youtube / Twitch Stream as a server-wide channel. +- Display & Browse the Web. +- Display Images and Videos (via direct link in the browser). + +## What do I need? + +You will need the following for this script to work. + +- [Ox Lib](https://github.com/overextended/ox_lib/releases) (works with any framework) + +- [Renderer](https://forum.cfx.re/t/release-generic-dui-2d-3d-renderer/131208) (works with any framework) + +If you want to make a fork, please obtain permission with a valid reason. + +## Need Support? + +Click here! diff --git a/server-data/resources/[esx_addons]/ptelevision/client/cursor.lua b/server-data/resources/[esx_addons]/ptelevision/client/cursor.lua new file mode 100644 index 000000000..cdf51cf3b --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/client/cursor.lua @@ -0,0 +1,88 @@ +local scale = 1.5 +local screenWidth = math.floor(1920 / scale) +local screenHeight = math.floor(1080 / scale) +shouldDraw = false + +function SetInteractScreen(bool) + if not shouldDraw and bool then + shouldDraw = bool + Citizen.CreateThread(function() + -- Create screen + local nX = 0 + local nY = 0 + + local w, h = screenWidth, screenHeight + + local minX, maxX = ((w - (w / 2)) / 2), (w - (w / 4)) + local totalX = minX - maxX + + local minY, maxY = ((h - (h / 2)) / 2), (h - (h / 4)) + local totalY = minY - maxY + + RequestTextureDictionary("fib_pc") + + -- Update controls while active + while shouldDraw do + nX = GetControlNormal(0, 239) * screenWidth + nY = GetControlNormal(0, 240) * screenHeight + DisableControlAction(0, 1, true) -- Disable looking horizontally + DisableControlAction(0, 2, true) -- Disable looking vertically + DisablePlayerFiring(PlayerPedId(), true) -- Disable weapon firing + DisableControlAction(0, 142, true) -- Disable aiming + DisableControlAction(0, 106, true) -- Disable in-game mouse controls + -- Update mouse position when changed + DrawSprite("ptelevision_b_dict", "ptelevision_b_txd", 0.5, 0.5, 0.5, 0.5, 0.0, 255, 255, 255, 255) + if nX ~= mX or nY ~= mY then + mX = nX + mY = nY + local duiX = -screenWidth * ((mX - minX) / totalX) + local duiY = -screenHeight * ((mY - minY) / totalY) + BlockWeaponWheelThisFrame() + if not (mX > 325) then + mX = 325 + end + if not (mX < 965) then + mX = 965 + end + if not (mY > 185) then + mY = 185 + end + if not (mY < 545) then + mY = 545 + end + SendDuiMouseMove(duiObj, math.floor(duiX), math.floor(duiY)) + end + DrawSprite("fib_pc", "arrow", mX / screenWidth, mY / screenHeight, 0.005, 0.01, 0.0, 255, 255, 255, 255) + + -- Send scroll and click events to dui + + if IsControlPressed(0, 177) then + SetInteractScreen(false) + OpenTVMenu() + end -- scroll up + if IsControlPressed(0, 172) then + SendDuiMouseWheel(duiObj, 10, 0) + end -- scroll up + if IsControlPressed(0, 173) then + SendDuiMouseWheel(duiObj, -10, 0) + end -- scroll down + + if IsDisabledControlJustPressed(0, 24) then + SendDuiMouseDown(duiObj, "left") + elseif IsDisabledControlJustReleased(0, 24) then + SendDuiMouseUp(duiObj, "left") + SendDuiMouseUp(duiObj, "right") + end + if IsDisabledControlJustPressed(0, 25) then + SendDuiMouseDown(duiObj, "right") + elseif IsDisabledControlJustReleased(0, 24) then + SendDuiMouseUp(duiObj, "right") + end + + Wait(0) + end + end) + else + shouldDraw = bool + end +end diff --git a/server-data/resources/[esx_addons]/ptelevision/client/dui.lua b/server-data/resources/[esx_addons]/ptelevision/client/dui.lua new file mode 100644 index 000000000..8a5dab7d7 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/client/dui.lua @@ -0,0 +1,44 @@ +function CreateNamedRenderTargetForModel(name, model) + local handle = 0 + if not IsNamedRendertargetRegistered(name) then + RegisterNamedRendertarget(name, 0) + end + if not IsNamedRendertargetLinked(model) then + LinkNamedRendertarget(model) + end + if IsNamedRendertargetRegistered(name) then + handle = GetNamedRendertargetRenderId(name) + end + return handle +end + +function RequestTextureDictionary(dict) + RequestStreamedTextureDict(dict) + while not HasStreamedTextureDictLoaded(dict) do + Wait(0) + end + return dict +end + +function LoadModel(model) + if not IsModelInCdimage(model) then + return + end + RequestModel(model) + while not HasModelLoaded(model) do + Wait(0) + end + return model +end + +function RenderScaleformTV(renderTarget, scaleform, entity) + SetTextRenderId(renderTarget) -- set render target + Set_2dLayer(4) + SetScriptGfxDrawBehindPausemenu(1) + --DrawRect(0.5, 0.5, 1.0, 0.5, 255, 0, 0, 255); -- WOAH! + local coords = GetEntityCoords(entity) + local rot = GetEntityRotation(entity) + DrawSprite("ptelevision_b_dict", "ptelevision_b_txd", 0.5, 0.5, 1.0, 1.0, 0.0, 255, 255, 255, 255) + SetTextRenderId(GetDefaultScriptRendertargetRenderId()) -- reset + SetScriptGfxDrawBehindPausemenu(0) +end diff --git a/server-data/resources/[esx_addons]/ptelevision/client/main.lua b/server-data/resources/[esx_addons]/ptelevision/client/main.lua new file mode 100644 index 000000000..75b124de0 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/client/main.lua @@ -0,0 +1,228 @@ +DEFAULT_URL = "https://cfx-nui-ptelevision/html/index.html" +duiUrl = DEFAULT_URL +duiObj = nil +tvObj = nil +volume = 0.5 +CURRENT_SCREEN = nil + +local Locations = Config.Locations + +function getDuiURL() + return duiUrl +end + +function SetVolume(coords, num) + volume = num + SetTelevisionLocal(coords, "volume", num) +end + +function GetVolume(dist, range) + if not volume then + return 0 + end + local rem = (dist / range) + rem = rem > volume and volume or rem + local _vol = math.floor((volume - rem) * 100) + return _vol +end + +function setDuiURL(url) + duiUrl = url + SetDuiUrl(duiObj, duiUrl) +end + +local sfName = "generic_texture_renderer" + +local width = 1280 +local height = 720 + +local sfHandle = nil +local txdHasBeenSet = false + +function loadScaleform(scaleform) + local scaleformHandle = RequestScaleformMovie(scaleform) + + while not HasScaleformMovieLoaded(scaleformHandle) do + scaleformHandle = RequestScaleformMovie(scaleform) + Citizen.Wait(0) + end + return scaleformHandle +end + +function ShowScreen(data) + CURRENT_SCREEN = data + sfHandle = loadScaleform(sfName) + runtimeTxd = "ptelevision_b_dict" + + local txd = CreateRuntimeTxd("ptelevision_b_dict") + duiObj = CreateDui(duiUrl, width, height) + local dui = GetDuiHandle(duiObj) + local tx = CreateRuntimeTextureFromDuiHandle(txd, "ptelevision_b_txd", dui) + + Citizen.Wait(10) + + PushScaleformMovieFunction(sfHandle, "SET_TEXTURE") + + PushScaleformMovieMethodParameterString("ptelevision_b_dict") + PushScaleformMovieMethodParameterString("ptelevision_b_txd") + + PushScaleformMovieFunctionParameterInt(0) + PushScaleformMovieFunctionParameterInt(0) + PushScaleformMovieFunctionParameterInt(width) + PushScaleformMovieFunctionParameterInt(height) + + PopScaleformMovieFunctionVoid() + Citizen.CreateThread(function() + TriggerServerEvent("ptelevision:requestSync", data.coords) + local tvObj = data.entity + local screenModel = Config.Models[data.model] + while duiObj do + if tvObj and sfHandle ~= nil and HasScaleformMovieLoaded(sfHandle) then + local pos = GetEntityCoords(tvObj) + local scale = screenModel.Scale + local offset = GetOffsetFromEntityInWorldCoords(tvObj, screenModel.Offset.x, screenModel.Offset.y, screenModel.Offset.z) + if screenModel.Target then + local id = CreateNamedRenderTargetForModel(screenModel.Target, data.model) + if id ~= -1 then + RenderScaleformTV(id, sfHandle, tvObj) + end + else + local hz = GetEntityHeading(tvObj) + DrawScaleformMovie_3dSolid(sfHandle, offset, 0.0, 0.0, -hz, 2.0, 2.0, 2.0, scale * 1, scale * (9 / 16), 2) + end + end + Citizen.Wait(0) + end + end) + Citizen.CreateThread(function() + local screen = CURRENT_SCREEN + local modelData = Config.Models[screen.model] + local coords = screen.coords + local range = modelData.Range + local _, lstatus = GetTelevisionLocal(coords) + if lstatus and lstatus.volume then + SetVolume(coords, lstatus.volume) + else + SetVolume(coords, modelData.DefaultVolume) + end + while duiObj do + local pcoords = GetEntityCoords(PlayerPedId()) + local dist = #(coords - pcoords) + SendDuiMessage( + duiObj, + json.encode({ + setVolume = true, + data = GetVolume(dist, range, volume), + }) + ) + Citizen.Wait(100) + end + end) +end + +function HideScreen() + CURRENT_SCREEN = nil + if duiObj then + DestroyDui(duiObj) + SetScaleformMovieAsNoLongerNeeded(sfHandle) + duiObj = nil + sfHandle = nil + end +end + +function GetClosestScreen() + local objPool = GetGamePool("CObject") + local closest = { dist = -1 } + local pcoords = GetEntityCoords(PlayerPedId()) + for i = 1, #objPool do + local entity = objPool[i] + local model = GetEntityModel(entity) + local data = Config.Models[model] + if data then + local coords = GetEntityCoords(entity) + local dist = #(pcoords - coords) + if (dist < closest.dist or closest.dist < 0) and dist < data.Range then + closest = { dist = dist, coords = coords, model = model, entity = entity } + end + end + end + return (closest.entity and closest or nil) +end + +Citizen.CreateThread(function() + Citizen.Wait(2000) + TriggerServerEvent("ptelevision:requestUpdate") + while true do + local wait = 2500 + local data = GetClosestScreen() + if data and not duiObj then + ShowScreen(data) + elseif (not data or #(v3(CURRENT_SCREEN.coords) - v3(data.coords)) > 0.01) and duiObj then + HideScreen() + end + Citizen.Wait(wait) + end +end) + +Citizen.CreateThread(function() + while true do + local wait = 2500 + for i = 1, #Locations do + local data = Locations[i] + local dist = #(GetEntityCoords(PlayerPedId()) - v3(data.Position)) + if not Locations[i].obj and dist < 20.0 then + LoadModel(data.Model) + Locations[i].obj = CreateObject(data.Model, data.Position.x, data.Position.y, data.Position.z) + SetEntityHeading(Locations[i].obj, data.Position.w) + FreezeEntityPosition(Locations[i].obj, true) + elseif Locations[i].obj and dist > 20.0 then + DeleteEntity(Locations[i].obj) + Locations[i].obj = nil + end + end + Citizen.Wait(wait) + end +end) + +RegisterNetEvent("ptelevision:requestUpdate", function(data) + Televisions = data.Televisions + Channels = data.Channels +end) + +RegisterNetEvent("ptelevision:requestSync", function(coords, data) + local tvObj = data.entity + + local _, status = GetTelevision(coords) + local screenModel = Config.Models[data.model] + if status and status["ptv_status"] then + local update_time = status.update_time + local status = status["ptv_status"] + Citizen.Wait(1000) + if status.type == "play" then + if status.channel and Channels[status.channel] then + PlayVideo({ url = Channels[status.channel].url, channel = status.channel }) + elseif status.url then + local time = math.floor(data.current_time - update_time) + PlayVideo({ url = status.url, time = time }) + end + elseif status.type == "browser" then + PlayBrowser({ url = status.url }) + end + end +end) + +RegisterNUICallback("pageLoaded", function(cb) + waitForLoad = false + if cb then + cb() + end +end) + +AddEventHandler("onResourceStop", function(name) + if name == GetCurrentResourceName() then + HideScreen() + for i = 1, #Locations do + DeleteEntity(Locations[i].obj) + end + end +end) diff --git a/server-data/resources/[esx_addons]/ptelevision/client/tv.lua b/server-data/resources/[esx_addons]/ptelevision/client/tv.lua new file mode 100644 index 000000000..0a52c02df --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/client/tv.lua @@ -0,0 +1,240 @@ +TelevisionsLocal = {} + +function SetChannel(index) + TriggerServerEvent("ptelevision:event", CURRENT_SCREEN, "ptv_status", { + type = "play", + channel = index, + }) +end + +function GetChannelList() + if not Channels then + return {} + end + local channel_list = {} + local menu_list = {} + local current = 1 + local screen = CURRENT_SCREEN + local ent = screen.entity + local _, status = GetTelevision(screen.coords) + local channel = nil + if status then + channel = status.channel + end + for index, value in pairs(Channels) do + table.insert(channel_list, { index = index, url = value.url }) + table.insert(menu_list, "Channel #" .. index .. " (" .. value.name .. ")") + if channel ~= nil and channel == index then + current = #channel_list + end + end + return { list = channel_list, display = menu_list, current = current } +end + +function BroadcastMenu() + local _source = GetPlayerServerId(PlayerId()) + for k, v in pairs(Channels) do + if v.source == _source then + TriggerServerEvent("ptelevision:broadcast", nil) + return + end + end + local input = lib.inputDialog("Live Broadcast", { "Channel Name:", "Broadcast URL:" }) + if input[1] and input[2] then + TriggerServerEvent("ptelevision:broadcast", { name = input[1], url = input[2] }) + end +end + +function WebBrowserMenu() + lib.hideMenu() + local input = lib.inputDialog("Web Browser", { "URL:" }) + + if input then + TriggerServerEvent("ptelevision:event", CURRENT_SCREEN, "ptv_status", { + type = "browser", + url = input[1], + }) + end + Citizen.Wait(300) + OpenTVMenu() +end + +function VideoMenu() + lib.hideMenu() + local input = lib.inputDialog("Video Player", { "URL:" }) + if input then + TriggerServerEvent("ptelevision:event", CURRENT_SCREEN, "ptv_status", { + type = "play", + url = input[1], + }) + end + Citizen.Wait(300) + OpenTVMenu() +end + +function VolumeMenu() + lib.hideMenu() + local input = lib.inputDialog("Volume", { "Set Volume (0-100):" }) + if tonumber(input[1]) then + local coords = CURRENT_SCREEN.coords + SetVolume(coords, tonumber(input[1]) / 100) + end + Citizen.Wait(300) + OpenTVMenu() +end + +function OpenTVMenu() + local screen = CURRENT_SCREEN + if not screen then + return + end + lib.hideMenu() + local ChannelList = GetChannelList() + lib.registerMenu({ + id = "ptelevision-menu", + title = "Television", + position = "top-right", + onSideScroll = function(selected, scrollIndex, args) + if selected == 3 then + SetChannel(ChannelList.list[scrollIndex].index) + end + end, + onSelected = function(selected, scrollIndex, args) end, + onClose = function(keyPressed) end, + options = { + { label = "Videos", description = "Play a video or stream on the screen." }, + { label = "Web Browser", description = "Access the web via your TV." }, + { label = "TV Channels", values = ChannelList.display, description = "Live TV Channels in San Andreas!", defaultIndex = ChannelList.current }, + { label = "Interact With Screen", description = "Allows you to control on-screen elements." }, + { label = "Set Volume", description = "Sets your TV's volume (For yourself)." }, + { label = "Close Menu", close = true }, + }, + }, function(selected, scrollIndex, args) + if selected == 1 then + VideoMenu() + elseif selected == 2 then + WebBrowserMenu() + elseif selected == 3 then + SetChannel(ChannelList.list[scrollIndex].index) + OpenTVMenu() + elseif selected == 4 then + SetInteractScreen(true) + elseif selected == 5 then + VolumeMenu() + end + end) + lib.showMenu("ptelevision-menu") +end + +function PlayBrowser(data) + while not IsDuiAvailable(duiObj) do + Wait(10) + end + setDuiURL(data.url) +end + +function PlayVideo(data) + while not IsDuiAvailable(duiObj) do + Wait(10) + end + if getDuiURL() ~= DEFAULT_URL then + waitForLoad = true + setDuiURL(DEFAULT_URL) + while waitForLoad do + Wait(10) + end + end + SendDuiMessage( + duiObj, + json.encode({ + setVideo = true, + data = data, + }) + ) +end + +function ResetDisplay() + setDuiURL(DEFAULT_URL) +end + +function GetTelevisionLocal(coords) + for k, v in pairs(TelevisionsLocal) do + if #(v3(v.coords) - v3(coords)) < 0.01 then + return k, v + end + end +end + +function SetTelevisionLocal(coords, key, value) + local index, data = GetTelevisionLocal(coords) + if index ~= nil then + if TelevisionsLocal[index] == nil then + TelevisionsLocal[index] = {} + end + TelevisionsLocal[index][key] = value + else + index = GetGameTimer() + while TelevisionsLocal[index] do + index = index + 1 + Citizen.Wait(0) + end + if TelevisionsLocal[index] == nil then + TelevisionsLocal[index] = {} + end + TelevisionsLocal[index][key] = value + end + TelevisionsLocal[index].coords = coords + return index +end + +RegisterNetEvent("ptelevision:event", function(data, index, key, value) + Televisions = data + local data = Televisions[index] + local screen = CURRENT_SCREEN + if screen and #(v3(screen.coords) - v3(data.coords)) < 0.001 then + local index, data = GetTelevision(screen.coords) + if index then + local event = value + if event.type == "play" then + local data = { url = event.url } + if event.channel then + data = Channels[event.channel] + data.channel = event.channel + end + PlayVideo(data) + elseif event.type == "browser" then + PlayBrowser({ url = event.url }) + end + end + end + SetTelevisionLocal(Televisions[index].coords, "start_time", GetGameTimer()) +end) + +RegisterNetEvent("ptelevision:broadcast", function(data, index) + Channels = data + if getDuiURL() == DEFAULT_URL then + local screen = CURRENT_SCREEN + local tvObj = screen.entity + local _, status = GetTelevision(screen.coords) + if status and status.channel == index and data[index] == nil then + ResetDisplay() + Citizen.Wait(10) + end + SendDuiMessage( + duiObj, + json.encode({ + showNotification = true, + channel = index, + data = data[index], + }) + ) + end +end) + +RegisterCommand("tv", function() + OpenTVMenu() +end) + +RegisterCommand("broadcast", function(source, args, raw) + BroadcastMenu() +end) diff --git a/server-data/resources/[esx_addons]/ptelevision/config.lua b/server-data/resources/[esx_addons]/ptelevision/config.lua new file mode 100644 index 000000000..97c36a96f --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/config.lua @@ -0,0 +1,154 @@ +Config = {} + +Config.Models = { -- Any TV Models used on the map or in locations must be defined here. + [`des_tvsmash_start`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_flatscreen_overlay`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_laptop_lester2`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_monitor_02`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_trev_tv_01`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_02`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_03_overlay`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_06`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_01`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_01_screen`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_02b`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_03`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_03b`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_michael`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_monitor_w_large`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_03`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, + [`prop_tv_flat_02`] = { + DefaultVolume = 0.5, + Range = 20.0, + Target = "tvscreen", -- Only use if prop has render-target name. + Scale = 0.085, + Offset = vector3(-1.02, -0.055, 1.04), + }, +} + +Config.Locations = { -- REMOVE ALL IF NOT USING ONESYNC, OR IT SHALL BREAK. + { + Model = `prop_tv_flat_01`, + Position = vector4(144.3038, -1037.4647, 29.4173, 70.81), + }, +} + +Config.Channels = { -- These channels are default channels and cannot be overriden. + { name = "Twitch", url = "twitch.tv/twitch" }, +} + +Config.BannedWords = { + "google", +} + +Config.Events = { -- Events for approving broadcasts / interactions (due to popular demand). + ScreenInteract = function(source, data, key, value, cb) -- cb() to approve. + if value.url then + for i = 1, #Config.BannedWords do + if string.find(value.url, Config.BannedWords[i]) then + return + end + end + end + cb() + end, + Broadcast = function(source, data, cb) -- cb() to approve. + cb() + end, +} diff --git a/server-data/resources/[esx_addons]/ptelevision/fxmanifest.lua b/server-data/resources/[esx_addons]/ptelevision/fxmanifest.lua new file mode 100644 index 000000000..8bfc3d884 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/fxmanifest.lua @@ -0,0 +1,32 @@ +fx_version("cerulean") +game("gta5") +author("Pickle Mods#0001") +version("v1.2.5") +ui_page("html/blank.html") + +files({ + "html/blank.html", + "html/index.html", + "html/style.css", + "html/main.js", + "html/VCR_OSD_MONO_1.001.ttf", +}) + +shared_scripts({ + "@ox_lib/init.lua", + "config.lua", + "shared/*.lua", +}) + +client_scripts({ + "client/cursor.lua", + "client/tv.lua", + "client/dui.lua", + "client/main.lua", +}) + +server_scripts({ + "server/*.lua", +}) + +lua54("yes") diff --git a/server-data/resources/[esx_addons]/ptelevision/html/VCR_OSD_MONO_1.001.ttf b/server-data/resources/[esx_addons]/ptelevision/html/VCR_OSD_MONO_1.001.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dcca687a434d5c7b6a3027e65e0b7d8728b25c71 GIT binary patch literal 75864 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 literal 0 HcmV?d00001 diff --git a/server-data/resources/[esx_addons]/ptelevision/html/blank.html b/server-data/resources/[esx_addons]/ptelevision/html/blank.html new file mode 100644 index 000000000..4f830fa74 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/html/blank.html @@ -0,0 +1,12 @@ + + + + + + + Document + + + + + \ No newline at end of file diff --git a/server-data/resources/[esx_addons]/ptelevision/html/index.html b/server-data/resources/[esx_addons]/ptelevision/html/index.html new file mode 100644 index 000000000..4cd66f808 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/html/index.html @@ -0,0 +1,27 @@ + + + + + + + Document + + + + + + + + +
+
+
+
NO SIGNAL
+
+
+ +
+ + + + \ No newline at end of file diff --git a/server-data/resources/[esx_addons]/ptelevision/html/main.js b/server-data/resources/[esx_addons]/ptelevision/html/main.js new file mode 100644 index 000000000..33ef02108 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/html/main.js @@ -0,0 +1,143 @@ +let player; +let playerData; +$(document).ready(function() { + $.post('https://ptelevision/pageLoaded', JSON.stringify({})); +}); + +function GetURLID(link) { + if (link == null) return; + const url = link.toString(); + const regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + const match = url.match(regExp); + if (match && match[2].length == 11) { + return { type: 'youtube', id: match[2] }; + } + else if (url.split('twitch.tv/').length > 1) { + + return { type: 'twitch', id: url.split('twitch.tv/')[1] }; + } +} + +function ChannelDisplay(channel, channelFound) { + if (channel) { + let temp = 'CH '; + if (channel > 9) { + temp += channel; + } + else { + temp += ('0' + channel); + } + $('#overlay span').show(); + $('#overlay span').html(temp); + } + else { + $('#overlay span').show(); + $('#overlay span').html(''); + } + if (channelFound) { + $('#tv-container').hide(); + } + else { + $('#tv-container').show(); + } +} + +function SetVideo(video_data) { + const url = video_data.url; + const channel = video_data.channel; + const data = GetURLID(url); + + playerData = data; + if (player) { + player.destroy(); + player = null; + } + if (data) { + if (data.type == 'youtube') { + player = new YT.Player('twitch-embed', { + height: '100%', + width: '100%', + videoId: data.id, + playerVars: { + 'playsinline': 1, + }, + events: { + 'onReady': function(event) { + event.target.playVideo(); + event.target.seekTo(video_data.time); + }, + 'onStateChange': function(event) { + if (event.data == YT.PlayerState.PLAYING) { + event.target.unMute(); + } + // eslint-disable-next-line no-inline-comments + else if (event.data == YT.PlayerState.PAUSED) { /* empty */ } + }, + }, + }); + } + else if (data.type == 'twitch') { + player = new Twitch.Player('twitch-embed', { + width: '100%', + height: '100%', + channel: data.id, + volume: 1.0, + }); + player.addEventListener(Twitch.Embed.VIDEO_READY, function() { + player.setMuted(false); + }); + } + + $('#overlay span').hide(); + $('#tv-container').hide(); + } + if (channel) { + ChannelDisplay(channel, url); + } +} + +function SetVolume(volume) { + + if (player && playerData && player.setVolume) { + if (playerData.type == 'twitch') { + player.setMuted(false); + player.setVolume(volume / 100.0); + } + else if (playerData.type == 'youtube') { + player.unMute(); + player.setVolume(volume); + } + } +} + +function ShowNotification(channel, data) { + $('#tv-container').addClass('notify'); + $('#tv-container div').addClass('notify'); + const display = $('#tv-container').is(':visible'); + $('#tv-container').show(); + $('#tv-container div').html('Channel #' + channel + (data ? (' (' + data.name + ')') : '') + ' is now ' + (data ? 'live!' : 'offline.')); + + setTimeout(function() { + $('#tv-container').removeClass('notify'); + $('#tv-container div').removeClass('notify'); + $('#tv-container div').html('NO SIGNAL'); + if (!display) { + $('#tv-container').hide(); + } + }, 3500); +} + +window.addEventListener('message', function(ev) { + if (ev.data.setVideo) { + SetVideo(ev.data.data); + } + else if (ev.data.setVolume) { + SetVolume(ev.data.data); + } + else if (ev.data.showNotification) { + ShowNotification(ev.data.channel, ev.data.data); + } +}); +$(document).ready(function() { + ChannelDisplay(); +}); diff --git a/server-data/resources/[esx_addons]/ptelevision/html/style.css b/server-data/resources/[esx_addons]/ptelevision/html/style.css new file mode 100644 index 000000000..e2db007e3 --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/html/style.css @@ -0,0 +1,93 @@ +@font-face { + font-family: 'BodyCam'; + src: url('VCR_OSD_MONO_1.001.ttf') format('truetype') /* Safari, Android, iOS */ +} + +* { + font-family: BodyCam; + color:white; +} + +#twitch-embed { + position: absolute; + width: 100%; + height: 100%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: -1; +} + +#overlay { + position: absolute; + top: 10px; + right: 10px; + z-index: 1; + font-size: 48pt !important; +} + +#background { + display: flex; + position: absolute; + top: 0; + left: 0; + width: 100%; + min-height: 100%; + background-color: rgb(42, 42, 42); + justify-content: center; + align-items: center; + flex-wrap: wrap; + z-index: -2; +} + +#tv-container { + display: flex; + position: absolute; + top: 0; + left: 0; + width: 100%; + min-height: 100%; + background-color: transparent; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} + +#tv-container.notify { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + min-height: 100%; +} + +#tv-container > div { + font-size: 24pt; + user-select: none; +} + +#tv-container > div.notify { + font-size: 24pt; + user-select: none; + width: -webkit-fill-available; + text-align: center; + padding: 25px; + background-color: rgba(0, 0, 0, 0.3); +} + +/* width */ +::-webkit-scrollbar { + width: 10px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: rgb(42, 42, 42); +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: rgb(66, 66, 66); +} \ No newline at end of file diff --git a/server-data/resources/[esx_addons]/ptelevision/server/main.lua b/server-data/resources/[esx_addons]/ptelevision/server/main.lua new file mode 100644 index 000000000..b39560fcc --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/server/main.lua @@ -0,0 +1,87 @@ +local Locations = {} + +function SetTelevision(coords, key, value, update) + local index, data = GetTelevision(coords) + if index ~= nil then + if Televisions[index] == nil then + Televisions[index] = {} + end + Televisions[index][key] = value + else + index = os.time() + while Televisions[index] do + index = index + 1 + Citizen.Wait(0) + end + if Televisions[index] == nil then + Televisions[index] = {} + end + Televisions[index][key] = value + end + Televisions[index].coords = coords + Televisions[index].update_time = os.time() + if update then + TriggerClientEvent("ptelevision:event", -1, Televisions, index, key, value) + end + return index +end + +function SetChannel(source, data) + if data then + for k, v in pairs(Channels) do + if Channels[k].source == source then + return + end + end + local index = 1 + while Channels[index] do + index = index + 1 + Citizen.Wait(0) + end + Channels[index] = data + Channels[index].source = source + TriggerClientEvent("ptelevision:broadcast", -1, Channels, index) + return + else + for k, v in pairs(Channels) do + if Channels[k].source == source then + Channels[k] = nil + TriggerClientEvent("ptelevision:broadcast", -1, Channels, k) + return + end + end + end +end + +RegisterNetEvent("ptelevision:requestSync", function(coords) + local _source = source + local index, data = GetTelevision(coords) + TriggerClientEvent("ptelevision:requestSync", _source, coords, { current_time = os.time() }) +end) + +RegisterNetEvent("ptelevision:event", function(data, key, value) + local _source = source + Config.Events.ScreenInteract(_source, data, key, value, function() + SetTelevision(data.coords, key, value, true) + end) +end) + +RegisterNetEvent("ptelevision:broadcast", function(data) + local _source = source + Config.Events.Broadcast(_source, data, function() + SetChannel(_source, data) + end) +end) + +RegisterNetEvent("ptelevision:requestUpdate", function() + local _source = source + TriggerClientEvent("ptelevision:requestUpdate", _source, { + Televisions = Televisions, + Channels = Channels, + }) +end) + +AddEventHandler("playerDropped", function(reason) + local _source = source + SetChannel(_source, nil) +end) diff --git a/server-data/resources/[esx_addons]/ptelevision/shared/main.lua b/server-data/resources/[esx_addons]/ptelevision/shared/main.lua new file mode 100644 index 000000000..8ce8df89a --- /dev/null +++ b/server-data/resources/[esx_addons]/ptelevision/shared/main.lua @@ -0,0 +1,31 @@ +Televisions = {} + +function v3(coord) + return vector3(coord.x, coord.y, coord.z), coord.w +end + +function DumpArray(obj, seen) + if type(obj) ~= "table" then + return obj + end + if seen and seen[obj] then + return seen[obj] + end + local s = seen or {} + local res = setmetatable({}, getmetatable(obj)) + s[obj] = res + for k, v in pairs(obj) do + res[DumpArray(k, s)] = DumpArray(v, s) + end + return res +end + +function GetTelevision(coords) + for k, v in pairs(Televisions) do + if #(v3(v.coords) - v3(coords)) < 0.01 then + return k, v + end + end +end + +Channels = DumpArray(Config.Channels)