This commit is contained in:
Nordi98 2025-08-12 20:30:24 +02:00
parent 57069485d5
commit 8d5055a73d
1412 changed files with 5583 additions and 0 deletions

View file

@ -0,0 +1,229 @@
local SPEED_LIMITS_RAW = LoadResourceFile(GetCurrentResourceName(), "/speedlimits.json")
local speedLimits = json.decode(SPEED_LIMITS_RAW)

HIST = { }
HIST.history = { }
HIST.loggedHistory = { }

local cfg = cfg
local savePrefix = 'prolaser4_'
local pendingChanges = false
local selfTestTimestamp = nil
local waitingForServer = false
local lastLoggedTarget

-- local function forward declarations
local GetTimeString, PadTime, CorrectHour
-- [[COMMANDS]]
-- CLEAR SAVED DATA / KVPS
RegisterCommand('lidarwipe', function(source, args)
DeleteResourceKvp(savePrefix .. 'history')
HUD:ShowNotification("~g~Success~s~: wiped local save data. Please restart for changes to take effect.")
HUD:ResizeOnScreenDisplay(true)
end)
TriggerEvent('chat:addSuggestion', '/lidarwipe', 'Deletes all local save data including local history, lidar position and scale.')

if cfg.logging then
-- MANUAL SAVE COMMAND
RegisterCommand('lidarupload', function(source, args)
lastLoggedTarget = nil
TriggerServerEvent('prolaser4:SendLogData', HIST.loggedHistory)
HIST.loggedHistory = { }
end)
TriggerEvent('chat:addSuggestion', '/lidarupload', 'Manually upload lidar event data to server. (debugging purposes)')

-- RECORDS INTERFACE
RegisterCommand('lidarrecords', function(source, args)
waitingForServer = true
TriggerServerEvent('prolaser4:GetLogData')
Wait(5000)
if waitingForServer then
HUD:ShowNotification("~r~Error~s~: Database timed out, check server console.")
TriggerServerEvent('prolaser4:DatabaseTimeout')
end
end)
TriggerEvent('chat:addSuggestion', '/lidarrecords', 'Review lidar records.')

-- [[EVENTS]]
RegisterNetEvent("prolaser4:ReturnLogData")
AddEventHandler("prolaser4:ReturnLogData", function(databaseData)
waitingForServer = false
HUD:SendDatabaseRecords(databaseData)
HUD:SetTabletState(true)
end)
end

-- [[THREADS]]
-- SAVE/LOAD HISTORY THREAD: saves history if it's been changed every 5 minutes.
CreateThread(function()
Wait(1000)
-- load save history
local historySaveData = GetResourceKvpString(savePrefix..'history')
if historySaveData ~= nil then
HIST.history = json.decode(historySaveData)
end
-- save pending changes to kvp
while true do
Wait(60000)
if pendingChanges then
-- as the data is being pushed, we don't want to attempt to update loggedData since it's being uploaded and emptied
lastLoggedTarget = nil
SetResourceKvp(savePrefix .. 'history', json.encode(HIST.history))
if cfg.logging and #HIST.loggedHistory > 0 then
TriggerServerEvent('prolaser4:SendLogData', HIST.loggedHistory)
HIST.loggedHistory = { }
end
pendingChanges = false
end
end
end)

-- [[FUNCTION]]
-- STORE LAST 100 CLOCKS IN DATA TABLE TO SEND TO NUI FOR DISPLAY
function HIST:StoreLidarData(target, speed, range, towards)
-- format clock data
local clockString = string.format("%03.0f mph %03.1f ft", speed, range)
if towards then
clockString = '-'..clockString
else
clockString = '+'..clockString
end
-- check is this the same target we just clocked if so update clock time and return
if self.history[1] ~= nil and self.history[1].target == target then
-- if new record is of higher speed, update to the new speed
if self.history[1].speed < speed then
self.history[1].clock = clockString
self.history[1].time = GetTimeString()
end
else
-- different vehicle, store data in table and add
local data = { target = target,
speed = speed,
time = GetTimeString(),
clock = clockString,
}
-- clear old history items FIFO (first-in-first-out)
while #self.history > 99 do
table.remove(self.history, 100)
end
table.insert(self.history, 1, data)
end
-- logging data
if cfg.logging then
if not cfg.loggingPlayersOnly or IsPedAPlayer(GetPedInVehicleSeat(target, -1)) then
if lastLoggedTarget ~= target then
local loggedData = { }
loggedData['speed'] = speed
loggedData['range'] = string.format("%03.1f", range)
loggedData['time'] = GetTimeString()
local targetPos = GetEntityCoords(target)
loggedData['targetX'] = targetPos.x
loggedData['targetY'] = targetPos.y
loggedData['selfTestTimestamp'] = selfTestTimestamp

local streetHash1, streetHash2 = GetStreetNameAtCoord(targetPos.x, targetPos.y, targetPos.z, Citizen.ResultAsInteger(), Citizen.ResultAsInteger())
local streetName1 = GetStreetNameFromHashKey(streetHash1)
local streetName2 = GetStreetNameFromHashKey(streetHash2)
if not cfg.loggingOnlySpeeders or speed > speedLimits[streetName1] then
if streetName2 == "" then
loggedData['street'] = streetName1
else
loggedData['street'] = string.format("%s / %s", streetName1, streetName2)
end
lastLoggedTarget = target
table.insert(self.loggedHistory, 1, loggedData)
pendingChanges = true
end
else
-- Update pending data to reflect higher clock.
local loggedData = self.loggedHistory[1]
if loggedData ~= nil and loggedData['speed'] ~= nil then
if speed > loggedData['speed'] then
loggedData['speed'] = speed
loggedData['range'] = string.format("%03.1f", range)
loggedData['time'] = GetTimeString()
local targetPos = GetEntityCoords(target)
loggedData['targetX'] = targetPos.x
loggedData['targetY'] = targetPos.y
loggedData['selfTestTimestamp'] = selfTestTimestamp

