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 @@ + + + + + + + Document + + + + + \ No newline at end of file diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..407f232 --- /dev/null +++ b/html/index.html @@ -0,0 +1,27 @@ + + + + + + + Document + + + + + + + + +
+
+
+
NO SIGNAL
+
+
+ +
+ + + + \ No newline at end of file diff --git a/html/main.js b/html/main.js new file mode 100644 index 0000000..1bf7e86 --- /dev/null +++ b/html/main.js @@ -0,0 +1,144 @@ +var player; +var playerData; +$(document).ready(function() { + $.post("https://ptelevision/pageLoaded", JSON.stringify({})) +}) + +function GetURLID(link) { + if (link == null) return; + let url = link.toString(); + var regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; + var 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) { + var 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) { + var url = video_data.url; + var channel = video_data.channel; + var 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(); + } + else if (event.data == YT.PlayerState.PAUSED) { + + } + } + } + }); + } + 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.setVolume(volume); + player.unMute(); + } + } +} + +function ShowNotification(channel, data) { + $("#tv-container").addClass("notify") + $("#tv-container div").addClass("notify") + var 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() +}) \ No newline at end of file diff --git a/html/style.css b/html/style.css new file mode 100644 index 0000000..d93ed21 --- /dev/null +++ b/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/main.lua b/server/main.lua new file mode 100644 index 0000000..ee7f27b --- /dev/null +++ b/server/main.lua @@ -0,0 +1,88 @@ +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/shared/main.lua b/shared/main.lua new file mode 100644 index 0000000..c5b855b --- /dev/null +++ b/shared/main.lua @@ -0,0 +1,25 @@ +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) \ No newline at end of file