--[[--------------------------------------------------------------------------------------- Wraith ARS 2X Created by WolfKnight For discussions, information on future updates, and more, join my Discord: https://discord.gg/fD4e6WD MIT License Copyright (c) 2020-2021 WolfKnight 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. ---------------------------------------------------------------------------------------]]-- -- Cache some of the main Lua functions and libraries local next = next local dot = dot local table = table local type = type local tostring = tostring local math = math local pairs = pairs --[[---------------------------------------------------------------------------------- UI loading and key binds registering ----------------------------------------------------------------------------------]]-- local function RegisterKeyBinds() if ( UTIL:IsResourceNameValid() ) then UTIL:Log( "Registering radar commands and key binds." ) -- Opens the remote control RegisterCommand( "radar_remote", function() if ( not RADAR:GetKeyLockState() ) then RADAR:OpenRemote() end end ) RegisterKeyMapping( "radar_remote", "Open Remote Control", "keyboard", CONFIG.keyDefaults.remote_control ) -- Locks speed from front antenna RegisterCommand( "radar_fr_ant", function() if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then RADAR:LockAntennaSpeed( "front", nil ) SYNC:LockAntennaSpeed( "front", RADAR:GetAntennaDataPacket( "front" ) ) end end ) RegisterKeyMapping( "radar_fr_ant", "Front Antenna Lock/Unlock", "keyboard", CONFIG.keyDefaults.front_lock ) -- Locks speed from rear antenna RegisterCommand( "radar_bk_ant", function() if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then RADAR:LockAntennaSpeed( "rear", nil ) SYNC:LockAntennaSpeed( "rear", RADAR:GetAntennaDataPacket( "rear" ) ) end end ) RegisterKeyMapping( "radar_bk_ant", "Rear Antenna Lock/Unlock", "keyboard", CONFIG.keyDefaults.rear_lock ) -- Locks front plate reader RegisterCommand( "radar_fr_cam", function() if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then READER:LockCam( "front", true, false ) SYNC:LockReaderCam( "front", READER:GetCameraDataPacket( "front" ) ) end end ) RegisterKeyMapping( "radar_fr_cam", "Front Plate Reader Lock/Unlock", "keyboard", CONFIG.keyDefaults.plate_front_lock ) -- Locks rear plate reader RegisterCommand( "radar_bk_cam", function() if ( not RADAR:GetKeyLockState() and PLY:CanControlRadar() ) then READER:LockCam( "rear", true, false ) SYNC:LockReaderCam( "rear", READER:GetCameraDataPacket( "rear" ) ) end end ) RegisterKeyMapping( "radar_bk_cam", "Rear Plate Reader Lock/Unlock", "keyboard", CONFIG.keyDefaults.plate_rear_lock ) -- Toggles the key lock state RegisterCommand( "radar_key_lock", function() RADAR:ToggleKeyLock() end ) RegisterKeyMapping( "radar_key_lock", "Toggle Keybind Lock", "keyboard", CONFIG.keyDefaults.key_lock ) -- Deletes all of the KVPs RegisterCommand( "reset_radar_data", function() DeleteResourceKvp( "wk_wars2x_ui_data" ) DeleteResourceKvp( "wk_wars2x_om_data" ) DeleteResourceKvp( "wk_wars2x_new_user" ) UTIL:Notify( "Radar data deleted, please immediately restart your game without opening the radar's remote." ) end, false ) TriggerEvent( "chat:addSuggestion", "/reset_radar_data", "Resets the KVP data stored for the wk_wars2x resource." ) else UTIL:Log( "ERROR: Resource name is not wk_wars2x. Key binds will not be registered for compatibility reasons. Contact the server owner and ask them to change the resource name back to wk_wars2x" ) end end local function LoadUISettings() UTIL:Log( "Attempting to load saved UI settings data." ) -- Try and get the saved UI data local uiData = GetResourceKvpString( "wk_wars2x_ui_data" ) -- If the data exists, then we send it off! if ( uiData ~= nil ) then SendNUIMessage( { _type = "loadUiSettings", data = json.decode( uiData ) } ) UTIL:Log( "Saved UI settings data loaded!" ) -- If the data doesn't exist, then we send the defaults else SendNUIMessage( { _type = "setUiDefaults", data = CONFIG.uiDefaults } ) UTIL:Log( "Could not find any saved UI settings data." ) end end --[[---------------------------------------------------------------------------------- Radar variables NOTE - This is not a config, do not touch anything unless you know what you are actually doing. ----------------------------------------------------------------------------------]]-- RADAR = {} RADAR.vars = { -- Whether or not the radar's UI is visible displayed = false, -- The radar's power, the system simulates the radar unit powering up when the user clicks the -- power button on the interface power = false, poweringUp = false, -- Whether or not the radar should be hidden, e.g. the display is active but the player then steps -- out of their vehicle hidden = false, -- These are the settings that are used in the operator menu settings = { -- Should the system calculate and display faster targets ["fastDisplay"] = CONFIG.menuDefaults["fastDisplay"], -- Sensitivity for each radar mode, this changes how far the antennas will detect vehicles ["same"] = CONFIG.menuDefaults["same"], ["opp"] = CONFIG.menuDefaults["opp"], -- The volume of the audible beep ["beep"] = CONFIG.menuDefaults["beep"], -- The volume of the verbal lock confirmation ["voice"] = CONFIG.menuDefaults["voice"], -- The volume of the plate reader audio ["plateAudio"] = CONFIG.menuDefaults["plateAudio"], -- The speed unit used in conversions ["speedType"] = CONFIG.menuDefaults["speedType"], -- The state of automatic speed locking ["fastLock"] = CONFIG.menuDefaults["fastLock"], -- The speed limit for automatic speed locking ["fastLimit"] = CONFIG.menuDefaults["fastLimit"] }, -- These 3 variables are for the in-radar menu that can be accessed through the remote control, the menuOptions table -- stores all of the information about each of the settings the user can change menuActive = false, currentOptionIndex = 1, menuOptions = { { displayText = { "¦¦¦", "FAS" }, optionsText = { "On¦", "Off" }, options = { true, false }, optionIndex = -1, settingText = "fastDisplay" }, { displayText = { "¦SL", "SEn" }, optionsText = { "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "same" }, { displayText = { "¦OP", "SEn" }, optionsText = { "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "opp" }, { displayText = { "bEE", "P¦¦" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "beep" }, { displayText = { "VOI", "CE¦" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "voice" }, { displayText = { "PLt", "AUd" }, optionsText = { "Off", "¦1¦", "¦2¦", "¦3¦", "¦4¦", "¦5¦" }, options = { 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 }, optionIndex = -1, settingText = "plateAudio" }, { displayText = { "Uni", "tS¦" }, optionsText = { "USA", "INT" }, options = { "mph", "kmh" }, optionIndex = -1, settingText = "speedType" } }, -- Player's vehicle speed, mainly used in the dynamic thread wait update patrolSpeed = 0, -- Antennas, this table contains all of the data needed for operation of the front and rear antennas antennas = { -- Variables for the front antenna [ "front" ] = { xmit = false, -- Whether the antenna is transmitting or in hold mode = 0, -- Current antenna mode, 0 = none, 1 = same, 2 = opp, 3 = same and opp speed = 0, -- Speed of the vehicle caught by the front antenna dir = nil, -- Direction the caught vehicle is going, 0 = towards, 1 = away fastSpeed = 0, -- Speed of the fastest vehicle caught by the front antenna fastDir = nil, -- Direction the fastest vehicle is going speedLocked = false, -- A speed has been locked for this antenna lockedSpeed = nil, -- The locked speed lockedDir = nil, -- The direction of the vehicle that was locked lockedType = nil -- The locked type, 1 = strongest, 2 = fastest }, -- Variables for the rear antenna [ "rear" ] = { xmit = false, -- Whether the antenna is transmitting or in hold mode = 0, -- Current antenna mode, 0 = none, 1 = same, 2 = opp, 3 = same and opp speed = 0, -- Speed of the vehicle caught by the front antenna dir = nil, -- Direction the caught vehicle is going, 0 = towards, 1 = away fastSpeed = 0, -- Speed of the fastest vehicle caught by the front antenna fastDir = nil, -- Direction the fastest vehicle is going speedLocked = false, -- A speed has been locked for this antenna lockedSpeed = nil, -- The locked speed lockedDir = nil, -- The direction of the vehicle that was locked lockedType = nil -- The locked type, 1 = strongest, 2 = fastest } }, -- The maximum distance that the radar system's ray traces can go, changing this will change the max -- distance in-game, but I wouldn't really put it more than 500.0 maxCheckDist = 350.0, -- Cached dynamic vehicle sphere sizes, automatically populated when the system is running sphereSizes = {}, -- Table to store tables for hit entities of captured vehicles capturedVehicles = {}, -- Table to store the valid vehicle models validVehicles = {}, -- The current vehicle data for display activeVehicles = {}, -- Vehicle pool, automatically populated when the system is running, holds all of the current -- vehicle IDs for the player using entity enumeration (see cl_utils.lua) vehiclePool = {}, -- Ray trace state, this is used so the radar system doesn't initiate another set of ray traces until -- the current set has finished rayTraceState = 0, -- Number of ray traces, automatically cached when the system first runs numberOfRays = 0, -- The wait time for the ray trace system, this changes dynamically based on if the player's vehicle is stationary -- or not threadWaitTime = 500, -- Key lock, when true, prevents any of the radar's key events from working, like the ELS key lock keyLock = false } -- Speed conversion values RADAR.speedConversions = { ["mph"] = 2.236936, ["kmh"] = 3.6 } -- These vectors are used in the custom ray tracing system RADAR.rayTraces = { { startVec = { x = 0.0 }, endVec = { x = 0.0, y = 0.0 }, rayType = "same" }, { startVec = { x = -5.0 }, endVec = { x = -5.0, y = 0.0 }, rayType = "same" }, { startVec = { x = 5.0 }, endVec = { x = 5.0, y = 0.0 }, rayType = "same" }, { startVec = { x = -10.0 }, endVec = { x = -10.0, y = 0.0 }, rayType = "opp" }, { startVec = { x = -17.0 }, endVec = { x = -17.0, y = 0.0 }, rayType = "opp" } } -- Each of these are used for sorting the captured vehicle data, the 'strongest' filter is used for the main -- target window of each antenna, whereas the 'fastest' filter is used for the fast target window of each antenna RADAR.sorting = { strongest = function( a, b ) return a.size > b.size end, fastest = function( a, b ) return a.speed > b.speed end } --[[---------------------------------------------------------------------------------- Radar essentials functions ----------------------------------------------------------------------------------]]-- -- Returns if the radar's power is on or off function RADAR:IsPowerOn() return self.vars.power end -- Returns if the radar system is powering up, the powering up stage only takes 2 seconds function RADAR:IsPoweringUp() return self.vars.poweringUp end -- Allows the powering up state variable to be set function RADAR:SetPoweringUpState( state ) self.vars.poweringUp = state end -- Toggles the radar power function RADAR:SetPowerState( state, instantOverride ) local currentState = self:IsPowerOn() -- Only power up if the system is not already powering up if ( not self:IsPoweringUp() and currentState ~= state ) then -- Toggle the power variable self.vars.power = state -- Send the NUI message to toggle the power SendNUIMessage( { _type = "radarPower", state = state, override = instantOverride, fast = self:IsFastDisplayEnabled() } ) -- Power is now turned on if ( self:IsPowerOn() ) then -- Also make sure the operator menu is inactive self:SetMenuState( false ) -- Only do the power up simulation if allowed if ( not instantOverride ) then -- Tell the system the radar is 'powering up' self:SetPoweringUpState( true ) -- Set a 2 second countdown Citizen.SetTimeout( 2000, function() -- Tell the system the radar has 'powered up' self:SetPoweringUpState( false ) -- Let the UI side know the system has loaded SendNUIMessage( { _type = "poweredUp", fast = self:IsFastDisplayEnabled() } ) end ) end else -- If the system is being turned off, then we reset the antennas self:ResetAntenna( "front" ) self:ResetAntenna( "rear" ) end end end -- Toggles the display state of the radar system function RADAR:ToggleDisplayState() -- Toggle the display variable self.vars.displayed = not self.vars.displayed -- Send the toggle message to the NUI side SendNUIMessage( { _type = "setRadarDisplayState", state = self:GetDisplayState() } ) end -- Gets the display state function RADAR:GetDisplayState() return self.vars.displayed end -- Return the state of the fastDisplay setting, short hand direct way to check if the fast system is enabled function RADAR:IsFastDisplayEnabled() return self:GetSettingValue( "fastDisplay" ) end -- Returns if either of the antennas are transmitting function RADAR:IsEitherAntennaOn() return self:IsAntennaTransmitting( "front" ) or self:IsAntennaTransmitting( "rear" ) end -- Sends an update to the NUI side with the current state of the antennas and if the fast system is enabled function RADAR:SendSettingUpdate() -- Create a table to store the setting information for the antennas local antennas = {} -- Iterate through each antenna and grab the relevant information for ant in UTIL:Values( { "front", "rear" } ) do antennas[ant] = {} antennas[ant].xmit = self:IsAntennaTransmitting( ant ) antennas[ant].mode = self:GetAntennaMode( ant ) antennas[ant].speedLocked = self:IsAntennaSpeedLocked( ant ) antennas[ant].fast = self:ShouldFastBeDisplayed( ant ) end -- Send a message to the NUI side with the current state of the antennas SendNUIMessage( { _type = "settingUpdate", antennaData = antennas } ) end -- Returns if a main task can be performed -- A main task such as the ray trace thread should only run if the radar's power is on, the system is not in the -- process of powering up, and the operator menu is not open function RADAR:CanPerformMainTask() return self:IsPowerOn() and not self:IsPoweringUp() and not self:IsMenuOpen() end -- Returns/sets what the dynamic thread wait time is function RADAR:GetThreadWaitTime() return self.vars.threadWaitTime end function RADAR:SetThreadWaitTime( time ) self.vars.threadWaitTime = time end -- Returns/sets the radr's display hidden state function RADAR:GetDisplayHidden() return self.vars.hidden end function RADAR:SetDisplayHidden( state ) self.vars.hidden = state end -- Opens the remote only if the pause menu is not open and the player's vehicle state is valid, as the -- passenger can also open the remote, we check the config variable as well. function RADAR:OpenRemote() if ( not IsPauseMenuActive() and PLY:CanViewRadar() ) then -- Get the remote open state from the other player local openByOtherPly = SYNC:IsRemoteAlreadyOpen( PLY:GetOtherPed() ) -- Check that the remote can be opened if ( not openByOtherPly ) then -- Tell the NUI side to open the remote SendNUIMessage( { _type = "openRemote" } ) SYNC:SetRemoteOpenState( true ) if ( CONFIG.allow_quick_start_video ) then -- Display the new user popup if we can local show = GetResourceKvpInt( "wk_wars2x_new_user" ) if ( show == 0 ) then SendNUIMessage( { _type = "showNewUser" } ) end end -- Bring focus to the NUI side SetNuiFocus( true, true ) else UTIL:Notify( "Another player already has the remote open." ) end end end -- Event to open the remote RegisterNetEvent( "wk:openRemote" ) AddEventHandler( "wk:openRemote", function() RADAR:OpenRemote() end ) -- Returns if the passenger can view the radar too function RADAR:IsPassengerViewAllowed() return CONFIG.allow_passenger_view end -- Returns if the passenger can control the radar and plate reader, reliant on the passenger being -- able to view the radar and plate reader too function RADAR:IsPassengerControlAllowed() return CONFIG.allow_passenger_view and CONFIG.allow_passenger_control end -- Returns if we only auto lock vehicle speeds if said vehicle is a player function RADAR:OnlyLockFastPlayers() return CONFIG.only_lock_players end -- Returns if the fast limit option should be available for the radar function RADAR:IsFastLimitAllowed() return CONFIG.allow_fast_limit end -- Only create the functions if the fast limit config option is enabled if ( RADAR:IsFastLimitAllowed() ) then -- Adds settings into the radar's variables for when the allow_fast_limit variable is true function RADAR:CreateFastLimitConfig() -- Create the options for the menu local fastOptions = { { displayText = { "FAS", "Loc" }, optionsText = { "On¦", "Off" }, options = { true, false }, optionIndex = 2, settingText = "fastLock" }, { displayText = { "FAS", "SPd" }, optionsText = {}, options = {}, optionIndex = 12, settingText = "fastLimit" } } -- Iterate from 5 to 200 in steps of 5 and insert into the fast limit option for i = 5, 200, 5 do local text = UTIL:FormatSpeed( i ) table.insert( fastOptions[2].optionsText, text ) table.insert( fastOptions[2].options, i ) end -- Add the fast options to the main menu options table if ( CONFIG.fast_limit_first_in_menu ) then table.insert( self.vars.menuOptions, 1, fastOptions[2] ) --FasSpd table.insert( self.vars.menuOptions, 2, fastOptions[1] ) --FasLoc else table.insert( self.vars.menuOptions, fastOptions[1] ) --FasLoc table.insert( self.vars.menuOptions, fastOptions[2] ) --FasSpd end end -- Returns the numerical fast limit function RADAR:GetFastLimit() return self:GetSettingValue( "fastLimit" ) end -- Returns if the fast lock menu option is on or off function RADAR:IsFastLockEnabled() return self:GetSettingValue( "fastLock" ) end end -- Toggles the internal key lock state, which stops any of the radar's key binds from working function RADAR:ToggleKeyLock() -- Check the player state is valid if ( PLY:CanViewRadar() ) then -- Toggle the key lock variable self.vars.keyLock = not self.vars.keyLock -- Tell the NUI side to display the key lock message SendNUIMessage( { _type = "displayKeyLock", state = self:GetKeyLockState() } ) end end -- Returns the key lock state function RADAR:GetKeyLockState() return self.vars.keyLock end --[[---------------------------------------------------------------------------------- Radar menu functions ----------------------------------------------------------------------------------]]-- -- Sets the menu state to the given state function RADAR:SetMenuState( state ) -- Make sure that the radar's power is on if ( self:IsPowerOn() ) then -- Set the menuActive variable to the given state self.vars.menuActive = state -- If we are opening the menu, make sure the first item is displayed if ( state ) then self.vars.currentOptionIndex = 1 end end end -- Closes the operator menu function RADAR:CloseMenu( playAudio ) -- Set the internal menu state to be closed (false) RADAR:SetMenuState( false ) -- Send a setting update to the NUI side RADAR:SendSettingUpdate() -- Play a menu done beep if ( playAudio or playAudio == nil ) then SendNUIMessage( { _type = "audio", name = "done", vol = RADAR:GetSettingValue( "beep" ) } ) end -- Save the operator menu values local omData = json.encode( RADAR.vars.settings ) SetResourceKvp( "wk_wars2x_om_data", omData ) -- Send the operator menu to the passenger if allowed if ( self:IsPassengerViewAllowed() ) then local updatedOMData = self:GetOMTableData() SYNC:SendUpdatedOMData( updatedOMData ) end end -- Returns if the operator menu is open function RADAR:IsMenuOpen() return self.vars.menuActive end -- This function changes the menu index variable so the user can iterate through the options in the operator menu function RADAR:ChangeMenuIndex() -- Create a temporary variable of the current menu index plus 1 local temp = self.vars.currentOptionIndex + 1 -- If the temporary value is larger than how many options there are, set it to 1, this way the menu -- loops back round to the start of the menu if ( temp > #self.vars.menuOptions ) then temp = 1 end -- Set the menu index variable to the temporary value we created self.vars.currentOptionIndex = temp -- Call the function to send an update to the NUI side self:SendMenuUpdate() end -- Returns the option table of the current menu index function RADAR:GetMenuOptionTable() return self.vars.menuOptions[self.vars.currentOptionIndex] end -- Changes the index for an individual option -- E.g. { "On" "Off" }, index = 2 would be "Off" function RADAR:SetMenuOptionIndex( index ) self.vars.menuOptions[self.vars.currentOptionIndex].optionIndex = index end -- Returns the option value for the current option function RADAR:GetMenuOptionValue() local opt = self:GetMenuOptionTable() local index = opt.optionIndex return opt.options[index] end -- This function is similar to RADAR:ChangeMenuIndex() but allows for iterating forward and backward through options function RADAR:ChangeMenuOption( dir ) -- Get the option table of the currently selected option local opt = self:GetMenuOptionTable() -- Get the current option index of the selected option local index = opt.optionIndex -- Cache the size of this setting's options table local size = #opt.options -- As the XMIT/HOLD buttons are used for changing the option values, we have to check which button is being pressed if ( dir == "front" ) then index = index + 1 if ( index > size ) then index = 1 end elseif ( dir == "rear" ) then index = index - 1 if ( index < 1 ) then index = size end end -- Update the option's index self:SetMenuOptionIndex( index ) -- Change the value of the setting in the main RADAR.vars.settings table self:SetSettingValue( opt.settingText, self:GetMenuOptionValue() ) -- Call the function to send an update to the NUI side self:SendMenuUpdate() end -- Returns what text should be displayed in the boxes for the current option -- E.g. "¦SL" "SEN" function RADAR:GetMenuOptionDisplayText() return self:GetMenuOptionTable().displayText end -- Returns the option text of the currently selected setting function RADAR:GetMenuOptionText() local opt = self:GetMenuOptionTable() return opt.optionsText[opt.optionIndex] end -- Sends a message to the NUI side with updated information on what should be displayed for the menu function RADAR:SendMenuUpdate() SendNUIMessage( { _type = "menu", text = self:GetMenuOptionDisplayText(), option = self:GetMenuOptionText() } ) end -- Used to set individual settings within RADAR.vars.settings, as all of the settings use string keys, using this -- function makes updating settings easier function RADAR:SetSettingValue( setting, value ) -- Make sure that we're not trying to set a nil value for the setting if ( value ~= nil ) then -- Set the setting's value self.vars.settings[setting] = value -- If the setting that's being updated is same or opp, then we update the end coordinates for the ray tracer if ( setting == "same" or setting == "opp" ) then self:UpdateRayEndCoords() end end end -- Returns the value of the given setting function RADAR:GetSettingValue( setting ) return self.vars.settings[setting] end -- Attempts to load the saved operator menu data function RADAR:LoadOMData() UTIL:Log( "Attempting to load saved operator menu data." ) -- Try and get the data local rawData = GetResourceKvpString( "wk_wars2x_om_data" ) -- If the data exists, decode it and replace the operator menu table if ( rawData ~= nil ) then local omData = json.decode( rawData ) self.vars.settings = omData UTIL:Log( "Saved operator menu data loaded!" ) else UTIL:Log( "Could not find any saved operator menu data." ) end end -- Updates the operator menu option indexes, as the default menu values can be changed in the config, we -- need to update the indexes otherwise the menu will display the wrong values function RADAR:UpdateOptionIndexes( loadSaved ) if ( loadSaved ) then self:LoadOMData() end -- Iterate through each of the internal settings for k, v in pairs( self.vars.settings ) do -- Iterate through all of the menu options for i, t in pairs( self.vars.menuOptions ) do -- If the current menu option is the same as the current setting if ( t.settingText == k ) then -- Iterate through the option values of the current menu option for oi, ov in pairs( t.options ) do -- If the value of the current option set in the config matches the current value of -- the option value, then we update the option index variable if ( v == ov ) then t.optionIndex = oi end end end end end end --[[---------------------------------------------------------------------------------- Radar basics functions ----------------------------------------------------------------------------------]]-- -- Returns the patrol speed value stored function RADAR:GetPatrolSpeed() return self.vars.patrolSpeed end -- Returns the current vehicle pool function RADAR:GetVehiclePool() return self.vars.vehiclePool end -- Returns the maximum distance a ray trace can go function RADAR:GetMaxCheckDist() return self.vars.maxCheckDist end -- Returns the table sorting function 'strongest' function RADAR:GetStrongestSortFunc() return self.sorting.strongest end -- Returns the table sorting function 'fastest' function RADAR:GetFastestSortFunc() return self.sorting.fastest end -- Sets the patrol speed to a formatted version of the given number function RADAR:SetPatrolSpeed( speed ) if ( type( speed ) == "number" ) then self.vars.patrolSpeed = self:GetVehSpeedConverted( speed ) end end -- Sets the vehicle pool to the given value if it's a table function RADAR:SetVehiclePool( pool ) if ( type( pool ) == "table" ) then self.vars.vehiclePool = pool end end --[[---------------------------------------------------------------------------------- Radar ray trace functions ----------------------------------------------------------------------------------]]-- -- Returns what the current ray trace state is function RADAR:GetRayTraceState() return self.vars.rayTraceState end -- Caches the number of ray traces in RADAR.rayTraces function RADAR:CacheNumRays() self.vars.numberOfRays = #self.rayTraces end -- Returns the number of ray traces the system has function RADAR:GetNumOfRays() return self.vars.numberOfRays end -- Increases the system's ray trace state ny 1 function RADAR:IncreaseRayTraceState() self.vars.rayTraceState = self.vars.rayTraceState + 1 end -- Resets the ray trace state to 0 function RADAR:ResetRayTraceState() self.vars.rayTraceState = 0 end -- This function is used to determine if a sphere intersect is in front or behind the player's vehicle, the -- sphere intersect calculation has a 'tProj' value that is a line from the centre of the sphere that goes onto -- the line being traced. This value will either be positive or negative and can be used to work out the -- relative position of a point. function RADAR:GetIntersectedVehIsFrontOrRear( t ) if ( t > 8.0 ) then return 1 -- vehicle is in front elseif ( t < -8.0 ) then return -1 -- vehicle is behind end return 0 -- vehicle is next to self end -- This function is used to check if a line going from point A to B intersects with a given sphere, it's used in -- the radar system to check if the patrol vehicle can detect any vehicles. As the default ray trace system in GTA -- cannot detect vehicles beyond 40~ units, my system acts as a replacement that allows the detection of vehicles -- much further away (400+ units). Also, as my system uses sphere intersections, each sphere can have a different -- radius, which means that larger vehicles can have larger spheres, and smaller vehicles can have smaller spheres. function RADAR:GetLineHitsSphereAndDir( c, radius, rs, re ) -- Take the vector3's and turn them into vector2's, this way all of the calculations below are for an -- infinite cylinder rather than a sphere, which also means that vehicles can be detected even when on -- an incline! local rayStart = vector2( rs.x, rs.y ) local rayEnd = vector2( re.x, re.y ) local centre = vector2( c.x, c.y ) -- First we get the normalised ray, this way we then know the direction the ray is going local rayNorm = norm( rayEnd - rayStart ) -- Then we calculate the ray from the start point to the centre position of the sphere local rayToCentre = centre - rayStart -- Now that we have the ray to the centre of the sphere, and the normalised ray direction, we -- can calculate the shortest point from the centre of the sphere onto the ray itself. This -- would then give us the opposite side of the right angled triangle. All of the resulting -- values are also in squared form, as performing square root functions is slower. local tProj = dot( rayToCentre, rayNorm ) local oppLenSqr = dot( rayToCentre, rayToCentre ) - ( tProj * tProj ) -- Square the radius local radiusSqr = radius * radius -- Calculate the distance of the ray trace to make sure we only return valid results if the trace -- is actually within the distance local rayDist = #( rayEnd - rayStart ) local distToCentre = #( rayStart - centre ) - ( radius * 2 ) -- Now all we have to do is compare the squared opposite length and the radius squared, this -- will then tell us if the ray intersects with the sphere. if ( oppLenSqr < radiusSqr and not ( distToCentre > rayDist ) ) then return true, self:GetIntersectedVehIsFrontOrRear( tProj ) end return false, nil end -- This function is used to check if the target vehicle is in the same general traffic flow as the player's vehicle -- is sitting. If the angle is too great, then the radar would have an incorrect return for the speed. function RADAR:IsVehicleInTraffic( tgtVeh, relPos ) local tgtHdg = GetEntityHeading( tgtVeh ) local plyHdg = GetEntityHeading( PLY.veh ) -- Work out the heading difference, but also take into account extreme opposites (e.g. 5deg and 350deg) local hdgDiff = math.abs( ( plyHdg - tgtHdg + 180 ) % 360 - 180 ) if ( relPos == 1 and hdgDiff > 45 and hdgDiff < 135 ) then return false elseif ( relPos == -1 and hdgDiff > 45 and ( hdgDiff < 135 or hdgDiff > 215 ) ) then return false end return true end -- This function is the main custom ray trace function, it performs most of the major tasks for checking a vehicle -- is valid and should be tested. It also makes use of the LOS native to make sure that we can only trace a vehicle -- if actually nas a direct line of sight with the player's vehicle, this way we don't pick up vehicles behind walls -- for example. It then creates a dynamic sphere for the vehicle based on the actual model dimensions of it, adds a -- small bit of realism, as real radars usually return the strongest target speed. function RADAR:ShootCustomRay( plyVeh, veh, s, e ) -- Get the world coordinates of the target vehicle local pos = GetEntityCoords( veh ) -- Calculate the distance between the target vehicle and the start point of the ray trace, note how we don't -- use GetDistanceBetweenCoords or Vdist, the method below still returns the same result with less cpu time local dist = #( pos - s ) -- We only perform a trace on the target vehicle if it exists, isn't the player's vehicle, and the distance is -- less than the max distance defined by the system if ( DoesEntityExist( veh ) and veh ~= plyVeh and dist < self:GetMaxCheckDist() ) then -- Get the speed of the target vehicle local entSpeed = GetEntitySpeed( veh ) -- Check that the target vehicle is within the line of sight of the player's vehicle local visible = HasEntityClearLosToEntity( plyVeh, veh, 15 ) -- 13 seems okay, 15 too (doesn't grab ents through ents) -- Get the pitch of the player's vehicle local pitch = GetEntityPitch( plyVeh ) -- Now we check that the target vehicle is moving and is visible if ( entSpeed > 0.1 and ( pitch > -35 and pitch < 35 ) and visible ) then -- Get the dynamic radius as well as the size of the target vehicle local radius, size = self:GetDynamicRadius( veh ) -- Check that the trace line intersects with the target vehicle's sphere local hit, relPos = self:GetLineHitsSphereAndDir( pos, radius, s, e ) -- Return all of the information if the vehicle was hit and is in the flow of traffic if ( hit and self:IsVehicleInTraffic( veh, relPos ) ) then return true, relPos, dist, entSpeed, size end end end -- Return a whole lot of nothing return false, nil, nil, nil, nil end -- This function is used to gather all of the data on vehicles that have been hit by the given trace line, when -- a vehicle is hit, all of the information about that vehicle is put into a keyless table which is then inserted -- into a main table. When the loop has finished, the function then returns the table with all of the data. function RADAR:GetVehsHitByRay( ownVeh, vehs, s, e ) -- Create the table that will be used to store all of the results local caughtVehs = {} -- Set the variable to say if there has been data collected local hasData = false -- Iterate through all of the vehicles for _, veh in pairs( vehs ) do -- Shoot a custom ray trace to see if the vehicle gets hit local hit, relativePos, distance, speed, size = self:ShootCustomRay( ownVeh, veh, s, e ) -- If the vehicle is hit, then we create a table containing all of the information if ( hit ) then -- Create the table to store the data local vehData = {} vehData.veh = veh vehData.relPos = relativePos vehData.dist = distance vehData.speed = speed vehData.size = size -- Insert the table into the caught vehicles table table.insert( caughtVehs, vehData ) -- Change the has data variable to true, this way the table will be returned hasData = true end end -- If the caughtVehs table actually has data, then return it if ( hasData ) then return caughtVehs end end -- This function is used to gather all of the vehicles hit by a given line trace, and then insert it into the -- internal captured vehicles table. function RADAR:CreateRayThread( vehs, from, startX, endX, endY, rayType ) -- Get the start and end points for the ray trace based on the given start and end coordinates local startPoint = GetOffsetFromEntityInWorldCoords( from, startX, 0.0, 0.0 ) local endPoint = GetOffsetFromEntityInWorldCoords( from, endX, endY, 0.0 ) -- Get all of the vehicles hit by the ray local hitVehs = self:GetVehsHitByRay( from, vehs, startPoint, endPoint ) -- Insert the captured vehicle data and pass the ray type too self:InsertCapturedVehicleData( hitVehs, rayType ) -- Increase the ray trace state self:IncreaseRayTraceState() end -- This function iterates through each of the traces defined in RADAR.rayTraces and creates a 'thread' for -- them, passing along all of the vehicle pool data and the player's vehicle function RADAR:CreateRayThreads( ownVeh, vehicles ) for _, v in pairs( self.rayTraces ) do self:CreateRayThread( vehicles, ownVeh, v.startVec.x, v.endVec.x, v.endVec.y, v.rayType ) end end -- When the user changes either the same lane or opp lane sensitivity from within the operator menu, this function -- is then called to update the end coordinates for all of the traces function RADAR:UpdateRayEndCoords() for _, v in pairs( self.rayTraces ) do -- Calculate what the new end coordinate should be local endY = self:GetSettingValue( v.rayType ) * self:GetMaxCheckDist() -- Update the end Y coordinate in the traces table v.endVec.y = endY end end --[[---------------------------------------------------------------------------------- Radar antenna functions ----------------------------------------------------------------------------------]]-- -- Toggles the state of the given antenna between hold and transmitting, only works if the radar's power is -- on. Also runs a callback function when present. function RADAR:ToggleAntenna( ant ) -- Check power is on if ( self:IsPowerOn() ) then -- Toggle the given antennas state self.vars.antennas[ant].xmit = not self.vars.antennas[ant].xmit -- Update the interface with the new antenna transmit state SendNUIMessage( { _type = "antennaXmit", ant = ant, on = self:IsAntennaTransmitting( ant ) } ) -- Play some audio specific to the transmit state SendNUIMessage( { _type = "audio", name = self:IsAntennaTransmitting( ant ) and "xmit_on" or "xmit_off", vol = self:GetSettingValue( "beep" ) } ) end end -- Returns if the given antenna is transmitting function RADAR:IsAntennaTransmitting( ant ) return self.vars.antennas[ant].xmit end -- Returns if the given relative position value is for the front or rear antenna function RADAR:GetAntennaTextFromNum( relPos ) if ( relPos == 1 ) then return "front" elseif ( relPos == -1 ) then return "rear" end end -- Returns the mode of the given antenna function RADAR:GetAntennaMode( ant ) return self.vars.antennas[ant].mode end -- Sets the mode of the given antenna if the mode is valid and the power is on. Also runs a callback function -- when present. function RADAR:SetAntennaMode( ant, mode ) -- Check the mode is actually a number, this is needed as the radar system relies on the mode to be -- a number to work if ( type( mode ) == "number" ) then -- Check the mode is in the valid range for modes, and that the power is on if ( mode >= 0 and mode <= 3 and self:IsPowerOn() ) then -- Update the mode for the antenna self.vars.antennas[ant].mode = mode -- Update the interface with the new mode SendNUIMessage( { _type = "antennaMode", ant = ant, mode = mode } ) -- Play a beep SendNUIMessage( { _type = "audio", name = "beep", vol = self:GetSettingValue( "beep" ) } ) end end end -- Returns/sets the speed for the given antenna function RADAR:GetAntennaSpeed( ant ) return self.vars.antennas[ant].speed end function RADAR:SetAntennaSpeed( ant, speed ) self.vars.antennas[ant].speed = speed end -- Returns/sets the direction for the given antenna function RADAR:GetAntennaDir( ant ) return self.vars.antennas[ant].dir end function RADAR:SetAntennaDir( ant, dir ) self.vars.antennas[ant].dir = dir end -- Sets the speed and direction in one go function RADAR:SetAntennaData( ant, speed, dir ) self:SetAntennaSpeed( ant, speed ) self:SetAntennaDir( ant, dir ) end -- Returns/sets the fast speed for the given antenna function RADAR:GetAntennaFastSpeed( ant ) return self.vars.antennas[ant].fastSpeed end function RADAR:SetAntennaFastSpeed( ant, speed ) self.vars.antennas[ant].fastSpeed = speed end -- Returns/sets the fast direction for the given antenna function RADAR:GetAntennaFastDir( ant ) return self.vars.antennas[ant].fastDir end function RADAR:SetAntennaFastDir( ant, dir ) self.vars.antennas[ant].fastDir = dir end -- Sets the fast speed and direction in one go function RADAR:SetAntennaFastData( ant, speed, dir ) self:SetAntennaFastSpeed( ant, speed ) self:SetAntennaFastDir( ant, dir ) end -- Returns if the stored speed for the given antenna is valid function RADAR:DoesAntennaHaveValidData( ant ) return self:GetAntennaSpeed( ant ) ~= nil end -- Returns if the stored fast speed for the given antenna is valid function RADAR:DoesAntennaHaveValidFastData( ant ) return self:GetAntennaFastSpeed( ant ) ~= nil end -- Returns if the fast label should be displayed function RADAR:ShouldFastBeDisplayed( ant ) if ( self:IsAntennaSpeedLocked( ant ) ) then return self:GetAntennaLockedType( ant ) == 2 else return self:IsFastDisplayEnabled() end end -- Returns if the given antenna has a locked speed function RADAR:IsAntennaSpeedLocked( ant ) return self.vars.antennas[ant].speedLocked end -- Sets the state of speed lock for the given antenna to the given state function RADAR:SetAntennaSpeedIsLocked( ant, state ) self.vars.antennas[ant].speedLocked = state end -- Sets a speed and direction to be locked in for the given antenna function RADAR:SetAntennaSpeedLock( ant, speed, dir, lockType, playAudio ) -- Check that the passed speed and direction are actually valid if ( speed ~= nil and dir ~= nil and lockType ~= nil ) then -- Set the locked speed and direction to the passed values self.vars.antennas[ant].lockedSpeed = speed self.vars.antennas[ant].lockedDir = dir self.vars.antennas[ant].lockedType = lockType -- Tell the system that a speed has been locked for the given antenna self:SetAntennaSpeedIsLocked( ant, true ) if ( playAudio ) then -- Send a message to the NUI side to play the beep sound with the current volume setting SendNUIMessage( { _type = "audio", name = "beep", vol = self:GetSettingValue( "beep" ) } ) -- Send a message to the NUI side to play the lock audio with the current voice volume setting SendNUIMessage( { _type = "lockAudio", ant = ant, dir = dir, vol = self:GetSettingValue( "voice" ) } ) end -- Great Scott! if ( speed == "¦88" and self:GetSettingValue( "speedType" ) == "mph" ) then math.randomseed( GetGameTimer() ) local chance = math.random() -- 15% chance if ( chance <= 0.15 ) then SendNUIMessage( { _type = "audio", name = "speed_alert", vol = self:GetSettingValue( "beep" ) } ) end end end end -- Returns the locked speed for the given antenna function RADAR:GetAntennaLockedSpeed( ant ) return self.vars.antennas[ant].lockedSpeed end -- Returns the locked direction for the given antenna function RADAR:GetAntennaLockedDir( ant ) return self.vars.antennas[ant].lockedDir end -- Returns the lock type for the given antenna function RADAR:GetAntennaLockedType( ant ) return self.vars.antennas[ant].lockedType end -- Resets the speed lock info to do with the given antenna function RADAR:ResetAntennaSpeedLock( ant ) -- Blank the locked speed and direction self.vars.antennas[ant].lockedSpeed = nil self.vars.antennas[ant].lockedDir = nil self.vars.antennas[ant].lockedType = nil -- Set the locked state to false self:SetAntennaSpeedIsLocked( ant, false ) end -- When the user presses the speed lock key for either antenna, this function is called to get the -- necessary information from the antenna, and then lock it into the display function RADAR:LockAntennaSpeed( ant, override, lockRegardless ) -- Only lock a speed if the antenna is on and the UI is displayed if ( self:IsPowerOn() and ( ( self:GetDisplayState() and not self:GetDisplayHidden() ) or lockRegardless ) and self:IsAntennaTransmitting( ant ) ) then -- Used to determine whether or not to play the audio and update the display. This is mainly for the passenger -- control system, as in theory one player could be in the operator menu, and the other player could lock a speed. local isMenuOpen = self:IsMenuOpen() -- Check if the antenna doesn't have a locked speed, if it doesn't then we lock in the speed, otherwise we -- reset the lock if ( not self:IsAntennaSpeedLocked( ant ) ) then -- Here we check if the override parameter is valid, if so then we set the radar's speed data to the -- speed data provided in the override table. if ( override ~= nil ) then self:SetAntennaData( ant, override[1], override[2] ) self:SetAntennaFastData( ant, override[3], override[4] ) end -- This override parameter is used for the passenger control system, as the speeds displayed on the -- recipients display can't be trusted. When the player who locks the speed triggers the sync, their -- speed data is collected and sent to the other player so that their speed data is overriden to be the same. override = override or { nil, nil, nil, nil } -- Set up a temporary table with 3 nil values, this way if the system isn't able to get a speed or -- direction, the speed lock function won't work local data = { nil, nil, nil } -- As the lock system is based on which speed is displayed, we have to check if there is a speed in the -- fast box, if there is then we lock in the fast speed, otherwise we lock in the strongest speed if ( self:IsFastDisplayEnabled() and self:DoesAntennaHaveValidFastData( ant ) ) then data[1] = self:GetAntennaFastSpeed( ant ) data[2] = self:GetAntennaFastDir( ant ) data[3] = 2 else data[1] = self:GetAntennaSpeed( ant ) data[2] = self:GetAntennaDir( ant ) data[3] = 1 end -- Lock in the speed data for the antenna self:SetAntennaSpeedLock( ant, data[1], data[2], data[3], not isMenuOpen ) else self:ResetAntennaSpeedLock( ant ) end if ( not isMenuOpen ) then -- Send an NUI message to change the lock label, otherwise we'd have to wait until the next main loop SendNUIMessage( { _type = "antennaLock", ant = ant, state = self:IsAntennaSpeedLocked( ant ) } ) SendNUIMessage( { _type = "antennaFast", ant = ant, state = self:ShouldFastBeDisplayed( ant ) } ) end end end -- Resets an antenna, used when the system is turned off function RADAR:ResetAntenna( ant ) -- Overwrite default behaviour, this is because when the system is turned off, the temporary memory is -- technically reset, as the setter functions require either the radar power to be on or the antenna to -- be transmitting, this is the only way to reset the values self.vars.antennas[ant].xmit = false self.vars.antennas[ant].mode = 0 self:ResetAntennaSpeedLock( ant ) end -- Returns a table with the given antenna's speed data and directions function RADAR:GetAntennaDataPacket( ant ) return { self:GetAntennaSpeed( ant ), self:GetAntennaDir( ant ), self:GetAntennaFastSpeed( ant ), self:GetAntennaFastDir( ant ) } end --[[---------------------------------------------------------------------------------- Radar captured vehicle functions ----------------------------------------------------------------------------------]]-- -- Returns the captured vehicles table function RADAR:GetCapturedVehicles() return self.vars.capturedVehicles end -- Resets the captured vehicles table to an empty table function RADAR:ResetCapturedVehicles() self.vars.capturedVehicles = {} end -- Takes the vehicle data from RADAR:CreateRayThread() and puts it into the main captured vehicles table, along -- with the ray type for that vehicle data set (e.g. same or opp) function RADAR:InsertCapturedVehicleData( t, rt ) -- Make sure the table being passed is valid and not empty if ( type( t ) == "table" and not UTIL:IsTableEmpty( t ) ) then -- Iterate through the given table for _, v in pairs( t ) do -- Add the ray type to the current row v.rayType = rt -- Insert it into the main captured vehicles table table.insert( self.vars.capturedVehicles, v ) end end end --[[---------------------------------------------------------------------------------- Radar dynamic sphere radius functions ----------------------------------------------------------------------------------]]-- -- Returns the dynamic sphere data for the given key if there is any function RADAR:GetDynamicDataValue( key ) return self.vars.sphereSizes[key] end -- Returns if dynamic sphere data exists for the given key function RADAR:DoesDynamicRadiusDataExist( key ) return self:GetDynamicDataValue( key ) ~= nil end -- Sets the dynamic sohere data for the given key to the given table function RADAR:SetDynamicRadiusKey( key, t ) self.vars.sphereSizes[key] = t end -- Inserts the given data into the dynamic spheres table, stores the radius and the actual summed up -- vehicle size. The key is just the model of a vehicle put into string form function RADAR:InsertDynamicRadiusData( key, radius, actualSize ) -- Check to make sure there is no data for the vehicle if ( self:GetDynamicDataValue( key ) == nil ) then -- Create a table to store the data in local data = {} -- Put the data into the temporary table data.radius = radius data.actualSize = actualSize -- Set the dynamic sphere data for the vehicle self:SetDynamicRadiusKey( key, data ) end end -- Returns the dynamic sphere data for the given vehicle function RADAR:GetRadiusData( key ) return self.vars.sphereSizes[key].radius, self.vars.sphereSizes[key].actualSize end -- This function is used to get the dynamic sphere data for a vehicle, if data already exists for the -- given vehicle, then the system just returns the already made data, otherwise the data gets created function RADAR:GetDynamicRadius( veh ) -- Get the model of the vehicle local mdl = GetEntityModel( veh ) -- Create a key based on the model local key = tostring( mdl ) -- Check to see if data already exists local dataExists = self:DoesDynamicRadiusDataExist( key ) -- If the data doesn't already exist, then we create it if ( not dataExists ) then -- Get the min and max points of the vehicle model local min, max = GetModelDimensions( mdl ) -- Calculate the size, as the min value is negative local size = max - min -- Get a numeric size which composes of the x, y, and z size combined local numericSize = size.x + size.y + size.z -- Get a dynamic radius for the given vehicle model that fits into the world of GTA local dynamicRadius = UTIL:Clamp( ( numericSize * numericSize ) / 12, 5.0, 11.0 ) -- Insert the newly created sphere data into the sphere data table self:InsertDynamicRadiusData( key, dynamicRadius, numericSize ) -- Return the data return dynamicRadius, numericSize end -- Return the stored data return self:GetRadiusData( key ) end --[[---------------------------------------------------------------------------------- Radar functions ----------------------------------------------------------------------------------]]-- -- Takes a GTA speed and converts it into the type defined by the user in the operator menu function RADAR:GetVehSpeedConverted( speed ) -- Get the speed unit from the settings local unit = self:GetSettingValue( "speedType" ) -- Return the coverted speed rounded to a whole number return UTIL:Round( speed * self.speedConversions[unit], 0 ) end -- Returns/sets the validity of the given vehicle model function RADAR:GetVehicleValidity( key ) return self.vars.validVehicles[key] end function RADAR:SetVehicleValidity( key, validity ) self.vars.validVehicles[key] = validity end -- Returns if vehicle validity data exists for the given vehicle model function RADAR:DoesVehicleValidityExist( key ) return self:GetVehicleValidity( key ) ~= nil end -- Returns if the given vehicle is valid, as we don't want the radar to detect boats, helicopters, or planes! function RADAR:IsVehicleValid( veh ) -- Get the model of the vehicle local mdl = GetEntityModel( veh ) -- Create a key based on the model local key = tostring( mdl ) -- Check if the vehicle model is valid local valid = self:GetVehicleValidity( key ) -- If the validity value hasn't been set for the vehicle model, then we do it now if ( valid == nil ) then -- If the model is not what we want, then set the validity to false if ( IsThisModelABoat( mdl ) or IsThisModelAHeli( mdl ) or IsThisModelAPlane( mdl ) ) then self:SetVehicleValidity( key, false ) return false else self:SetVehicleValidity( key, true ) return true end end return valid end -- Gathers all of the vehicles in the local area of the player function RADAR:GetAllVehicles() -- Create a temporary table local t = {} -- Iterate through vehicles for v in UTIL:EnumerateVehicles() do if ( self:IsVehicleValid( v ) ) then -- Insert the vehicle id into the temporary table table.insert( t, v ) end end -- Return the table return t end -- Used to check if an antennas mode fits with a ray type from the ray trace system function RADAR:CheckVehicleDataFitsMode( ant, rt ) -- Get the current mode value for the given antenna local mode = self:GetAntennaMode( ant ) -- Check that the given ray type matches up with the antenna's current mode if ( ( mode == 3 ) or ( mode == 1 and rt == "same" ) or ( mode == 2 and rt == "opp" ) ) then return true end -- Otherwise, return false as a last resort return false end -- This function is used to filter through the captured vehicles and work out what vehicles should be used for display -- on the radar interface function RADAR:GetVehiclesForAntenna() -- Create the vehs table to store the split up captured vehicle data local vehs = { ["front"] = {}, ["rear"] = {} } -- Create the results table to store the vehicle results, the first index is for the 'strongest' vehicle and the -- second index is for the 'fastest' vehicle local results = { ["front"] = { nil, nil }, ["rear"] = { nil, nil } } -- Loop through and split up the vehicles based on front and rear, this is simply because the actual system -- that gets all of the vehicles hit by the radar only has a relative position of either 1 or -1, which we -- then convert below into an antenna string! for ant in UTIL:Values( { "front", "rear" } ) do -- Check that the antenna is actually transmitting if ( self:IsAntennaTransmitting( ant ) ) then -- Iterate through the captured vehicles for k, v in pairs( self:GetCapturedVehicles() ) do -- Convert the relative position to antenna text local antText = self:GetAntennaTextFromNum( v.relPos ) -- Check the current vehicle's relative position is the same as the current antenna if ( ant == antText ) then -- Insert the vehicle into the table for the current antenna table.insert( vehs[ant], v ) end end -- As the radar is based on how the real Stalker DSR 2X works, we now sort the dataset by -- the 'strongest' (largest) target, this way the first result for the front and rear data -- will be the one that gets displayed in the target boxes. table.sort( vehs[ant], self:GetStrongestSortFunc() ) end end -- Now that we have all of the vehicles split into front and rear, we can iterate through both sets and get -- the strongest and fastest vehicle for display for ant in UTIL:Values( { "front", "rear" } ) do -- Check that the table for the current antenna is not empty if ( not UTIL:IsTableEmpty( vehs[ant] ) ) then -- Get the 'strongest' vehicle for the antenna for k, v in pairs( vehs[ant] ) do -- Check if the current vehicle item fits the mode set by the user if ( self:CheckVehicleDataFitsMode( ant, v.rayType ) ) then -- Set the result for the current antenna results[ant][1] = v break end end -- Here we get the vehicle for the fastest section, but only if the user has the fast mode enabled -- in the operator menu if ( self:IsFastDisplayEnabled() ) then -- Get the 'fastest' vehicle for the antenna table.sort( vehs[ant], self:GetFastestSortFunc() ) -- Create a temporary variable for the first result, reduces line length local temp = results[ant][1] -- Iterate through the vehicles for the current antenna for k, v in pairs( vehs[ant] ) do -- When we grab a vehicle for the fastest section, as it is like how the real system works, there are a few -- additional checks that have to be made if ( self:CheckVehicleDataFitsMode( ant, v.rayType ) and v.veh ~= temp.veh and v.size < temp.size and v.speed > temp.speed + 1.0 ) then -- Set the result for the current antenna results[ant][2] = v break end end end end end -- Return the results return { ["front"] = { results["front"][1], results["front"][2] }, ["rear"] = { results["rear"][1], results["rear"][2] } } end --[[---------------------------------------------------------------------------------- NUI callback ----------------------------------------------------------------------------------]]-- -- Runs when the "Toggle Display" button is pressed on the remote control RegisterNUICallback( "toggleRadarDisplay", function( data, cb ) -- Toggle the display state RADAR:ToggleDisplayState() cb( "ok" ) end ) -- Runs when the user presses the power button on the radar ui RegisterNUICallback( "togglePower", function( data, cb ) if ( PLY:CanControlRadar() ) then if ( not RADAR:IsPoweringUp() ) then -- Toggle the radar's power RADAR:SetPowerState( not RADAR:IsPowerOn(), false ) SYNC:SendPowerState( RADAR:IsPowerOn() ) end end cb( "ok" ) end ) -- Runs when the user presses the ESC or RMB when the remote is open RegisterNUICallback( "closeRemote", function( data, cb ) -- Remove focus to the NUI side SetNuiFocus( false, false ) if ( RADAR:IsMenuOpen() ) then RADAR:CloseMenu( false ) end SYNC:SetRemoteOpenState( false ) cb( "ok" ) end ) -- Runs when the user presses any of the antenna mode buttons on the remote RegisterNUICallback( "setAntennaMode", function( data, cb ) if ( PLY:CanControlRadar() ) then -- Only run the codw if the radar has power and is not powering up if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then -- As the mode buttons are used to exit the menu, we check for that if ( RADAR:IsMenuOpen() ) then RADAR:CloseMenu() else -- Change the mode for the designated antenna, pass along a callback which contains data from this NUI callback RADAR:SetAntennaMode( data.value, tonumber( data.mode ) ) -- Sync SYNC:SendAntennaMode( data.value, tonumber( data.mode ) ) end end end cb( "ok" ) end ) -- Runs when the user presses either of the XMIT/HOLD buttons on the remote RegisterNUICallback( "toggleAntenna", function( data, cb ) if ( PLY:CanControlRadar() ) then -- Only run the codw if the radar has power and is not powering up if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then -- As the xmit/hold buttons are used to change settings in the menu, we check for that if ( RADAR:IsMenuOpen() ) then -- Change the menu option based on which button is pressed RADAR:ChangeMenuOption( data.value ) -- Play a beep noise SendNUIMessage( { _type = "audio", name = "beep", vol = RADAR:GetSettingValue( "beep" ) } ) else -- Toggle the transmit state for the designated antenna, pass along a callback which contains data from this NUI callback RADAR:ToggleAntenna( data.value ) -- Sync SYNC:SendAntennaPowerState( RADAR:IsAntennaTransmitting( data.value ), data.value ) end end end cb( "ok" ) end ) -- Runs when the user presses the menu button on the remote control RegisterNUICallback( "menu", function( data, cb ) if ( PLY:CanControlRadar() ) then -- Only run the codw if the radar has power and is not powering up if ( RADAR:IsPowerOn() and not RADAR:IsPoweringUp() ) then -- As the menu button is a multipurpose button, we first check to see if the menu is already open if ( RADAR:IsMenuOpen() ) then -- As the menu is already open, we then iterate to the next option in the settings list RADAR:ChangeMenuIndex() else -- Set the menu state to open, which will prevent anything else within the radar from working RADAR:SetMenuState( true ) -- Send an update to the NUI side RADAR:SendMenuUpdate() end -- Play the standard audio beep SendNUIMessage( { _type = "audio", name = "beep", vol = RADAR:GetSettingValue( "beep" ) } ) end end cb( "ok" ) end ) -- Runs when the JavaScript side sends the UI data for saving RegisterNUICallback( "saveUiData", function( data, cb ) UTIL:Log( "Saving updated UI settings data." ) SetResourceKvp( "wk_wars2x_ui_data", json.encode( data ) ) cb( "ok" ) end ) -- Runs when the JavaScript side sends the quick start video has been watched RegisterNUICallback( "qsvWatched", function( data, cb ) SetResourceKvpInt( "wk_wars2x_new_user", 1 ) cb( "ok" ) end ) --[[---------------------------------------------------------------------------------- Main threads ----------------------------------------------------------------------------------]]-- -- Some people might not like the idea of the resource having a CPU MSEC over 0.10, but due to the functions -- and the way the whole radar system works, it will use over 0.10 a decent amount. In this function, we -- dynamically adjust the wait time in the main thread, so that when the player is driving their vehicle and -- moving, the system doesn't run as fast so as to use less CPU time. When they have their vehicle -- stationary, the system runs more often, which means that if a situation occurs such as a vehicle flying -- past them at a high rate of speed, the system will be able to pick it up as it is running faster. Also, as -- the user is stationary, if the system takes up an additional one or two frames per second, it won't really -- be noticeable. function RADAR:RunDynamicThreadWaitCheck() -- Get the speed of the local players vehicle local speed = self:GetPatrolSpeed() -- Check that the vehicle speed is less than 0.1 if ( speed < 0.1 ) then -- Change the thread wait time to 200 ms, the trace system will now run five times per second self:SetThreadWaitTime( 200 ) else -- Change the thread wait time to 500 ms, the trace system will now run two times a second self:SetThreadWaitTime( 500 ) end end -- Create the thread that will run the dynamic thread wait check, this check only runs every two seconds Citizen.CreateThread( function() while ( true ) do -- Run the function RADAR:RunDynamicThreadWaitCheck() -- Make the thread wait two seconds Citizen.Wait( 2000 ) end end ) -- This function handles the custom ray trace system that is used to gather all of the vehicles hit by -- the ray traces defined in RADAR.rayTraces. function RADAR:RunThreads() -- For the system to even run, the player needs to be sat in the driver's seat of a class 18 vehicle, the -- radar has to be visible and the power must be on, and either one of the antennas must be enabled. if ( PLY:CanViewRadar() and self:CanPerformMainTask() and self:IsEitherAntennaOn() ) then -- Before we create any of the custom ray trace threads, we need to make sure that the ray trace state -- is at zero, if it is not at zero, then it means the system is still currently tracing if ( self:GetRayTraceState() == 0 ) then -- Grab a copy of the vehicle pool local vehs = self:GetVehiclePool() -- Reset the main captured vehicles table self:ResetCapturedVehicles() -- Here we run the function that creates all of the main ray threads self:CreateRayThreads( PLY.veh, vehs ) -- Make the thread this function runs in wait the dynamic time defined by the system Citizen.Wait( self:GetThreadWaitTime() ) -- If the current ray trace state is the same as the total number of rays, then we reset the ray trace -- state back to 0 so the thread system can run again elseif ( self:GetRayTraceState() == self:GetNumOfRays() ) then -- Reset the ray trace state to 0 self:ResetRayTraceState() end end end -- Create the main thread that will run the threads function, the function itself is run every frame as the -- dynamic wait time is ran inside the function Citizen.CreateThread( function() while ( true ) do -- Run the function RADAR:RunThreads() -- Make the thread wait 0 ms Citizen.Wait( 0 ) end end ) -- This is the main function that runs and handles all information that is sent to the NUI side for display, all -- speed values are converted on the Lua side into a format that is displayable using the custom font on the NUI side function RADAR:Main() -- Only run any of the main code if all of the states are met, player in the driver's seat of a class 18 vehicle, and -- the system has to be able to perform main tasks if ( PLY:CanViewRadar() and self:CanPerformMainTask() ) then -- Create a table that will be used to store all of the data to be sent to the NUI side local data = {} -- Get the player's vehicle speed local entSpeed = GetEntitySpeed( PLY.veh ) -- Set the internal patrol speed to the speed obtained above, this is then used in the dynamic thread wait calculation self:SetPatrolSpeed( entSpeed ) -- Change what is displayed in the patrol speed box on the radar interface depending on if the players vehicle is -- stationary or moving if ( entSpeed == 0 ) then data.patrolSpeed = "¦[]" else local speed = self:GetVehSpeedConverted( entSpeed ) data.patrolSpeed = UTIL:FormatSpeed( speed ) end -- Get the vehicles to be displayed for the antenna, then we take the results from that and send the relevant -- information to the NUI side local av = self:GetVehiclesForAntenna() data.antennas = { ["front"] = nil, ["rear"] = nil } -- Iterate through the front and rear data and obtain the information to be displayed for ant in UTIL:Values( { "front", "rear" } ) do -- Check that the antenna is actually transmitting, no point in running all the checks below if the antenna is off if ( self:IsAntennaTransmitting( ant ) ) then -- Create a table for the current antenna to store the information data.antennas[ant] = {} -- When the system works out what vehicles to be used, both the "front" and "rear" keys have two items located -- at index 1 and 2. Index 1 stores the vehicle data for the antenna's 'strongest' vehicle, and index 2 stores -- the vehicle data for the 'fastest' vehicle. Here we iterate through both the indexes and just run checks to -- see if it is a particular type (e.g. if i % 2 == 0 then it's the 'fastest' vehicle) for i = 1, 2 do -- Create the table to store the speed and direction for this vehicle data data.antennas[ant][i] = { speed = "¦¦¦", dir = 0 } -- If the current iteration is the number 2 ('fastest') and there's a speed locked, grab the locked speed -- and direction if ( i == 2 and self:IsAntennaSpeedLocked( ant ) ) then data.antennas[ant][i].speed = self:GetAntennaLockedSpeed( ant ) data.antennas[ant][i].dir = self:GetAntennaLockedDir( ant ) -- Otherwise, continue with getting speed and direction data else -- The vehicle data exists for this slot if ( av[ant][i] ~= nil ) then -- Here we get the entity speed of the vehicle, the speed for this vehicle would've been obtained -- and stored in the trace stage, but the speed would've only been obtained and stored once, which -- means that it woulsn't be the current speed local vehSpeed = GetEntitySpeed( av[ant][i].veh ) local convertedSpeed = self:GetVehSpeedConverted( vehSpeed ) data.antennas[ant][i].speed = UTIL:FormatSpeed( convertedSpeed ) -- Work out if the vehicle is closing or away local ownH = UTIL:Round( GetEntityHeading( PLY.veh ), 0 ) local tarH = UTIL:Round( GetEntityHeading( av[ant][i].veh ), 0 ) data.antennas[ant][i].dir = UTIL:GetEntityRelativeDirection( ownH, tarH ) -- Set the internal antenna data as this actual dataset is valid if ( i % 2 == 0 ) then self:SetAntennaFastData( ant, data.antennas[ant][i].speed, data.antennas[ant][i].dir ) else self:SetAntennaData( ant, data.antennas[ant][i].speed, data.antennas[ant][i].dir ) end -- Lock the speed automatically if the fast limit system is allowed if ( self:IsFastLimitAllowed() ) then -- Make sure the speed is larger than the limit, and that there isn't already a locked speed if ( self:IsFastLockEnabled() and convertedSpeed > self:GetFastLimit() and not self:IsAntennaSpeedLocked( ant ) ) then if ( ( self:OnlyLockFastPlayers() and UTIL:IsPlayerInVeh( av[ant][i].veh ) ) or not self:OnlyLockFastPlayers() ) then if ( PLY:IsDriver() ) then self:LockAntennaSpeed( ant, nil, false ) SYNC:LockAntennaSpeed( ant, RADAR:GetAntennaDataPacket( ant ) ) end end end end else -- If the active vehicle is not valid, we reset the internal data if ( i % 2 == 0 ) then self:SetAntennaFastData( ant, nil, nil ) else self:SetAntennaData( ant, nil, nil ) end end end end end end -- Send the update to the NUI side SendNUIMessage( { _type = "update", speed = data.patrolSpeed, antennas = data.antennas } ) end end -- Main thread Citizen.CreateThread( function() -- Remove the NUI focus just in case SetNuiFocus( false, false ) -- Run the function to cache the number of rays, this way a hard coded number is never needed RADAR:CacheNumRays() -- Update the end coordinates for the ray traces based on the config, again, reduced hard coding RADAR:UpdateRayEndCoords() -- If the fast limit feature is allowed, create the config in the radar variables if ( RADAR:IsFastLimitAllowed() ) then RADAR:CreateFastLimitConfig() end -- Register the key binds RegisterKeyBinds() -- Wait a short period of time Citizen.Wait( 1000 ) -- Load the saved UI settings (if available) LoadUISettings() -- Set the player's remoteOpen decorator to false DecorSetBool( PlayerPedId(), "wk_wars2x_sync_remoteOpen", false ) -- Update the operator menu positions RADAR:UpdateOptionIndexes( true ) -- Run the main radar function while ( true ) do RADAR:Main() Citizen.Wait( 100 ) end end ) -- This function is pretty much straight from WraithRS, it does the job so I didn't see the point in not -- using it. Hides the radar UI when certain criteria is met, e.g. in pause menu or stepped out ot the -- patrol vehicle function RADAR:RunDisplayValidationCheck() if ( ( ( PLY.veh == 0 or ( PLY.veh > 0 and not PLY.vehClassValid ) ) and self:GetDisplayState() and not self:GetDisplayHidden() ) or IsPauseMenuActive() and self:GetDisplayState() ) then self:SetDisplayHidden( true ) SendNUIMessage( { _type = "setRadarDisplayState", state = false } ) elseif ( PLY:CanViewRadar() and self:GetDisplayState() and self:GetDisplayHidden() ) then self:SetDisplayHidden( false ) SendNUIMessage( { _type = "setRadarDisplayState", state = true } ) end end -- Runs the display validation check for the radar Citizen.CreateThread( function() Citizen.Wait( 100 ) while ( true ) do -- Run the check RADAR:RunDisplayValidationCheck() -- Wait half a second Citizen.Wait( 500 ) end end ) -- Update the vehicle pool every 3 seconds function RADAR:UpdateVehiclePool() -- Only update the vehicle pool if we need to if ( PLY:CanViewRadar() and self:CanPerformMainTask() and self:IsEitherAntennaOn() ) then -- Get the active vehicle set local vehs = self:GetAllVehicles() -- Update the vehicle pool self:SetVehiclePool( vehs ) end end -- Runs the vehicle pool updater Citizen.CreateThread( function() while ( true ) do -- Update the vehicle pool RADAR:UpdateVehiclePool() -- Wait 3 seconds Citizen.Wait( 3000 ) end end )