local streetHash1, streetHash2 = GetStreetNameAtCoord(targetPos.x, targetPos.y, targetPos.z, Citizen.ResultAsInteger(), Citizen.ResultAsInteger())
local streetName1 = GetStreetNameFromHashKey(streetHash1)
local streetName2 = GetStreetNameFromHashKey(streetHash2)
if streetName2 == "" then
loggedData['street'] = streetName1
else
loggedData['street'] = string.format("%s / %s", streetName1, streetName2)
end
end
end
end
end
end
end

-- [[ GLOBAL FUNCTIONS ]]
-- HUD->HIST store self-test datetime for SQL
function HIST:SetSelfTestTimestamp()
selfTestTimestamp = GetTimeString()
end

-- HIST->HUD return KVP theme save int/enum
function HIST:GetTabletTheme()
return GetResourceKvpInt(savePrefix..'tablet_theme')
end

-- HUD->HIST send NUI theme back to HIST for storage
function HIST:SaveTabletTheme(theme)
SetResourceKvpInt(savePrefix .. 'tablet_theme', theme)
end

-- HUD->HIST send NUI OSD style back to HIST for storage
function HIST:SaveOsdStyle(data)
SetResourceKvp(savePrefix .. 'osd_style', json.encode(data))
end

-- HIST->HUD return KVP for OSD style
function HIST:GetOsdStyle()
local osdStyle = GetResourceKvpString(savePrefix..'osd_style')
if osdStyle ~= nil then
return GetResourceKvpString(savePrefix..'osd_style')
end
return false
end

-- [[ LOCAL FUNCTIONS ]]
-- Gets formatted zulu time
GetTimeString = function()
local year, month, day, hour, minute, second = GetPosixTime()
-- for some reason PosixTime returns 1 hour ahead of correct time
hour = CorrectHour(hour)
-- pad time with leading zero if needed
month = PadTime(month)
day = PadTime(day)
hour = PadTime(hour)
minute = PadTime(minute)
second = PadTime(second)
return string.format("%s/%s/%s %s:%s:%s", month, day, year, hour, minute, second)
end

PadTime = function(time)
if time < 10 then
time = "0" .. time
end
return time
end

CorrectHour = function(hour)
hour = hour - 1
if hour < 1 then
hour = 23
elseif hour > 23 then
hour = 1
end
return hour
end

View file

@ -0,0 +1,715 @@
selfTestState = not cfg.performSelfTest

holdingLidarGun = false
local cfg = cfg
local lidarGunHash = GetHashKey(cfg.lidarGunHash)
local selfTestInProgress = false
local tempHidden = false
local shown = false
local hudMode = false
local isAiming = false
local isUsingKeyboard = false
local inFirstPersonPed = true
local fpAimDownSight = false
local tpAimDownSight = false
local ped, target
local playerId = PlayerId()
local targetHeading, pedHeading, towards
local lastTarget, lastDistance, lastTime
local beingShownLidarGun = false
local mainThreadRunning = true

local lidarFOV = (cfg.minFOV+cfg.maxFOV)*0.5
local currentLidarFOV
local cam, weap, zoomvalue
local rightAxisX, rightAxisY, rotation
local camInVehicle
local inVehicleDeltaCamRot

local isHistoryActive = false
local historyIndex = 0

local slowScroll = 500
local fastScroll = 50
local scrollWait = slowScroll
local scrollDirection = nil

local rangeScalar, velocityScalar
-- Metric vs Imperial
if cfg.useMetric then
rangeScalar = 1.0
velocityScalar = 3.6
else
rangeScalar = 3.28084
velocityScalar = 2.236936
end

-- local function forward declarations
local GetLidarReturn
local CheckInputRotation, HandleZoom
local PlayButtonPressBeep
local IsTriggerControlPressed, IsFpsAimControlPressed, IsFpsUnaimControlPressed, IsTpAimControlPressed, IsTpUnaimControlPressed

-- lidar jamming from sonoran [plate] = state.
local jammedList = { }

-- TOGGLE LIDAR DISPLAY COMMAND
RegisterCommand('lidar', function(source, args)
if holdingLidarGun and not hudMode then
-- open HUD Display and self-test
if shown == true then
HUD:SetLidarDisplayState(false)
else
HUD:SetLidarDisplayState(true)
end
shown = not shown
if not selfTestState and not selfTestInProgress then
selfTestInProgress = true
selfTestState = HUD:DisplaySelfTest()
end
end
end)
RegisterKeyMapping('lidar', 'Toggle Lidar Display', 'keyboard', cfg.toggleMenu)
TriggerEvent('chat:addSuggestion', '/lidar', 'Toggle lidar display.')

-- TOGGLE LIDAR WEAPON COMMAND
RegisterCommand('lidarweapon', function(source, args)
if HasPedGotWeapon(ped, lidarGunHash) then
RemoveWeaponFromPed(ped, lidarGunHash)
else
GiveWeaponToPed(ped, lidarGunHash, 0, false, false)
end
end)
TriggerEvent('chat:addSuggestion', '/lidarweapon', 'Equip / Remove lidar weapon.')

-- SHOW LIDAR TO NEAREST PLAYER COMMAND
RegisterCommand('lidarshow', function(source, args)
if holdingLidarGun and shown then
local players = GetActivePlayers()
local closestDistance = -1
local closestPlayer = -1
local playerCoords = GetEntityCoords(ped)

for i=1,#players do
local targetPed = GetPlayerPed(players[i])
if targetPed ~= ped then
local targetCoords = GetEntityCoords(targetPed)
local distance = #(playerCoords - targetCoords)
if distance <= 3 and (closestDistance == -1 or distance < closestDistance) then
closestPlayer = players[i]
closestDistance = distance
end
end
end
if closestPlayer ~= -1 then
HUD:GetCurrentDisplayData(GetPlayerServerId(closestPlayer))
end
end
end)
TriggerEvent('chat:addSuggestion', '/lidarshow', 'Show lidar display to nearest player for 5 seconds.')

