vRP = nil ESX = nil if Config.Notifications.Enabled and Config.Notifications.Framework.vRP then local Tunnel = module("vrp", "lib/Tunnel") local Proxy = module("vrp", "lib/Proxy") vRP = Proxy.getInterface("vRP") end if Config.Notifications.Enabled and Config.Notifications.Framework.ESX then ESX = exports["es_extended"]:getSharedObject() end if Config.PlaceGeneratorCommand.Inventory.quasarInventory or Config.PlaceGeneratorCommand.Inventory.coreInventory then function inventorySetupGenerator() TriggerEvent("SupplyLine:Client:EquipPortableGenerator") end exports("inventorySetupGenerator", inventorySetupGenerator) end if Config.EnablePositioningCommand then TriggerEvent('chat:addSuggestion', '/'.."findhydrantpositioning", "Find rope spawn point and rotation for a hydrant", { { name="type", help="supplyLine or lppLine" } }) RegisterCommand("findhydrantpositioning", function(source, args) local ropeLabel = args[1] or "supplyLine" local ped = PlayerPedId() local pedCoords = GetEntityCoords(ped) local targetHydrant = 0 local ropeType = 4 if HydrantRopes and HydrantRopes.RopeTypes and HydrantRopes.RopeTypes[ropeLabel] then ropeType = HydrantRopes.RopeTypes[ropeLabel] end -- Add your server's custom hydrant models to this list if you have them! local hydrantModels = { `prop_fire_hydrant_1`, `prop_fire_hydrant_2`, `prop_fire_hydrant_4`, `bv_water_pump` } for _, model in ipairs(hydrantModels) do local obj = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, 5.0, model, false, false, false) if obj ~= 0 then targetHydrant = obj break end end if targetHydrant == 0 then return Notify("No hydrant found nearby! Stand closer.") end RopeLoadTextures() local hydPos = GetEntityCoords(targetHydrant) local previewRope = AddRope(hydPos.x, hydPos.y, hydPos.z, 0.0, 0.0, 0.0, 20.0, ropeType, 20.0, 0.5, 1.0, false, false, false, 5.0, false, 0) local offSet = {x = 0.0, y = 0.0, z = 0.0} local rotation = {x = 0.0, y = 0.0, z = 0.0} local depth = 0.2 local mode = "MOVE" local editing = true Notify("~b~POSITIONING MODE~w~\n[TAB] Toggle Move/Rotate\n[Arrows] Adjust\n[INSERT/DELETE] Depth\n[ENTER] Save") SetEntityAlpha(targetHydrant, 150, false) local function RotationToDirection(rot) local radZ = math.rad(rot.z) local radX = math.rad(rot.x) local num = math.abs(math.cos(radX)) return vector3(-math.sin(radZ) * num, math.cos(radZ) * num, math.sin(radX)) end while editing do Wait(0) local speed = IsControlPressed(0, 21) and 0.05 or 0.005 if IsControlJustPressed(0, 192) then mode = (mode == "MOVE") and "ROTATE" or "MOVE" Notify("Mode: ~y~" .. mode) end if IsControlPressed(0, 121) then depth = depth + 0.005 end if IsControlPressed(0, 178) then depth = depth - 0.005 end local up = IsControlPressed(0, Config.PositioningControls.up) local down = IsControlPressed(0, Config.PositioningControls.down) local forward = IsControlPressed(0, Config.PositioningControls.forwards) local backward = IsControlPressed(0, Config.PositioningControls.backwards) local left = IsControlPressed(0, Config.PositioningControls.left) local right = IsControlPressed(0, Config.PositioningControls.right) if mode == "MOVE" then if up then offSet.z = offSet.z + speed end if down then offSet.z = offSet.z - speed end if forward then offSet.y = offSet.y + speed end if backward then offSet.y = offSet.y - speed end if left then offSet.x = offSet.x - speed end if right then offSet.x = offSet.x + speed end else local rSpeed = speed * 50.0 if up then rotation.x = rotation.x + rSpeed end if down then rotation.x = rotation.x - rSpeed end if left then rotation.z = rotation.z + rSpeed end if right then rotation.z = rotation.z - rSpeed end end local forwardVec = RotationToDirection(rotation) local localPortPos = vector3(offSet.x, offSet.y, offSet.z) local localBendPosA = localPortPos - (forwardVec * depth) local localBendPosB = localPortPos - (forwardVec * (depth * 0.5)) local worldPointC = GetOffsetFromEntityInWorldCoords(targetHydrant, localPortPos.x, localPortPos.y, localPortPos.z) local worldPointA = GetOffsetFromEntityInWorldCoords(targetHydrant, localBendPosA.x, localBendPosA.y, localBendPosA.z) local worldPointB = GetOffsetFromEntityInWorldCoords(targetHydrant, localBendPosB.x, localBendPosB.y, localBendPosB.z) local handPos = GetWorldPositionOfEntityBone(ped, GetPedBoneIndex(ped, 6286)) local vertexCount = GetRopeVertexCount(previewRope) PinRopeVertex(previewRope, 0, handPos.x, handPos.y, handPos.z) PinRopeVertex(previewRope, vertexCount - 1, worldPointA.x, worldPointA.y, worldPointA.z) PinRopeVertex(previewRope, vertexCount - 2, worldPointB.x, worldPointB.y, worldPointB.z) PinRopeVertex(previewRope, vertexCount - 3, worldPointC.x, worldPointC.y, worldPointC.z) if IsControlJustPressed(0, Config.PositioningControls.enter) then editing = false end end ResetEntityAlpha(targetHydrant) print(string.format("^2--- %s OFFSET RESULT ---^7", ropeLabel:upper())) print(string.format("{ x = %.3f, y = %.3f, z = %.3f, rx = %.3f, ry = %.3f, rz = %.3f, depth = %.3f },", offSet.x, offSet.y, offSet.z, rotation.x, rotation.y, rotation.z, depth)) DeleteRope(previewRope) Notify("Data printed to F8 console.") end, false) end function Notify(text) if not Config.Notifications.Enabled then return end if Config.Notifications.Framework.ESX then if ESX ~= nil then ESX.ShowNotification(text) end elseif Config.Notifications.Framework.QBCore then TriggerEvent('QBCore:Notify', text, 'primary') elseif Config.Notifications.Framework.QBX then exports.qbx_core:Notify(text, 'primary') elseif Config.Notifications.Framework.vRP then vRP.notify(source, {text}) elseif Config.Notifications.Framework.okok then exports['okokNotify']:Alert("Supply Line", text, 2000, 'info', true) else showBaseNotification(text) end end RegisterNetEvent('SupplyLine:Client:showNotification', function(message) Notify(message) end) function showBaseNotification(message) -- Base game notifications SetNotificationTextEntry("STRING") AddTextComponentString(message) DrawNotification(0,1) end function DisplayHelpText(text) SetTextComponentFormat("STRING") AddTextComponentString(text) DisplayHelpTextFromStringLabel(0, 0, ConfigHose.Notifications.HelpTextSound, -1) end function drawInstructionalText(msg, coords) AddTextEntry('instructionalText', msg) SetFloatingHelpTextWorldPosition(1, coords.x, coords.y, coords.z) SetFloatingHelpTextStyle(1, 1, 2, -1, 3, 0) BeginTextCommandDisplayHelp('instructionalText') AddTextComponentSubstringPlayerName(msg) EndTextCommandDisplayHelp(2, false, false, -1) end function DrawTextOnScreen(text, x, y, scale, r, g, b, a, center) SetTextFont(0) SetTextProportional(1) SetTextScale(scale, scale) SetTextColour(r, g, b, a) SetTextDropshadow(0, 0, 0, 0, 255) SetTextEdge(2, 0, 0, 0, 150) SetTextDropShadow() SetTextOutline() SetTextEntry("STRING") if center then SetTextCentre(1) end AddTextComponentString(text) DrawText(x, y) end local hydrantInteractionId = 'hydrantInteraction' local hydrantInteractionActive = false if Config.InteractType.HydrantInteraction.Drawtext or Config.InteractType.HydrantInteraction.ScreenPrompt then StartHoseWaterLevelDisplayThread() CreateThread(function() while true do local waitTime = 2500 local player = GetPlayerPed(PlayerId()) local playerPos = GetEntityCoords(player, false) local fillingVehicle = nil if not IsPedInAnyVehicle(player, false) then local closestHydrant = GetClosestHydrant(playerPos) if closestHydrant and DoesEntityExist(closestHydrant) then if not NetworkGetEntityIsNetworked(closestHydrant) then NetworkRegisterEntityAsNetworked(closestHydrant) end local hydrantId = NetworkGetNetworkIdFromEntity(closestHydrant) if NetworkDoesNetworkIdExist(hydrantId) then local hydrantPos = GetEntityCoords(closestHydrant) local rope = HydrantRopes.currentRopes[hydrantId] fillingVehicle = HydrantManager.fillingVehicles[hydrantId] if rope then if fillingVehicle then if Config.InteractType.HydrantInteraction.Drawtext then drawInstructionalText(Config.Translations.stopFilling, vec3(hydrantPos.x, hydrantPos.y, hydrantPos.z + 1.0)) elseif Config.InteractType.HydrantInteraction.ScreenPrompt then DisplayHelpText(Config.Translations.stopFilling) end else if Config.InteractType.HydrantInteraction.Drawtext then drawInstructionalText(Config.Translations.startFilling, vec3(hydrantPos.x, hydrantPos.y, hydrantPos.z + 1.0)) elseif Config.InteractType.HydrantInteraction.ScreenPrompt then DisplayHelpText(Config.Translations.startFilling) end end else if HoseBridge.isLineEquipped and Config.InteractType.HydrantInteraction.Drawtext then drawInstructionalText(Config.Translations.connectLine, vec3(hydrantPos.x, hydrantPos.y, hydrantPos.z + 1.0)) elseif Config.InteractType.HydrantInteraction.ScreenPrompt then DisplayHelpText(Config.Translations.connectLine) end end waitTime = 1 if IsControlJustPressed(1, Config.SupplyLineKeys.ConnectHydrant) and isPermissionAllowed() then HandleHydrantInteraction(hydrantId, hydrantPos) TriggerEvent('SmartHose:Client:Bridge:DeleteHandRopeS') end if IsControlJustPressed(1, Config.SupplyLineKeys.StartStopFilling) and rope and isPermissionAllowed() then local fillingVehicle = HydrantManager.fillingVehicles[hydrantId] if fillingVehicle then HandleStartStopFilling(hydrantId, false) else HandleStartStopFilling(hydrantId, true) end end end end end Wait(waitTime) end end) elseif Config.InteractType.HydrantInteraction.ThirdEye.enabled then local hydrantModels = {} for _, offset in ipairs(Config.HydrantOffsets) do table.insert(hydrantModels, offset.model) end StartHoseWaterLevelDisplayThread() if Config.InteractType.HydrantInteraction.ThirdEye.oxTarget then local jobGroups = GetAllowedJobGroups() local HydrantOptions = { { name = 'connect_line', label = Config.Translations.thirdEyeConnect, icon = 'fa-solid fa-link', groups = jobGroups, canInteract = function(entity, distance, data) if not HoseBridge.isLineEquipped then return false end if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) if not NetworkGetEntityIsNetworked(entity) then return false end end local netId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[netId] if rope then return false end return true end, onSelect = function(data) local hydrantEntity = data.entity HandleHydrantInteractionEntity(hydrantEntity) TriggerEvent('SmartHose:Client:Bridge:DeleteHandRopeS') end, }, { name = 'start_filling', label = Config.Translations.thirdEyeStartFill, icon = 'fa-solid fa-play', groups = jobGroups, canInteract = function(entity, distance, data) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then return false end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[hydrantId] local fillingVehicle = HydrantManager.fillingVehicles[hydrantId] return rope and not fillingVehicle end, onSelect = function(data) local hydrantEntity = data.entity HandleStartFillingEntity(hydrantEntity) end, }, { name = 'stop_filling', label = Config.Translations.thirdEyeStopFill, icon = 'fa-solid fa-stop', groups = jobGroups, canInteract = function(entity, distance, data) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then return false end local fillingVehicle = HydrantManager.fillingVehicles[NetworkGetNetworkIdFromEntity(entity)] return fillingVehicle ~= nil end, onSelect = function(data) local hydrantEntity = data.entity HandleStopFillingEntity(hydrantEntity) end, }, { name = 'disconnect_line', label = Config.Translations.thirdEyeDisconnect, icon = 'fa-solid fa-unlink', groups = jobGroups, canInteract = function(entity, distance, data) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then return false end local rope = HydrantRopes.currentRopes[NetworkGetNetworkIdFromEntity(entity)] return rope ~= nil end, onSelect = function(data) local hydrantEntity = data.entity HandleHydrantInteractionEntity(hydrantEntity) end, }, } exports.ox_target:addModel(hydrantModels, HydrantOptions) elseif Config.InteractType.HydrantInteraction.ThirdEye.qbTarget then local allowedJobs = GetAllowedJobGroups() exports['qb-target']:AddTargetModel(hydrantModels, { options = { { label = Config.Translations.thirdEyeConnect, icon = 'fa-solid fa-link', job = allowedJobs, canInteract = function(entity) if not HoseBridge.isLineEquipped then return false end if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end local rope = HydrantRopes.currentRopes[NetworkGetNetworkIdFromEntity(entity)] return not rope end, action = function(entity) if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end HandleHydrantInteractionEntity(entity) TriggerEvent('SmartHose:Client:Bridge:DeleteHandRopeS') end, }, { label = Config.Translations.thirdEyeStartFill, icon = 'fa-solid fa-play', job = allowedJobs, canInteract = function(entity) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[hydrantId] local fillingVehicle = HydrantManager.fillingVehicles[hydrantId] return rope and not fillingVehicle end, action = function(entity) if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end HandleStartFillingEntity(entity) end, }, { label = Config.Translations.thirdEyeStopFill, icon = 'fa-solid fa-stop', job = allowedJobs, canInteract = function(entity) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end local fillingVehicle = HydrantManager.fillingVehicles[NetworkGetNetworkIdFromEntity(entity)] return fillingVehicle ~= nil end, action = function(entity) if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end HandleStopFillingEntity(entity) end, }, { label = Config.Translations.thirdEyeDisconnect, icon = 'fa-solid fa-unlink', job = allowedJobs, canInteract = function(entity) if HasObjectBeenBroken(entity) then return false end if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end local rope = HydrantRopes.currentRopes[NetworkGetNetworkIdFromEntity(entity)] return rope ~= nil end, action = function(entity) if not NetworkGetEntityIsNetworked(entity) then NetworkRegisterEntityAsNetworked(entity) Wait(0) end HandleHydrantInteractionEntity(entity) end, }, }, distance = 2.5, }) end elseif Config.InteractType.HydrantInteraction.WorldInteraction then CreateThread(function() local idleWait = 1000 local playerPed = PlayerPedId() local playerPos = GetEntityCoords(playerPed) while true do Wait(idleWait) playerPed = PlayerPedId() playerPos = GetEntityCoords(playerPed) if IsPedInAnyVehicle(playerPed, false) then if hydrantInteractionActive then for _, hydrantConfig in ipairs(Config.HydrantOffsets) do exports.interact:RemoveModelInteraction(hydrantConfig.model, hydrantInteractionId) end hydrantInteractionActive = false end idleWait = 1500 else local hydrant = GetClosestHydrant(playerPos) if hydrant and DoesEntityExist(hydrant) and not HasObjectBeenBroken(hydrant) then local hydrantPos = GetEntityCoords(hydrant) local distanceToHydrant = #(playerPos - hydrantPos) if distanceToHydrant < 4.0 then if not hydrantInteractionActive then for _, hydrantConfig in ipairs(Config.HydrantOffsets) do exports.interact:AddModelInteraction({ model = hydrantConfig.model, offset = vec3(0.0, 0.0, 0.5), name = 'Hydrant Interaction', id = hydrantInteractionId, distance = 5.0, interactDst = 1.5, ignoreLos = true, options = { { label = Config.Translations.thirdEyeConnect, action = function(entity) HandleHydrantInteractionEntity(entity) end, canInteract = function(entity) if not isPermissionAllowed() or HasObjectBeenBroken(entity) then return false end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[hydrantId] return not rope end, }, { label = Config.Translations.thirdEyeStartFill, action = function(entity) HandleStartFillingEntity(entity) end, canInteract = function(entity) if not isPermissionAllowed() or HasObjectBeenBroken(entity) then return false end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[hydrantId] local fillingVehicle = HydrantManager.fillingVehicles[hydrantId] return rope and not fillingVehicle end, }, { label = Config.Translations.thirdEyeStopFill, action = function(entity) HandleStopFillingEntity(entity) end, canInteract = function(entity) if not isPermissionAllowed() or HasObjectBeenBroken(entity) then return false end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local fillingVehicle = HydrantManager.fillingVehicles[hydrantId] return fillingVehicle ~= nil end, }, { label = Config.Translations.thirdEyeDisconnect, action = function(entity) HandleHydrantInteractionEntity(entity) end, canInteract = function(entity) if not isPermissionAllowed() or HasObjectBeenBroken(entity) then return false end local hydrantId = NetworkGetNetworkIdFromEntity(entity) local rope = HydrantRopes.currentRopes[hydrantId] return rope ~= nil end, }, } }) end hydrantInteractionActive = true end idleWait = 0 else if hydrantInteractionActive then for _, hydrantConfig in ipairs(Config.HydrantOffsets) do exports.interact:RemoveModelInteraction(hydrantConfig.model, hydrantInteractionId) end hydrantInteractionActive = false end idleWait = 500 end else if hydrantInteractionActive then for _, hydrantConfig in ipairs(Config.HydrantOffsets) do exports.interact:RemoveModelInteraction(hydrantConfig.model, hydrantInteractionId) end hydrantInteractionActive = false end idleWait = 1000 end end end end) else warn('Invalid interaction configuration') end