diff --git a/client/cursor.lua b/client/cursor.lua new file mode 100644 index 0000000..7f86cd8 --- /dev/null +++ b/client/cursor.lua @@ -0,0 +1,85 @@ +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/client/dui.lua b/client/dui.lua new file mode 100644 index 0000000..6c97281 --- /dev/null +++ b/client/dui.lua @@ -0,0 +1,26 @@ +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 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 \ No newline at end of file diff --git a/client/main.lua b/client/main.lua new file mode 100644 index 0000000..220059c --- /dev/null +++ b/client/main.lua @@ -0,0 +1,223 @@ +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() + waitForLoad = false +end) + +AddEventHandler('onResourceStop', function(name) + if name == GetCurrentResourceName() then + HideScreen() + for i=1, #Locations do + DeleteEntity(Locations[i].obj) + end + end +end) \ No newline at end of file diff --git a/client/tv.lua b/client/tv.lua new file mode 100644 index 0000000..b599e26 --- /dev/null +++ b/client/tv.lua @@ -0,0 +1,226 @@ +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) \ No newline at end of file diff --git a/client/utils.lua b/client/utils.lua new file mode 100644 index 0000000..5cb43a9 --- /dev/null +++ b/client/utils.lua @@ -0,0 +1,32 @@ +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 \ No newline at end of file diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..138aab2 --- /dev/null +++ b/config.lua @@ -0,0 +1,40 @@ +Config = {} + +Config.Models = { -- Any TV Models used on the map or in locations must be defined here. + [`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) + } +} + +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 = "Pickle Mods", url = "twitch.tv/picklemods"}, +} + +Config.BannedWords = { + "google", +} + +Config.Events = { -- Events for approving broadcasts / interactions (due to popular demand). + ScreenInteract = function(source, data, key, value, cb) -- cb() to approve. + for i=1, #Config.BannedWords do + if string.find(value.url, Config.BannedWords) then + return + end + end + cb() + end, + Broadcast = function(source, data, cb) -- cb() to approve. + cb() + end, +} \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..2d77d9c --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,33 @@ +fx_version "cerulean" +game "gta5" +author "Pickle Mods#0001" + +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/utils.lua", + "client/tv.lua", + "client/dui.lua", + "client/main.lua", +} + +server_scripts { + "server/*.lua" +} + +lua54 'yes' \ No newline at end of file diff --git a/html/VCR_OSD_MONO_1.001.ttf b/html/VCR_OSD_MONO_1.001.ttf new file mode 100644 index 0000000..dcca687 Binary files /dev/null and b/html/VCR_OSD_MONO_1.001.ttf differ diff --git a/html/blank.html b/html/blank.html new file mode 100644 index 0000000..c244d68 --- /dev/null +++ b/html/blank.html @@ -0,0 +1,12 @@ + + +
+ + + +