-- RESIZE / MOVE OSD
RegisterCommand('lidarmove', function(source, args)
if holdingLidarGun and shown and not hudMode then
if args[1] ~= nil and string.upper(args[1]) == 'TRUE' then
HUD:ResizeOnScreenDisplay(true)
HUD:ShowNotification("~g~Success~s~: ProLaser4 OSD position and scale reset.")
else
HUD:ResizeOnScreenDisplay()
HUD:DisplayControlHint('moveOSD')
end
end
end)
TriggerEvent('chat:addSuggestion', '/lidarmove', 'Move and resize Lidar OSD.', { { name = "reset (opt.)", help = "Optional: resets position and scale of OSD <true/false>." } } );

--Crash recovery command
RegisterCommand('lidarrecovercrash', function()
if not mainThreadRunning then
local timer = 3000
local blocked = false
CreateThread(function()
while timer > 0 do
timer = timer - 1
if mainThreadRunning then
blocked = true
end
Wait(1)
end
end)
if not blocked then
print("^3ProLaser4 Development Log: attempting to recover from a crash... This may not work. Please make a bug report with log file.")
HUD:ShowNotification("~r~ProLaser4~w~: ~y~please make a bug report with log file~w~.")
HUD:ShowNotification("~r~ProLaser4~w~: attempting to recover from a crash...")
CreateThread(MainThread)
return
end
end
print("^3ProLaser4 Development: unable to recover, appears to be running. ~y~Please make a bug report with log file~w~.")
HUD:ShowNotification("~r~ProLaser4~w~: unable to recover, running. ~y~Please make a bug report with log file~w~.")
end)
TriggerEvent('chat:addSuggestion', '/lidarrecovercrash', 'Attempts to recover ProLaser4 after the resource crashed.')

-- /lidarshow event - show lidar display to nearby player.
RegisterNetEvent("prolaser4:ReturnDisplayData")
AddEventHandler("prolaser4:ReturnDisplayData", function(displayData)
if not shown then
beingShownLidarGun = false
HUD:SetSelfTestState(true)
if (displayData.onHistory) then
HUD:SetHistoryState(true)
HUD:SetHistoryData(displayData.counter, { time = displayData.time, clock = displayData.clock } )
else
HUD:SetHistoryState(false)
HUD:SendPeersDisplayData(displayData)
end
Wait(500)
HUD:SetLidarDisplayState(true)
beingShownLidarGun = true
local timer = GetGameTimer() + 8000
while GetGameTimer() < timer do
-- if displayed again, do not hide return and use new event thread
if not beingShownLidarGun then
return
end
Wait(250)
end
HUD:SetLidarDisplayState(false)
Wait(500)
HUD:SetHistoryState(false)
HUD:SetSelfTestState(selfTestState)
end
end)

-- MAIN GET VEHICLE TO CLOCKTHREAD & START UP
CreateThread(function()
CreateThread(MainThread)
Wait(2000)
-- Initalize lidar state and vars LUA->JS
HUD:SetSelfTestState(selfTestState, false)
HUD:SendBatteryPercentage()
HUD:SendConfigData()

-- Texture load check & label replacement.
AddTextEntry(cfg.lidarNameHashString, "ProLaser 4")
RequestStreamedTextureDict(cfg.lidarGunTextureDict)
while not HasStreamedTextureDictLoaded(cfg.lidarGunTextureDict) do
Wait(100)
end

while true do
ped = PlayerPedId()
holdingLidarGun = GetSelectedPedWeapon(ped) == lidarGunHash
if holdingLidarGun then
isInVehicle = IsPedInAnyVehicle(ped, true)
isAiming = IsPlayerFreeAiming(playerId)
isGtaMenuOpen = IsWarningMessageActive() or IsPauseMenuActive()
Wait(100)
else
Wait(500)
end

end
end)

-- REMOVE CONTROLS & HUD MESSAGE
CreateThread( function()
while true do
Wait(1)
if holdingLidarGun then
HideHudComponentThisFrame(2)
if isAiming then
if not hudMode then
DrawSprite(cfg.lidarGunTextureDict, "lidar_reticle", 0.5, 0.5, 0.005, 0.01, 0.0, 200, 200, 200, 255)
else
DisableControlAction(0, 26, true) -- INPUT_LOOK_BEHIND
end
-- if aiming down sight disable change weapon to enable scrolling without HUD wheel opening
DisableControlAction(0, 99, true) -- INPUT_VEH_SELECT_NEXT_WEAPON
DisableControlAction(0, 16, true) -- INPUT_SELECT_NEXT_WEAPON
DisableControlAction(0, 17, true) -- INPUT_SELECT_PREV_WEAPON
end
DisablePlayerFiring(ped, true) -- Disable Weapon Firing
DisableControlAction(0, 24, true) -- Disable Trigger Action
DisableControlAction(0, cfg.previousHistory, true)
DisableControlAction(0, cfg.nextHistory, true)
DisableControlAction(0, 142, true) -- INPUT_MELEE_ATTACK_ALTERNATE
end
end
end)

-- ADS HUD Call -> JS
CreateThread( function()
while true do
if holdingLidarGun or hudMode then
inFirstPersonPed = not isInVehicle and GetFollowPedCamViewMode() == 4
inFirstPersonVeh = isInVehicle and GetFollowVehicleCamViewMode() == 4
if not hudMode and fpAimDownSight and (inFirstPersonPed or inFirstPersonVeh) then
if not shown then
shown = true
if not selfTestState and not selfTestInProgress then
selfTestInProgress = true
HUD:DisplaySelfTest()
end
end
hudMode = true
HUD:SetDisplayMode('ADS')
DisplayRadar(false)
elseif shown and hudMode and not (fpAimDownSight and (inFirstPersonPed or inFirstPersonVeh)) then
hudMode = false
HUD:SetDisplayMode('DISPLAY')
DisplayRadar(true)
end
Wait(100)
else
Wait(500)
end
end
end)

