Initial Repository.

This commit is contained in:
Pickle
2022-10-30 04:05:51 -04:00
committed by GitHub
parent 5f12013bad
commit 23e16d606a
16 changed files with 868 additions and 0 deletions

View File

@@ -0,0 +1 @@
resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'

View File

@@ -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

158
ptelevision/client/main.lua Normal file
View File

@@ -0,0 +1,158 @@
DEFAULT_URL = "https://cfx-nui-ptelevision/html/index.html"
duiUrl = DEFAULT_URL
duiObj = nil
tvObj = nil
function getDuiURL()
return duiUrl
end
function GetVolume(dist, range)
local rem = (dist / range)
rem = rem > 1 and 1 or rem
return (100 - math.floor(rem * 100))
end
function setDuiURL(url)
duiUrl = url
SetDuiUrl(duiObj, duiUrl)
end
local scale = 0.13
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)
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()
local tvObj = NetToObj(data.net_id)
local status = Entity(tvObj).state['ptv_status']
local lstatus = Entity(tvObj).state['ptv_status_local']
if status then
Citizen.Wait(1000)
if status.type == "play" and lstatus then
if (status.channel and GlobalState.Channels[status.channel]) then
PlayVideo({url = GlobalState.Channels[status.channel].url, channel = status.channel})
elseif (status.url) then
PlayVideo({url = status.url, time = math.floor((GetGameTimer() - lstatus.start_time) / 1000)})
end
elseif (status.type == "browser") then
PlayBrowser({ url = status.url })
end
end
while duiObj do
if (tvObj and sfHandle ~= nil and HasScaleformMovieLoaded(sfHandle)) then
local pos = GetEntityCoords(tvObj)
local min, max = GetModelDimensions(GetEntityModel(tvObj))
local scale = 0.085
local offset = GetOffsetFromEntityInWorldCoords(tvObj, -1.02, -0.055, 1.04)
local hz = GetEntityHeading(tvObj)
DrawScaleformMovie_3dNonAdditive(sfHandle, offset.x, offset.y, offset.z, 0.0, -hz, 0.0, 2.0, 2.0, 2.0, scale * 1, scale * (9/16), 1, 2)
end
Citizen.Wait(0)
end
end)
Citizen.CreateThread(function()
local screen = GetClosestScreen()
local modelData = Config.Models[screen.model]
local coords = screen.coords
local range = modelData.Range
while duiObj do
local pcoords = GetEntityCoords(PlayerPedId())
local dist = #(coords - pcoords)
SendDuiMessage(duiObj, json.encode({
setVolume = true,
data = GetVolume(dist, range)
}))
Citizen.Wait(100)
end
end)
end
function HideScreen()
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, net_id = ObjToNet(entity)}
end
end
end
return (closest.entity and closest or nil)
end
Citizen.CreateThread(function()
Citizen.Wait(2000)
while true do
local wait = 2500
local data = GetClosestScreen()
if (data and not duiObj) then
ShowScreen(data)
elseif (not data and duiObj) then
HideScreen()
end
Citizen.Wait(wait)
end
end)
RegisterNUICallback("pageLoaded", function()
waitForLoad = false
end)
AddEventHandler('onResourceStop', function(name)
if name == GetCurrentResourceName() then
HideScreen()
end
end)

174
ptelevision/client/tv.lua Normal file
View File

