392 lines
14 KiB
Lua
392 lines
14 KiB
Lua
-- Client Functions
|
|
|
|
QBCore = nil
|
|
ESX = nil
|
|
|
|
Citizen.CreateThread(function()
|
|
if Config.Enable_QBCore_Permissions.Check_By_Job or Config.Enable_QBCore_Permissions.Check_By_Permissions or (Config.GearData.Enable_Gear_Permissions and Config.GearData.Enable_QBCore_Permissions.Check_By_Job or Config.GearData.Enable_QBCore_Permissions.Check_By_Permissions) then
|
|
QBCore = exports["qb-core"]:GetCoreObject()
|
|
end
|
|
|
|
if Config.Enable_ESX_Permissions.Check_By_Job or Config.Enable_ESX_Permissions.Check_By_Permissions or (Config.GearData.Enable_Gear_Permissions and Config.GearData.Enable_ESX_Permissions.Check_By_Job or Config.GearData.Enable_ESX_Permissions.Check_By_Permissions) then
|
|
ESX = exports["es_extended"]:getSharedObject()
|
|
end
|
|
end)
|
|
|
|
-- ============================================
|
|
-- FUNCTIONS FOR EXTERNAL USAGE (For developers only, no support will be provided for this by Nights Software)
|
|
-- ============================================
|
|
|
|
--- Handles when a callout is offered to the player.
|
|
-- @param calloutData table The data of the callout.
|
|
function OnIsOfferedCallout(calloutData)
|
|
-- Add your code here. Keep in mind they are offered a callout. It is possible they will not accept the callout.
|
|
-- local jsonReady = CloneWithoutFunctions(calloutData)
|
|
TriggerServerEvent('ErsIntegration::OnIsOfferedCallout', calloutData)
|
|
end
|
|
|
|
--- Handles when a callout is accepted by the player.
|
|
-- @param calloutData table The data of the callout.
|
|
function OnAcceptedCalloutOffer(calloutData)
|
|
-- Add your code here. Keep in mind they have accepted a callout. It is possible they will cancel before arrival (and spawn of entities).
|
|
TriggerServerEvent('ErsIntegration::OnAcceptedCalloutOffer', calloutData)
|
|
end
|
|
|
|
--- Handles when the player arrives at a callout.
|
|
-- @param calloutData table The data of the callout.
|
|
function OnArrivedAtCallout(calloutData)
|
|
-- Add your code here. This is triggered right before the entities are built for a callout. This code will execute first.
|
|
TriggerServerEvent('ErsIntegration::OnArrivedAtCallout', calloutData)
|
|
end
|
|
|
|
--- Handles when a callout is ended (as the host). This does not mean the callout is completed.
|
|
-- @param calloutData table The data of the callout.
|
|
function OnEndedACallout(calloutData)
|
|
-- Add your code here. This is triggered right before the entities are deleted or callout is cancelled serverside. This code will execute first.
|
|
TriggerServerEvent('ErsIntegration::OnEndedACallout', calloutData)
|
|
end
|
|
|
|
--- Handles when a callout is completed successfully.
|
|
-- @param calloutData table The data of the callout.
|
|
function OnCalloutCompletedSuccesfully(calloutData)
|
|
-- Add your code here. This is triggered right after the entire callout task list is completed.
|
|
TriggerServerEvent('ErsIntegration::OnCalloutCompletedSuccesfully', calloutData)
|
|
end
|
|
|
|
--- Handles when a pullover is initiated.
|
|
-- @param pedData table The data of the ped.
|
|
-- @param vehicleData table The data of the vehicle.
|
|
function OnPullover(pedData, vehicleData)
|
|
-- Add your custom pullover logic here or trigger (and build) a server event to handle it.
|
|
TriggerServerEvent('ErsIntegration::OnPullover', pedData, vehicleData)
|
|
end
|
|
|
|
--- Handles when a pullover is ended.
|
|
-- @param pedData table The data of the ped.
|
|
-- @param vehicleData table The data of the vehicle.
|
|
function OnPulloverEnded(pedData, vehicleData)
|
|
-- Add your custom pullover ended logic here or trigger (and build) a server event to handle it.
|
|
TriggerServerEvent('ErsIntegration::OnPulloverEnded', pedData, vehicleData)
|
|
end
|
|
|
|
--- Handles when a pursuit is started.
|
|
-- @param pedData table The data of the ped.
|
|
function OnPursuitStarted(pedData)
|
|
-- Add your custom pursuit started logic here or trigger (and build) a server event to handle it.
|
|
TriggerServerEvent('ErsIntegration::OnPursuitStarted', pedData)
|
|
end
|
|
|
|
--- Handles when a pursuit is ended.
|
|
-- @param pedData table The data of the ped.
|
|
function OnPursuitEnded(pedData)
|
|
-- Add your custom pursuit ended logic here or trigger (and build) a server event to handle it.
|
|
TriggerServerEvent('ErsIntegration::OnPursuitEnded', pedData)
|
|
end
|
|
|
|
-- ============================================
|
|
-- GEAR SYSTEM
|
|
-- ============================================
|
|
|
|
--- Handles when a NPC gives gear to the player.
|
|
-- @param data table The data of the NPC.
|
|
function OnNPCGivesGear(data)
|
|
local clothingData, weaponData, healthData = data.clothingData, data.weaponData, data.healthData
|
|
|
|
-- Player ped model
|
|
local isModelAMultiplayerModel = (clothingData.modelName == "mp_m_freemode_01" or clothingData.modelName == "mp_f_freemode_01")
|
|
local pedEntityModelHash = GetEntityModel(PlayerPedId())
|
|
if isModelAMultiplayerModel then
|
|
if Config.GearData.ForceMPPedWhenPlayerIsNotAnMPPed then
|
|
if pedEntityModelHash ~= GetHashKey(clothingData.modelName) then
|
|
local newModel = GetHashKey(clothingData.modelName)
|
|
RequestModel(newModel)
|
|
|
|
local attempts = 0
|
|
while not HasModelLoaded(newModel) and attempts < 10 do
|
|
Citizen.Wait(500)
|
|
attempts = attempts + 1
|
|
end
|
|
|
|
if HasModelLoaded(newModel) then
|
|
|
|
SetPlayerModel(PlayerId(), newModel)
|
|
SetPedComponentVariation(PlayerPedId(), 0, 0, 0, 2)
|
|
|
|
pedEntityModelHash = GetEntityModel(PlayerPedId())
|
|
|
|
SetModelAsNoLongerNeeded(newModel)
|
|
Citizen.Wait(500)
|
|
|
|
-- Confirm the model is set correctly
|
|
attempts = 0
|
|
while (pedEntityModelHash ~= GetHashKey(clothingData.modelName)) and attempts < 5 do
|
|
SetPlayerModel(PlayerId(), newModel)
|
|
Citizen.Wait(500)
|
|
pedEntityModelHash = GetEntityModel(PlayerPedId())
|
|
attempts = attempts + 1
|
|
end
|
|
|
|
SetPedDefaultComponentVariation(PlayerPedId())
|
|
|
|
if Config.Debug then
|
|
print("Set player model to: " .. newModel)
|
|
end
|
|
else
|
|
print("ERROR: Could not load model, please try fetching gear again...")
|
|
end
|
|
end
|
|
end
|
|
else
|
|
if Config.GearData.EnableSetClothing then
|
|
local model = clothingData.modelName
|
|
if IsModelInCdimage(model) and IsModelValid(model) then
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do
|
|
Wait(0)
|
|
end
|
|
SetPlayerModel(PlayerId(), model)
|
|
SetModelAsNoLongerNeeded(model)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Clothes
|
|
if Config.GearData.EnableSetClothing then
|
|
if isModelAMultiplayerModel then
|
|
ERS_SetOutfit(PlayerPedId(), clothingData)
|
|
if Config.Debug then
|
|
print("Setting outfit...")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Weapons
|
|
if Config.GearData.EnableGiveWeapons then
|
|
TriggerServerEvent(Config.EventPrefix..":setWeaponsAmmoComponents", weaponData)
|
|
end
|
|
|
|
-- Health and armor
|
|
if healthData.Enabled then
|
|
local playerPed = PlayerPedId()
|
|
-- Health
|
|
if healthData.Health then
|
|
local entityHealth = GetEntityHealth(playerPed)
|
|
local newHealth = math.min(entityHealth + healthData.Health, 200)
|
|
|
|
if newHealth ~= entityHealth then
|
|
SetEntityHealth(playerPed, newHealth)
|
|
end
|
|
end
|
|
|
|
-- Armor
|
|
if healthData.Armor then
|
|
local entityArmor = GetPedArmour(playerPed)
|
|
local newArmor = math.min(entityArmor + healthData.Armor, 200)
|
|
|
|
if newArmor ~= entityArmor then
|
|
SetPedArmour(playerPed, newArmor)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================
|
|
-- FUNCTIONS
|
|
-- ============================================
|
|
|
|
function message(lineOne, lineTwo, lineThree, duration)
|
|
BeginTextCommandDisplayHelp("THREESTRINGS")
|
|
AddTextComponentSubstringPlayerName(lineOne)
|
|
AddTextComponentSubstringPlayerName(lineTwo or "")
|
|
AddTextComponentSubstringPlayerName(lineThree or "")
|
|
EndTextCommandDisplayHelp(0, false, true, duration or 5000)
|
|
end
|
|
|
|
function notify(notificationText)
|
|
SetNotificationTextEntry("STRING")
|
|
AddTextComponentString(notificationText)
|
|
DrawNotification(true, true)
|
|
end
|
|
|
|
function Draw3DText(x,y,z,text,scl)
|
|
local onScreen,_x,_y=World3dToScreen2d(x,y,z)
|
|
local px,py,pz=table.unpack(GetGameplayCamCoords())
|
|
local dist = GetDistanceBetweenCoords(px,py,pz, x,y,z, 1)
|
|
local scale = (1/dist)*scl
|
|
local fov = (1/GetGameplayCamFov())*100
|
|
local scale = scale*fov
|
|
if onScreen then
|
|
SetTextScale(0.0*scale, 1.1*scale)
|
|
SetTextFont(0)
|
|
SetTextProportional(1)
|
|
SetTextColour(255, 255, 255, 255)
|
|
SetTextDropshadow(0, 0, 0, 0, 255)
|
|
SetTextEdge(2, 0, 0, 0, 150)
|
|
SetTextDropShadow()
|
|
--SetTextOutline()
|
|
SetTextEntry("STRING")
|
|
SetTextCentre(1)
|
|
AddTextComponentString("~h~"..text)
|
|
DrawText(_x,_y)
|
|
end
|
|
end
|
|
|
|
function DrawTxt(text, x, y, scale, size)
|
|
SetTextFont(0)
|
|
SetTextProportional(1)
|
|
SetTextScale(scale, size)
|
|
SetTextDropshadow(1, 0, 0, 0, 255)
|
|
SetTextColour(255, 255, 255, 255)
|
|
SetTextEdge(1, 0, 0, 0, 255)
|
|
SetTextDropShadow()
|
|
SetTextOutline()
|
|
SetTextEntry("STRING")
|
|
AddTextComponentString(text)
|
|
DrawText(x, y)
|
|
end
|
|
|
|
function firstToUpper(str)
|
|
return (str:gsub("^%l", string.upper))
|
|
end
|
|
|
|
function allToUpper(str)
|
|
return (string.upper(str))
|
|
end
|
|
|
|
function LoadAnimDict(dict)
|
|
RequestAnimDict(dict)
|
|
-- 1.5s timeout with debug print if timeout is reached
|
|
local startTime = GetGameTimer()
|
|
while (not HasAnimDictLoaded(dict)) and (GetGameTimer() - startTime < 1500) do
|
|
Citizen.Wait(1)
|
|
end
|
|
if not HasAnimDictLoaded(dict) then
|
|
print(string.format("ERROR: Failed to load animation dictionary: %s in 1.5s", dict))
|
|
else
|
|
local timeTaken = GetGameTimer() - startTime
|
|
print(string.format("INFO: Successfully loaded animation dictionary: %s in %s ms", dict, timeTaken))
|
|
end
|
|
end
|
|
|
|
function PlaySound(folder, file, vol)
|
|
SendNUIMessage({
|
|
transactionType = 'playSound',
|
|
transactionFolder = folder,
|
|
transactionFile = file,
|
|
transactionVolume = vol
|
|
})
|
|
end
|
|
|
|
function PlayRadioAnimation()
|
|
local plyPed = PlayerPedId()
|
|
local userServerId = GetPlayerServerId(NetworkGetPlayerIndexFromPed(plyPed))
|
|
local playerNetId = NetworkGetNetworkIdFromEntity(plyPed)
|
|
|
|
local animDict = Config.RadioAnimationDictionary
|
|
local animName = Config.RadioAnimationName
|
|
local duration = Config.RadioAnimationDuration
|
|
|
|
local taskType = "TaskPlayAnim"
|
|
local paramsList = {
|
|
initiatorSrc = userServerId,
|
|
targetNetId = playerNetId,
|
|
dict = animDict,
|
|
anim = animName,
|
|
blendInSpeed = 8.0,
|
|
blendOutSpeed = -8.0,
|
|
duration = duration,
|
|
flag = 50,
|
|
playbackRate = 0,
|
|
lockX = false,
|
|
lockY = false,
|
|
lockZ = false
|
|
}
|
|
ERS_TriggerEntityTaskForAllClients(taskType, paramsList)
|
|
end
|
|
|
|
function CloneWithoutFunctions(tbl)
|
|
local copy = {}
|
|
for key, value in pairs(tbl) do
|
|
if type(value) == "table" then
|
|
copy[key] = CloneWithoutFunctions(value)
|
|
elseif type(value) ~= "function" then
|
|
copy[key] = value
|
|
end
|
|
end
|
|
return copy
|
|
end
|
|
|
|
--============ POSTAL CODE INTEGRATION ============--
|
|
|
|
local raw = nil
|
|
local postals = nil
|
|
local nearestCalloutPostal = nil
|
|
|
|
if Config.Enable_Nearest_Postal then
|
|
-- Use this, or adjust this to your postal system as an integration
|
|
raw = LoadResourceFile("nearest-postal", GetResourceMetadata("nearest-postal", 'postal_file'))
|
|
if raw == nil then
|
|
print("^1ERROR^7 Postal resource 'nearest-postal' file does not exist (is not installed) or failed to load. Check https://docs.nights-software.com for installation support.")
|
|
else
|
|
postals = json.decode(raw)
|
|
end
|
|
end
|
|
|
|
function getPostal(x, y) -- Editing this? Postals can not be anything other than numbers!
|
|
if Config.Enable_Nearest_Postal then
|
|
local theCalloutPostal = nil
|
|
if postals ~= nil then
|
|
local ndm = -1 -- nearest distance magnitude
|
|
local ni = -1 -- nearest index
|
|
for i, p in ipairs(postals) do
|
|
local dm = (x - p.x) ^ 2 + (y - p.y) ^ 2 -- distance magnitude
|
|
if ndm == -1 or dm < ndm then
|
|
ni = i
|
|
ndm = dm
|
|
end
|
|
end
|
|
--setting the nearest
|
|
if ni ~= -1 then
|
|
local nd = math.sqrt(ndm) -- nearest distance
|
|
nearestCalloutPostal = {i = ni, d = nd}
|
|
end
|
|
|
|
local text = postals[nearestCalloutPostal.i].code, nearestCalloutPostal.d
|
|
theCalloutPostal = text
|
|
else
|
|
return "Unknown postal"
|
|
end
|
|
return theCalloutPostal
|
|
else
|
|
return "Unknown postal"
|
|
end
|
|
end
|
|
|
|
|
|
-- Discord Webhook Integrations
|
|
|
|
function OnSendDispatchMessage(message)
|
|
local messageData = {
|
|
title = "DISPATCH MESSAGE SYSTEM",
|
|
description = tostring(message),
|
|
color = 11876095, -- https://www.mathsisfun.com/hexadecimal-decimal-colors.html (Decimal colors is what this requires, so the one with numbers only)
|
|
authorname = "Emergency Response Simulator",
|
|
-- authoravatarurl = player.discordMember.avatar,
|
|
|
|
sender = "Discord user",
|
|
senderdiscordid = 0, -- Set serverside
|
|
|
|
-- subjecttitle = "Plate: "..plate,
|
|
-- subjectdescription = PersonalData.userCurrentStreetName.." at postal: "..PersonalData.userCurrentPostal,
|
|
|
|
footer = "Emergency Response Simulator - by Nights Software in collaboration with London Studios",
|
|
footericon = "https://assets.ea-rp.com/img/ERS_Logo.png",
|
|
|
|
thumbnail = "https://assets.ea-rp.com/img/ERS_Logo_Sq.png",
|
|
image = "https://assets.ea-rp.com/img/ERS_Logo.png",
|
|
|
|
discordwebhookurltype = "dispatch",
|
|
systemname = "Dispatch - System",
|
|
}
|
|
TriggerServerEvent(Config.EventPrefix..":sendDiscordEmbedMessage", messageData)
|
|
end |