--LIDAR MAIN THREAD: handle hiding lidar NUI, self-test, ADS aiming, clocking, and control handling.
function MainThread()
while true do
-- Crash recovery variable, resets to true at end of loop.
mainThreadRunning = false
Wait(1)
-- Hide HUD if weapon not selected, keep lidar on
if ( ( not holdingLidarGun and not beingShownLidarGun ) or isGtaMenuOpen) and shown and not tempHidden then
HUD:SetDisplayMode('DISPLAY')
hudMode = false
HUD:SetLidarDisplayState(false)
tempHidden = true
elseif holdingLidarGun and not isGtaMenuOpen and tempHidden then
HUD:SetLidarDisplayState(true)
tempHidden = false
end

if holdingLidarGun then
isUsingKeyboard = IsUsingKeyboard(0)
-- toggle ADS if first person and aim, otherwise unADS
if IsFpsAimControlPressed() then
fpAimDownSight = true
SetPlayerForcedAim(playerId, true)
elseif IsFpsUnaimControlPressed() then
fpAimDownSight = false
SetPlayerForcedAim(playerId, false)
-- Simulate control just released, if still holding right click disable the control till they unclick to prevent retoggling accidently
while IsControlJustPressed(0,25) or IsDisabledControlPressed(0,25) or IsControlPressed(0,177) or IsDisabledControlPressed(0,177) or IsControlJustPressed(0,68) or IsDisabledControlPressed(0,68) do
DisableControlAction(0, 25, true) -- INPUT_AIM
DisableControlAction(0, 177, true) -- INPUT_CELLPHONE_CANCEL
DisableControlAction(0, 68, true) -- INPUT_VEH_AIM
Wait(1)
end
Wait(100)
elseif not fpAimDownSight and (inFirstPersonPed or inFirstPersonVeh) and isAiming then
fpAimDownSight = true
SetPlayerForcedAim(playerId, true)
end
-- toggle ADS if in third person and aim, otherwide unaim
if not (inFirstPersonPed or inFirstPersonVeh) then
if IsTpAimControlPressed() then
tpAimDownSight = true
SetPlayerForcedAim(playerId, true)
elseif IsTpUnaimControlPressed() then
tpAimDownSight = false
SetPlayerForcedAim(playerId, false)
-- Simulate control just released, if still holding right click disable the control till they unclick to prevent retoggling accidently
while IsControlJustPressed(0,25) or IsDisabledControlPressed(0,25) or IsControlPressed(0,177) or IsDisabledControlPressed(0,177) or IsControlJustPressed(0,68) or IsDisabledControlPressed(0,68) do
DisableControlAction(0, 25, true) -- INPUT_AIM
DisableControlAction(0, 177, true) -- INPUT_CELLPHONE_CANCEL
DisableControlAction(0, 68, true) -- INPUT_VEH_AIM
Wait(1)
end
end
end
-- Get target speed and update display
if shown and not tempHidden and selfTestState then
if IsTriggerControlPressed() then
found, target = GetEntityPlayerIsFreeAimingAt(playerId)
if IsPedInAnyVehicle(target) then
target = GetVehiclePedIsIn(target, false)
end
speed, range, towards = GetLidarReturn(target, ped)
if towards ~= -1 then
HUD:SendLidarUpdate(speed, range, towards)
HIST:StoreLidarData(target, speed, range, towards)
else
HUD:ClearLidarDisplay()
end
Wait(250)
-- Hides history if on first, otherwise go to previous history
elseif IsDisabledControlPressed(0, cfg.previousHistory) and isUsingKeyboard and #HIST.history > 0 then
if isHistoryActive then
historyIndex = historyIndex - 1
if scrollWait == slowScroll then
PlayButtonPressBeep()
end
if historyIndex > 0 then
HUD:SetHistoryData(historyIndex, HIST.history[historyIndex])
Wait(scrollWait)
else
isHistoryActive = false
HUD:SetHistoryState(false)
end
end
-- Displays history if not shown, otherwise go to next history page.
elseif IsDisabledControlPressed(0, cfg.nextHistory) and isUsingKeyboard and #HIST.history > 0 then
isHistoryActive = true
HUD:SetHistoryState(isHistoryActive)
if historyIndex < #HIST.history then
if scrollWait == slowScroll then
PlayButtonPressBeep()
end
historyIndex = historyIndex + 1
HUD:SetHistoryData(historyIndex, HIST.history[historyIndex])
Wait(scrollWait)
end
elseif IsDisabledControlJustReleased(0, cfg.changeSight) and isUsingKeyboard and fpAimDownSight then
HUD:ChangeSightStyle()
end
end
-- force unaim if no longer holding lidar gun
elseif tpAimDownSight or fpAimDownSight then
SetPlayerForcedAim(playerId, false)
tpAimDownSight = false
fpAimDownSight = false
else
Wait(500)
end
-- Crash detection: iteration completed successfully
mainThreadRunning = true
end
end

-- ADVANCED CONTROL HANDLING
-- Handles controller vs keyboard events, faster validation checking.
IsTriggerControlPressed = function()
if not isAiming then
return false
end

if isHistoryActive then
return false
end
-- Angle Limitation
if tpAimDownSight and (GetGameplayCamRelativeHeading() < -131 or GetGameplayCamRelativeHeading() > 178) then
return false
end
-- INPUT_ATTACK or INPUT_VEH_HANDBRAKE (LCLICK, SPACEBAR, CONTROLLER RB)
-- On foot, LMOUSE and Trigger In vehicle RB
if (IsDisabledControlPressed(0, 24) and (not isInVehicle or isUsingKeyboard)) or (IsControlPressed(0, 76) and isInVehicle) then
return true
end
return false
end

-----------------------------------------
IsFpsAimControlPressed = function()
if fpAimDownSight then
return false
end
if not inFirstPersonPed or not inFirstPersonVeh then
return false
end
-- LBUMPER OR LMOUSE IN VEHICLE
if IsControlJustPressed(0,68) and isInVehicle then
return true
end
-- LTRIGGER OR LMOUSE ON FOOT
if IsControlJustPressed(0, 25) and not isInVehicle then
return true
end
return false
end

-----------------------------------------
IsFpsUnaimControlPressed= function()
if not fpAimDownSight then
return false
end