@@ -0,0 +1,174 @@
function SetChannel(index)
TriggerServerEvent("ptelevision:event", GetClosestScreen().net_id, "ptv_status", {
type = "play",
channel = index,
})
end
function GetChannelList()
local channel_list = {}
local menu_list = {}
local current = 1
local screen = GetClosestScreen()
local ent = NetToObj(screen.net_id)
local status = Entity(ent).state['ptv_status']
local channel = nil
if (status) then
channel = status.channel
end
for index,value in pairs(GlobalState.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(GlobalState.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()
local input = lib.inputDialog('Web Browser', {'URL:'})
if not input then OpenTVMenu() end
TriggerServerEvent("ptelevision:event", GetClosestScreen().net_id, "ptv_status", {
type = "browser",
url = input[1]
})
OpenTVMenu()
end
function VideoMenu()
local input = lib.inputDialog('Video Player', {'URL:'})
if not input then OpenTVMenu() end
TriggerServerEvent("ptelevision:event", GetClosestScreen().net_id, "ptv_status", {
type = "play",
url = input[1]
})
OpenTVMenu()
end
function OpenTVMenu()
local screen = GetClosestScreen()
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 = '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
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
AddStateBagChangeHandler("ptv_status", nil, function(bagName, key, value, reserved, replicated)
local net_id = tonumber(bagName:gsub('entity:', ''), 10)
local ent = NetToObj(net_id)
local screen = GetClosestScreen()
if (screen and screen.net_id == net_id and DoesEntityExist(ent)) then
local event = value
if (event.type == "play") then
local data = { url = event.url }
if (event.channel) then
data = GlobalState.Channels[event.channel]
data.channel = event.channel
end
PlayVideo(data)
elseif (event.type == "browser") then
PlayBrowser({ url = event.url })
end
end
Entity(ent).state:set('ptv_status_local', {
start_time = GetGameTimer()
}, false)
end)
RegisterNetEvent("ptelevision:broadcast", function(index, data)
if getDuiURL() == DEFAULT_URL then
local screen = GetClosestScreen()
local tvObj = NetToObj(screen.net_id)
local status = Entity(tvObj).state['ptv_status']
if (status and status.channel == index and data == nil) then
ResetDisplay()
Citizen.Wait(10)
end
SendDuiMessage(duiObj, json.encode({
showNotification = true,
channel = index,
data = data
}))
end
end)
RegisterCommand('tv', function()
OpenTVMenu()
end)
RegisterCommand("broadcast", function(source, args, raw)
BroadcastMenu()
end)

View File

@@ -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

23
ptelevision/config.lua Normal file
View File

@@ -0,0 +1,23 @@
Config = {}
Config.Models = {
[`prop_tv_flat_01`] = {
Range = 20.0,
Scale = 0.085,
Offset = vector3(-1.02, -0.055, 1.04),
}
}
Config.Locations = {
{
Model = `prop_tv_flat_01`,
Position = vector4(144.3038, -1037.4647, 29.4173, 70.1882)
},
{
Model = `prop_tv_flat_01`,
Position = vector4(264.0882, -830.7057, 29.4569, 340.7550)
},
}
Config.Channels = { -- These channels are default channels and cannot be overriden.
{name = "Pickle Mods", url = "twitch.tv/picklemods"},
}

View File

@@ -0,0 +1,32 @@
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/main.lua",
}
server_scripts {
"server/*.lua"
}
lua54 'yes'

Binary file not shown.

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="background-color: transparent">
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="style.css">
<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
<script src="https://player.twitch.tv/js/embed/v1.js"></script>
<script src="https://www.youtube.com/iframe_api"> </script>
<script src="main.js"></script>
</head>
<body>
<!-- Add a placeholder for the Twitch embed -->
<div id="background">
</div>
<div id="tv-container">
<div>NO SIGNAL</div>
</div>
<div id="overlay"><span></span></div>
<div id="twitch-embed"></div>
<!-- Load the Twitch embed script -->
</body>
</html>

143
ptelevision/html/main.js Normal file
View File

@@ -0,0 +1,143 @@
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<span style="font-size: 18pt !important;"> </span>'
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) {
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()
})

View File

@@ -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);
}

View File

@@ -0,0 +1,79 @@
local Locations = {}
GlobalState.Channels = {}
GlobalState.Channels = DumpArray(Config.Channels)
RegisterNetEvent("ptelevision:event", function(net_id, key, value)
local ent = NetworkGetEntityFromNetworkId(net_id)
if (DoesEntityExist(ent)) then
Entity(ent).state:set(key, value, true)
else
Entity(ent).state:set(key, value, true)
end
end)
RegisterNetEvent("ptelevision:broadcast", function(data)
local _source = source
local broadcasts = GlobalState.Channels
if data then
for k,v in pairs(broadcasts) do
if (broadcasts[k].source == _source) then
return
end
end
local index = 1
while true do
if not (broadcasts[index]) then
broadcasts[index] = data
broadcasts[index].source = _source
TriggerClientEvent("ptelevision:broadcast", -1, index, broadcasts[index])
break
end
index = index + 1
Citizen.Wait(0)
end
else
for k,v in pairs(broadcasts) do
if (broadcasts[k].source == _source) then
broadcasts[k] = nil
TriggerClientEvent("ptelevision:broadcast", -1, k, broadcasts[k])
break
end
end
end
GlobalState.Channels = broadcasts
end)
Citizen.CreateThread(function()
Citizen.Wait(1000)
local locations = Config.Locations
for i=1, #locations do
local data = locations[i]
local obj = CreateObject(data.Model, data.Position.x, data.Position.y, data.Position.z, true)
SetEntityHeading(obj, data.Position.w)
table.insert(Locations, {data = data, obj = obj})
end
end)
AddEventHandler('onResourceStop', function(name)
if name == GetCurrentResourceName() then
for i=1, #Locations do
local data = Locations[i]
DeleteEntity(Locations[i].obj)
end
end
end)
AddEventHandler('playerDropped', function(reason)
local source = _source
local broadcasts = GlobalState.Channels
for k,v in pairs(broadcasts) do
if (broadcasts[k].source == _source) then
broadcasts[k] = nil
TriggerClientEvent("ptelevision:broadcast", -1, k, broadcasts[k])
break
end
end
GlobalState.Channels = broadcasts
end)

View File

@@ -0,0 +1,9 @@
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