if not (inFirstPersonPed or inFirstPersonVeh) then
return true
end
-- LBUMPER OR LMOUSE IN VEHICLE
if IsControlJustPressed(0,68) and isInVehicle then
return true
end
-- LTRIGGER OR LMOUSE ON FOOT
if IsControlJustPressed(0, 25) and not isInVehicle then
return true
end

-- BACKSPACE, ESC, RMOUSE or V (view-change)
if (isUsingKeyboard and (IsControlJustPressed(0,177)) or IsControlJustPressed(0, 0)) then
return true
end
return false
end

-----------------------------------------
IsTpAimControlPressed = function()
if tpAimDownSight then
return false
end
-- LBUMPER OR LMOUSE IN VEHICLE
if IsControlJustPressed(0,68) and isInVehicle then
return true
end
-- LTRIGGER OR LMOUSE ON FOOT
if IsControlJustPressed(0, 25) and not isInVehicle then
return true
end
return false
end

-----------------------------------------
IsTpUnaimControlPressed = function()
if not tpAimDownSight then
return false
end
-- LBUMPER OR LMOUSE IN VEHICLE
if IsControlJustPressed(0,68) and isInVehicle then
return true
end
-- LTRIGGER OR LMOUSE ON FOOT
if IsControlJustPressed(0, 25) and not isInVehicle then
return true
end

-- BACKSPACE, ESC, RMOUSE or V (view-change)
if (isUsingKeyboard and (IsControlJustPressed(0,177)) or IsControlJustPressed(0, 0)) then
return true
end
return false
end

-----------------------------------------

-- SCROLL SPEED: handles fast scrolling, if holding scroll increase scroll speed.
CreateThread(function()
Wait(1000)
while true do
if holdingLidarGun and isHistoryActive then
if IsDisabledControlPressed(0, cfg.nextHistory) then
local count = 0
while IsDisabledControlPressed(0, cfg.nextHistory) do
count = count + 1
if count > 15 then
scrollDirection = 'next'
scrollWait = fastScroll
break;
end
Wait(100)
end
if scrollDirection == 'next' and not IsDisabledControlPressed(0, cfg.nextHistory) then
scrollWait = slowScroll
end
elseif IsDisabledControlPressed(0, cfg.previousHistory) then
local count = 0
while IsDisabledControlPressed(0, cfg.previousHistory) do
count = count + 1
if count > 15 then
scrollDirection = 'prev'
scrollWait = fastScroll
break;
end
Wait(100)
end
if scrollDirection == 'prev' and not IsDisabledControlPressed(0, cfg.previousHistory) then
scrollWait = slowScroll
end
end
else
Wait(500)
end
Wait(0)
end
end)

-- AIM DOWNSIGHTS CAM & ZOOM
CreateThread(function()
while true do
if holdingLidarGun then
if fpAimDownSight then
cam = CreateCam("DEFAULT_SCRIPTED_FLY_CAMERA", true)
weap = GetCurrentPedWeaponEntityIndex(ped)
if isInVehicle then
AttachCamToEntity(cam, weap, -0.018, -0.2, -0.05, true)
camInVehicle = true
else
AttachCamToEntity(cam, weap, 0.0, -0.2, -0.0, true)
camInVehicle = false
end
SetCamRot(cam, GetGameplayCamRot(2), 2)
SetCamFov(cam, lidarFOV)
RenderScriptCams(true, false, 0, 1, 0)
if cfg.displayControls then
HUD:DisplayControlHint('fpADS')
cfg.displayControls = false
end

while fpAimDownSight and not IsEntityDead(ped) do
if ((camInVehicle and not isInVehicle) or (not camInVehicle and isInVehicle)) or not holdingLidarGun then
fpAimDownSight = false
SetPlayerForcedAim(playerId, false)
break
end
zoomvalue = (1.0/(cfg.maxFOV-cfg.minFOV))*(lidarFOV-cfg.minFOV)
CheckInputRotation(cam, zoomvalue)
HandleZoom(cam)
Wait(1)
end
RenderScriptCams(false, false, 0, 1, 0)
SetScaleformMovieAsNoLongerNeeded(scaleform)
DestroyCam(cam, false)
end
Wait(1)
else
Wait(500)
end
end
end)


--FUNCTIONS--
-- COSINE ERROR CALULCATIONS AND TOWARDS/AWAY STATE
-- SEE: https://copradar.com/chapts/chapt2/ch2d1.html
GetLidarReturn = function(target, ped)
-- no target found
if target == 0 then
return 0, 0, -1
end
-- sonoran jammer
if cfg.sonoranJammer then
if IsEntityAVehicle(target) and next(jammedList) ~= nil then
if jammedList[GetVehicleNumberPlateText(target)] then
return 0, 0, -1
end
end
end

-- towards calculations
targetHeading = GetEntityHeading(target)
if hudMode then
pedHeading = GetCamRot(cam, 2)[3]
else
pedHeading = GetEntityHeading(ped) + GetGameplayCamRelativeHeading()
end
towards = false
diffHeading = math.abs((pedHeading - targetHeading + 180) % 360 - 180)
if ( diffHeading > 135 ) then
towards = true
end
if diffHeading < 160 and diffHeading > 110 or
diffHeading > 20 and diffHeading < 70 then
if math.random(0, 100) > 15 then
return 0, 0, -1
end
end
targetPos = GetEntityCoords(target)
distance = #(targetPos-GetEntityCoords(ped))
if lastDistance ~= 0 and lastTarget == target then
-- distance traveled in meters
distanceTraveled = lastDistance - distance
-- time between last clock and current
timeElapsed = (lastTime - GetGameTimer()) / 1000
-- distance over time with conversion from neters to miles.
speedEstimate = math.abs((distanceTraveled * velocityScalar) / timeElapsed)
-- update last values to determine next clock
lastDistance, lastTarget, lastTime = distance, target, GetGameTimer()
else
lastDistance, lastTarget, lastTime = distance, target, GetGameTimer()
return 0, 0, -1
end
return speedEstimate, distance, towards
end

-- AIM DOWNSIGHTS PAN
CheckInputRotation = function(cam, zoomvalue)
rightAxisX = GetDisabledControlNormal(0, 220)
rightAxisY = GetDisabledControlNormal(0, 221)
rotation = GetCamRot(cam, 2)
if rightAxisX ~= 0.0 or rightAxisY ~= 0.0 then
if isInVehicle then
newZ = rotation.z + rightAxisX*-1.0*(cfg.verticalPanSpeed-zoomvalue*8)
newX = math.max(math.min(20.0, rotation.x + rightAxisY*-1.0*(cfg.horizontalPanSpeed-zoomvalue*8)), -20.0) -- Clamping at top (cant see top of heli) and at bottom (doesn't glitch out in -90deg)
SetCamRot(cam, newX, 0.0, newZ, 2)
SetGameplayCamRelativeRotation(0.0, 0.0, 0.0)
-- limit ADS rotation while in vehicle
inVehicleDeltaCamRot = (GetCamRot(cam, 2)[3] - GetEntityHeading(ped) + 180) % 360 - 180
while inVehicleDeltaCamRot < -75 and inVehicleDeltaCamRot > -130 do
newZ = newZ + 0.2
SetCamRot(cam, newX, 0.0, newZ, 2)
inVehicleDeltaCamRot = (GetCamRot(cam, 2)[3] - GetEntityHeading(ped) + 180) % 360 - 180
Wait(1)
end
while inVehicleDeltaCamRot > 178 or (inVehicleDeltaCamRot > -180 and inVehicleDeltaCamRot < -130) do
newZ = newZ - 0.2
SetCamRot(cam, newX, 0.0, newZ, 2)
inVehicleDeltaCamRot = (GetCamRot(cam, 2)[3] - GetEntityHeading(ped) + 180) % 360 - 180
Wait(1)
end
else
newZ = rotation.z + rightAxisX*-1.0*(cfg.verticalPanSpeed-zoomvalue*8)
newX = math.max(math.min(40.0, rotation.x + rightAxisY*-1.0*(cfg.horizontalPanSpeed-zoomvalue*8)), -89.5) -- Clamping at top (cant see top of heli) and at bottom (doesn't glitch out in -90deg)
SetCamRot(cam, newX, 0.0, newZ, 2)
SetGameplayCamRelativeRotation(0.0, newX, newZ)
end
end
end

-- AIM DOWNSIGHTS ZOOM
HandleZoom = function(cam)
if IsDisabledControlPressed(0,15) or IsDisabledControlPressed(0, 99) then -- Scrollup
lidarFOV = math.max(lidarFOV - cfg.zoomSpeed, cfg.maxFOV)
end
if IsDisabledControlPressed(0,334) or IsDisabledControlPressed(0, 16) then
lidarFOV = math.min(lidarFOV + cfg.zoomSpeed/6, cfg.minFOV) -- ScrollDown
end
currentLidarFOV = GetCamFov(cam)
if math.abs(lidarFOV-currentLidarFOV) < 0.1 then -- the difference is too small, just set the value directly to avoid unneeded updates to FOV of order 10^-5
lidarFOV = currentLidarFOV
end
SetCamFov(cam, currentLidarFOV + (lidarFOV - currentLidarFOV)*0.03) -- Smoothing of camera zoom
end

-- Play NUI front in audio.
PlayButtonPressBeep = function()
SendNUIMessage({
action = 'PlayButtonPressBeep',
file = 'LidarBeep',
})
end

--[[SONORAN RADAR / LIDAR JAMMER]]
if cfg.sonoranJammer then
RegisterNetEvent( "Sonoran:SendJammedListToClient", function (listFromServer)
jammedList = listFromServer
end)
end

View file

@ -0,0 +1,232 @@
--[[
Checkout semver.lua semantic versioning library for LUA
https://github.com/kikito/semver.lua

Copyright (c) 2011 Enrique García Cota

MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]

semver = {
_VERSION = '1.2.1',
_DESCRIPTION = 'semver for Lua',
_URL = 'https://github.com/kikito/semver.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2015 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of tother software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and tother permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}

local function checkPositiveInteger(number, name)
assert(number >= 0, name .. ' must be a valid positive number')
assert(math.floor(number) == number, name .. ' must be an integer')
end

local function present(value)
return value and value ~= ''
end

-- splitByDot("a.bbc.d") == {"a", "bbc", "d"}
local function splitByDot(str)
str = str or ""
local t, count = {}, 0
str:gsub("([^%.]+)", function(c)
count = count + 1
t[count] = c
end)
return t
end

local function parsePrereleaseAndBuildWithSign(str)
local prereleaseWithSign, buildWithSign = str:match("^(-[^+]+)(+.+)$")
if not (prereleaseWithSign and buildWithSign) then
prereleaseWithSign = str:match("^(-.+)$")
buildWithSign = str:match("^(+.+)$")
end
assert(prereleaseWithSign or buildWithSign, ("The parameter %q must begin with + or - to denote a prerelease or a build"):format(str))
return prereleaseWithSign, buildWithSign
end

local function parsePrerelease(prereleaseWithSign)
if prereleaseWithSign then
local prerelease = prereleaseWithSign:match("^-(%w[%.%w-]*)$")
assert(prerelease, ("The prerelease %q is not a slash followed by alphanumerics, dots and slashes"):format(prereleaseWithSign))
return prerelease
end
end

local function parseBuild(buildWithSign)
if buildWithSign then
local build = buildWithSign:match("^%+(%w[%.%w-]*)$")
assert(build, ("The build %q is not a + sign followed by alphanumerics, dots and slashes"):format(buildWithSign))
return build
end
end

local function parsePrereleaseAndBuild(str)
if not present(str) then return nil, nil end

local prereleaseWithSign, buildWithSign = parsePrereleaseAndBuildWithSign(str)

local prerelease = parsePrerelease(prereleaseWithSign)
local build = parseBuild(buildWithSign)

return prerelease, build
end

local function parseVersion(str)
local sMajor, sMinor, sPatch, sPrereleaseAndBuild = str:match("^(%d+)%.?(%d*)%.?(%d*)(.-)$")
assert(type(sMajor) == 'string', ("Could not extract version number(s) from %q"):format(str))
local major, minor, patch = tonumber(sMajor), tonumber(sMinor), tonumber(sPatch)
local prerelease, build = parsePrereleaseAndBuild(sPrereleaseAndBuild)
return major, minor, patch, prerelease, build
end


-- return 0 if a == b, -1 if a < b, and 1 if a > b
local function compare(a,b)
return a == b and 0 or a < b and -1 or 1
end

local function compareIds(myId, otherId)
if myId == otherId then return 0
elseif not myId then return -1
elseif not otherId then return 1
end

local selfNumber, otherNumber = tonumber(myId), tonumber(otherId)

if selfNumber and otherNumber then -- numerical comparison
return compare(selfNumber, otherNumber)
-- numericals are always smaller than alphanums
elseif selfNumber then
return -1
elseif otherNumber then
return 1
else
return compare(myId, otherId) -- alphanumerical comparison
end
end

local function smallerIdList(myIds, otherIds)
local myLength = #myIds
local comparison

for i=1, myLength do
comparison = compareIds(myIds[i], otherIds[i])
if comparison ~= 0 then
return comparison == -1
end
-- if comparison == 0, continue loop
end

return myLength < #otherIds
end

local function smallerPrerelease(mine, other)
if mine == other or not mine then return false
elseif not other then return true
end

return smallerIdList(splitByDot(mine), splitByDot(other))
end

local methods = {}

function methods:nextMajor()
return semver(self.major + 1, 0, 0)
end
function methods:nextMinor()
return semver(self.major, self.minor + 1, 0)
end
function methods:nextPatch()
return semver(self.major, self.minor, self.patch + 1)
end

local mt = { __index = methods }
function mt:__eq(other)
return self.major == other.major and
self.minor == other.minor and
self.patch == other.patch and
self.prerelease == other.prerelease
-- notice that build is ignored for precedence in semver 2.0.0
end
function mt:__lt(other)
if self.major ~= other.major then return self.major < other.major end
if self.minor ~= other.minor then return self.minor < other.minor end
if self.patch ~= other.patch then return self.patch < other.patch end
return smallerPrerelease(self.prerelease, other.prerelease)
-- notice that build is ignored for precedence in semver 2.0.0
end
-- This works like the "pessimisstic operator" in Rubygems.
-- if a and b are versions, a ^ b means "b is backwards-compatible with a"
-- in other words, "it's safe to upgrade from a to b"
function mt:__pow(other)
if self.major == 0 then
return self == other
end
return self.major == other.major and
self.minor <= other.minor
end
function mt:__tostring()
local buffer = { ("%d.%d.%d"):format(self.major, self.minor, self.patch) }
if self.prerelease then table.insert(buffer, "-" .. self.prerelease) end
if self.build then table.insert(buffer, "+" .. self.build) end
return table.concat(buffer)
end

local function new(major, minor, patch, prerelease, build)
assert(major, "At least one parameter is needed")

if type(major) == 'string' then
major,minor,patch,prerelease,build = parseVersion(major)
end
patch = patch or 0
minor = minor or 0

checkPositiveInteger(major, "major")
checkPositiveInteger(minor, "minor")
checkPositiveInteger(patch, "patch")

local result = {major=major, minor=minor, patch=patch, prerelease=prerelease, build=build}
return setmetatable(result, mt)
end

setmetatable(semver, { __call = function(_, ...) return new(...) end })
semver._VERSION= semver(semver._VERSION)

return semver

View file

@ -0,0 +1,209 @@
local cfg = cfg

-- ShowLidar, repeater event to nearest player to show lidar to.
RegisterServerEvent('prolaser4:SendDisplayData')
AddEventHandler('prolaser4:SendDisplayData', function(target, data)
TriggerClientEvent('prolaser4:ReturnDisplayData', target, data)
end)

-- Database timeout event from client->server for server console log.
RegisterServerEvent('prolaser4:DatabaseTimeout')
AddEventHandler('prolaser4:DatabaseTimeout', function()
print(string.format('^8[ERROR]: ^3Database timed out for %s after 5 seconds. Lidar records tablet unavailable.\n\t\t1) Ensure your database is online.\n\t\t2) restart oxmysql.\n\t\t3) restart ProLaser4.^7', GetPlayerName(source)))
end)

function DebugPrint(text)
if cfg.serverDebugging then
print(text)
end
end

--[[--------------- ADVANCED LOGGING --------------]]
if cfg.logging and MySQL ~= nil then
local isInsertActive = false
LOGGED_EVENTS = { }
TEMP_LOGGED_EVENTS = { }
---------------- QUERIES ----------------
local insertQuery = [[
INSERT INTO prolaser4
(timestamp, speed, distance, targetX, targetY, player, street, selfTestTimestamp)
VALUES
(STR_TO_DATE(?, "%m/%d/%Y %H:%i:%s"), ?, ?, ?, ?, ?, ?, STR_TO_DATE(?, "%m/%d/%Y %H:%i:%s"))
]]
local selectQueryRaw = [[
SELECT
rid,
DATE_FORMAT(timestamp, "%c/%d/%y %H:%i") AS timestamp,
speed,
distance as 'range',
targetX,
targetY,
player,
street,
DATE_FORMAT(selfTestTimestamp, "%m/%d/%Y %H:%i") AS selfTestTimestamp
FROM prolaser4
ORDER BY timestamp
LIMIT
]]
local selectQuery = string.format("%s %s", selectQueryRaw, cfg.loggingSelectLimit)
local countQuery = 'SELECT COUNT(*) FROM prolaser4'
local cleanupQuery = 'DELETE FROM prolaser4 WHERE timestamp < DATE_SUB(NOW(), INTERVAL ? DAY);'
-----------------------------------------
-- Debugging Command
RegisterCommand('lidarsqlupdate', function(source, args)
-- check if from server console
if source == 0 then
DebugPrint('^3[INFO]: Manually inserting records to SQL.^7')
InsertRecordsToSQL()
else
DebugPrint(string.format('^3[INFO]: Attempted to manually insert records but got source %s.^7', source))
TriggerClientEvent('chat:addMessage', source, { args = { '^1Error', 'This command can only be executed from the console.' } })
end
end)
-----------------------------------------
-- Main thread, every restart remove old records if needed, insert records every X minutes as defined by cfg.loggingInsertInterval.
CreateThread(function()
local insertWait = cfg.loggingInsertInterval * 60000
if cfg.loggingCleanUpInterval ~= -1 then
CleanUpRecordsFromSQL()
end
while true do
InsertRecordsToSQL()
Wait(insertWait)
end
end)

---------------- SETTER / INSERT ----------------
-- Shared event handler colate all lidar data from all players for SQL submission.
RegisterServerEvent('prolaser4:SendLogData')
AddEventHandler('prolaser4:SendLogData', function(logData)
local playerName = GetPlayerName(source)
if not isInsertActive then
for i, entry in ipairs(logData) do
entry.player = playerName
table.insert(LOGGED_EVENTS, entry)
end
else
-- since the insertion is active, inserting now may result in lost data, store temporarily.
for i, entry in ipairs(logData) do
entry.player = playerName
table.insert(TEMP_LOGGED_EVENTS, entry)
end
end
end)

-- Inserts records to SQL table
function InsertRecordsToSQL()
if not isInsertActive then
if #LOGGED_EVENTS > 0 then
DebugPrint(string.format('^3[INFO]: Started inserting %s records.^7', #LOGGED_EVENTS))
isInsertActive = true
-- Execute the insert statement for each entry
for _, entry in ipairs(LOGGED_EVENTS) do
MySQL.insert(insertQuery, {entry.time, entry.speed, entry.range, entry.targetX, entry.targetY, entry.player, entry.street, entry.selfTestTimestamp}, function(returnData) end)
end
-- Remove processed records
LOGGED_EVENTS = {}
isInsertActive = false
-- Copy over temp entries to be processed next run
for _, entry in ipairs(TEMP_LOGGED_EVENTS) do
table.insert(LOGGED_EVENTS, entry)
end
-- Remove copied over values.
TEMP_LOGGED_EVENTS = {}
DebugPrint('^3[INFO]: Finished inserting records.^7')
end
end
end
---------------- GETTER / SELECT ----------------
-- C->S request all record data
RegisterNetEvent('prolaser4:GetLogData')
AddEventHandler('prolaser4:GetLogData', function()
SelectRecordsFromSQL(source)
end)

-- Get all record data and return to client
function SelectRecordsFromSQL(source)
DebugPrint(string.format('^3[INFO]: Getting records for %s.^7', GetPlayerName(source)))
MySQL.query(selectQuery, {}, function(result)
DebugPrint(string.format('^3[INFO]: Returned %s from select query.^7', #result))
if result then
TriggerClientEvent('prolaser4:ReturnLogData', source, result)
end
end)
end
------------------ AUTO CLEANUP -----------------
-- Remove old records after X days old.
function CleanUpRecordsFromSQL()
DebugPrint('^3[INFO]: Removing old records.^7');
MySQL.query(cleanupQuery, {cfg.loggingCleanUpInterval}, function(returnData)
DebugPrint(string.format('^3[INFO]: Removed %s records (older than %s days)^7', returnData.affectedRows, cfg.loggingCleanUpInterval));
end)
end
------------------ RECORD COUNT -----------------
function GetRecordCount()
local recordCount = '^8FAILED TO RETRIEVE ^7'
MySQL.query(countQuery, {}, function(returnData)
if returnData and returnData[1] and returnData[1]['COUNT(*)'] then
recordCount = returnData[1]['COUNT(*)']
end
end)
Wait(500)
return recordCount
end
end

--[[------------ STARTUP / VERSION CHECKING -----------]]
CreateThread( function()
local currentVersion = semver(GetResourceMetadata(GetCurrentResourceName(), 'version', 0))
local repoVersion = semver('0.0.0')
local recordCount = 0
-- Get prolaser4 version from github
PerformHttpRequest('https://raw.githubusercontent.com/TrevorBarns/ProLaser4/main/version', function(err, responseText, headers)
if responseText ~= nil and responseText ~= '' then
repoVersion = semver(responseText:gsub('\n', ''))
end
end)
if cfg.logging then
if MySQL == nil then
print('^3[WARNING]: logging enabled, but oxmysql not found. Did you uncomment the oxmysql\n\t\t lines in fxmanifest.lua?\n\n\t\t Remember, changes to fxmanifest are only loaded after running `refresh`, then `restart`.^7')
recordCount = '^8NO CONNECTION^7'
else
recordCount = GetRecordCount()
end
end
Wait(1000)
print('\n\t^7 _______________________________________________________')
print('\t|^8 ____ __ __ __ ^7|')
print('\t|^8 / __ \\_________ / / ____ ________ _____/ // / ^7|')
print('\t|^8 / /_/ / ___/ __ \\/ / / __ `/ ___/ _ \\/ ___/ // /_ ^7|')
print('\t|^8 / ____/ / / /_/ / /___/ /_/ (__ ) __/ / /__ __/ ^7|')
print('\t|^8 /_/ /_/ \\____/_____/\\__,_/____/\\___/_/ /_/ ^7|')
print('\t^7|_______________________________________________________|')
print(('\t|\t INSTALLED: %-26s|'):format(currentVersion))
print(('\t|\t LATEST: %-26s|'):format(repoVersion))
if cfg.logging then
if type(recordCount) == 'number' then
print(('\t|\t RECORD COUNT: %-26s|'):format(recordCount))
else
print(('\t|\t RECORD COUNT: %-30s|'):format(recordCount))
end
end
if currentVersion < repoVersion then
print('\t^7|_______________________________________________________|')
print('\t|\t ^8STABLE UPDATE AVAILABLE ^7|')
print('\t|^8 DOWNLOAD AT: ^7|')
print('\t|^5 github.com/TrevorBarns/ProLaser4/releases ^7|')
end
print('\t^7|_______________________________________________________|')
print('\t^7| Updates, Support, Feedback: ^5discord.gg/PXQ4T8wB9 ^7|')
print('\t^7|_______________________________________________________|\n\n')
end)