if Config.EnableTowCommands then CreateThread(function() TriggerEvent("chat:addSuggestion", "/tow", "Start the process to tow/attach a vehicle.") TriggerEvent("chat:addSuggestion", "/untow", "Start the process to untow a specific attached vehicle.") end) end -- Requests network control of an entity local function RequestNetworkControlOfObject(netId, itemEntity) if NetworkDoesNetworkIdExist(netId) then NetworkRequestControlOfNetworkId(netId) while not NetworkHasControlOfNetworkId(netId) do Wait(100) NetworkRequestControlOfNetworkId(netId) end end if DoesEntityExist(itemEntity) then NetworkRequestControlOfEntity(itemEntity) while not NetworkHasControlOfEntity(itemEntity) do Wait(100) NetworkRequestControlOfEntity(itemEntity) end end end -- Checks if the vehicle is allowed to be used to tow --- @param vehicle - The vehicle entity to check --- @return boolean - True/false if the vehicle is allowed to be used to tow local function isAllowedTowVehicle(vehicle) if Config.AllowAllVehiclesToTow then return true end local isAllowed = false for _, veh in pairs(Config.AllowedTowVehicles) do if GetEntityModel(vehicle) == GetHashKey(veh) then isAllowed = true break end end return isAllowed end -- Draws a marker above the target object that the player is looking at -- If the object is hovered, it will draw a yellow marker, once selected it will draw a red marker -- This needs to be called every frame to render correctly --- @param targetVehicle - The vehicle entity to draw the marker on --- @param isSelected boolean - Whether the vehicle had already been selected or is just hovered over --- @param markerType number - The type of marker to draw local function DrawMarkerOnTarget(targetVehicle, isSelected, markerType) -- local markerType = 0 local scale = 0.3 local alpha = 255 local bounce = true local faceCam = false local iUnk = 0 local rotate = false local textureDict = nil local textureName = nil local drawOnEnts = false local pos = GetEntityCoords(targetVehicle, true) local colorYellow = { red = 255, green = 255, blue = 0, } local colorRed = { red = 255, green = 50, blue = 0, } local color = colorYellow -- If isSelected then use color red, else use color yellow if isSelected then color = colorRed end DrawMarker(markerType, pos.x, pos.y, pos.z + 2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, scale, scale, scale - 0.1, color.red, color.green, color.blue, alpha, bounce, faceCam, iUnk, rotate, textureDict, textureName, drawOnEnts) end -- Used by the raycast functions to get the direction the camera is looking local function RotationToDirection(rotation) local adjustedRotation = { x = (math.pi / 180) * rotation.x, y = (math.pi / 180) * rotation.y, z = (math.pi / 180) * rotation.z, } local direction = { x = -math.sin(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), y = math.cos(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), z = math.sin(adjustedRotation.x), } return direction end -- Uses a RayCast to get the entity, coords, and whether we "hit" something with the raycast --- @param distance - The distance to cast the ray --- @return hit, coords, entity - Whether the raycast hit something, the coords of the hit, and the entity that was hit local function RayCastGamePlayCamera(distance) local cameraRotation = GetGameplayCamRot() local cameraCoord = GetGameplayCamCoord() local direction = RotationToDirection(cameraRotation) local destination = { x = cameraCoord.x + direction.x * distance, y = cameraCoord.y + direction.y * distance, z = cameraCoord.z + direction.z * distance, } local _, hit, coords, _, entity = GetShapeTestResult(StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, destination.x, destination.y, destination.z, -1, PlayerPedId(), 0)) return hit, coords, entity end -- Draws a line and marker at the end of the raycast where the player is looking --- @param coords - The coords to draw the line and marker at local function DrawRayCastLine(coords) local color = { r = 37, g = 192, b = 192, a = 200, } local position = GetEntityCoords(PlayerPedId()) if coords.x ~= 0.0 and coords.y ~= 0.0 then DrawLine(position.x, position.y, position.z, coords.x, coords.y, coords.z, color.r, color.g, color.b, color.a) DrawMarker(28, coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 0.0, 180.0, 0.0, 0.05, 0.05, 0.05, color.r, color.g, color.b, color.a, false, true, 2, nil, nil, false) end end -- Activates the ray cast mode to get the target object the player is looking at -- Listens for keypress to detect when the player has selected the target -- Checks if the vehicle is allowed to be used to tow and that the vehicle is not the same as the already selected vehicle --- @param shouldCheckAllowedVehicles - boolean - Whether to check if the vehicle is allowed to be used to tow --- @param otherSelectedVehicle - The vehicle entity that has already been selected --- @param shouldDrawOutline - boolean - Whether to draw an outline on the selected vehicle/object --- @return vehicle - The vehicle entity that the player has selected local function rayCastGetSelectedVehicle(shouldCheckAllowedVehicles, otherSelectedVehicle, shouldDrawOutline) local hit, coords, targetObj = RayCastGamePlayCamera(Config.TowDistance) -- Draw the line to the coords where the player is looking DrawRayCastLine(coords) if hit and DoesEntityExist(targetObj) and targetObj ~= otherSelectedVehicle and (Config.AllowHaulingProps or IsEntityAVehicle(targetObj)) then DrawMarkerOnTarget(targetObj, false, 0) -- Listen for keypress of ConfirmVehicleSelectionKey key if (IsControlJustPressed(0, Config.ConfirmVehicleSelectionKey)) then if not shouldCheckAllowedVehicles or isAllowedTowVehicle(targetObj) then if shouldDrawOutline then SetEntityDrawOutline(targetObj, true) SetEntityDrawOutlineColor(0, 255, 255, 255) SetEntityDrawOutlineShader(0) end return targetObj end end end end -- Attaches the targetObject onto the towVehicle in its current position, -- respecting both the targetObjects rotation and offset from the towVehicle -- If the vehicle + object are not touching, it will not attach them --- @param towVehicle - The vehicle entity that will be doing the towing --- @param targetObject - The vehicle entity that will be towed local function attachTargetObjectToTowVehicle(towVehicle, targetObject) local towNetId = NetworkGetNetworkIdFromEntity(towVehicle) local targetNetId = NetworkGetNetworkIdFromEntity(targetObject) -- Get control of the entities. This makes it so you dont need to sit in the vehicle first RequestNetworkControlOfObject(targetNetId, targetObject) RequestNetworkControlOfObject(towNetId, towVehicle) -- Necessary to make sure the player has control of both entities Wait(500) local isTargetTouchingTowVehicle = IsEntityTouchingEntity(towVehicle, targetObject) if isTargetTouchingTowVehicle then -- Calculate target object rotation relative to the tow vehicle -- This ensures that the object maintains its current rotation when attached to the tow vehicle local targetObjectRotation = GetEntityRotation(targetObject, 2) local towVehicleRotation = GetEntityRotation(towVehicle, 2) local newRotX = targetObjectRotation.x - towVehicleRotation.x local newRotY = targetObjectRotation.y - towVehicleRotation.y local newRotZ = targetObjectRotation.z - towVehicleRotation.z -- This ensures the target vehicle is attached to the tow vehicle at its current position local offsetCoords = GetOffsetFromEntityGivenWorldCoords(towVehicle, GetEntityCoords(targetObject)) AttachEntityToEntity(targetObject, towVehicle, 0, offsetCoords.x, offsetCoords.y, offsetCoords.z, newRotX, newRotY, newRotZ, 0, true, true, false, 2, true) Notify("Object has been strapped down.", "success", 5000) else Notify("Target object must be touching the tow vehicle in order to be towed", "error", 7500) end end -- Run a thread to handle selecting the tow vehicle and target vehicle local function startTowingSelectMode() local towVehicle = nil local targetObject = nil local hasNotifiedSelectTowTruck = false local hasNotifiedSelectTarget = false local hasNotifiedConfirm = false local isTowingSelectModeEnabled = true CreateThread(function() local instructionalButtons = nil while isTowingSelectModeEnabled do -- STEP 1: Runs a raycast to get the vehicle the player wants to tow to if not towVehicle then if not hasNotifiedSelectTowTruck then hasNotifiedSelectTowTruck = true instructionalButtons = DrawInstructionalButtons(Config.ConfirmVehicleSelectionKey, "Select tow vehicle", Config.ExitVehicleSelectionKey, "Cancel") end towVehicle = rayCastGetSelectedVehicle(true, nil, true) -- STEP 2: Runs a raycast to get the vehicle/object the player wants to be towed elseif not targetObject then if not hasNotifiedSelectTarget then hasNotifiedSelectTarget = true instructionalButtons = DrawInstructionalButtons(Config.ConfirmVehicleSelectionKey, "Select target to tow", Config.ExitVehicleSelectionKey, "Cancel") end targetObject = rayCastGetSelectedVehicle(false, towVehicle, true) -- STEP 3: Notify the player to press ConfirmVehicleSelectionKey to confirm the selections and complete the tow else if not hasNotifiedConfirm then hasNotifiedConfirm = true instructionalButtons = DrawInstructionalButtons(Config.ConfirmVehicleSelectionKey, "Confirm selection", Config.ExitVehicleSelectionKey, "Cancel") end -- Listen for keypress of the selection key to attach the vehicles together if (IsControlJustPressed(0, Config.ConfirmVehicleSelectionKey)) then isTowingSelectModeEnabled = false -- Remove outlines on confirm SetEntityDrawOutline(towVehicle, false) SetEntityDrawOutline(targetObject, false) attachTargetObjectToTowVehicle(towVehicle, targetObject) end end -- Draw markers on selected tow vehicle if towVehicle and isTowingSelectModeEnabled then DrawMarkerOnTarget(towVehicle, true, 39) end -- Listen for keypress of exit vehicle selection key to break out of the thread and cancel select mode if (IsControlJustPressed(0, Config.ExitVehicleSelectionKey)) then SetEntityDrawOutline(towVehicle, false) SetEntityDrawOutline(targetObject, false) isTowingSelectModeEnabled = false end DrawScaleformMovieFullscreen(instructionalButtons, 255, 255, 255, 255, 0) Wait(1) end end) end -- Run a thread to handle selecting the target vehicle that you want to untow local function startUnTowSelectMode() local targetObject = nil local isUnTowingSelectModeEnabled = true local instructionalButtons = DrawInstructionalButtons(Config.ConfirmVehicleSelectionKey, "Select target to untow", Config.ExitVehicleSelectionKey, "Cancel") CreateThread(function() while isUnTowingSelectModeEnabled do -- targetObject will be undefined until the player selects it from the raycast selection mode targetObject = rayCastGetSelectedVehicle(false, nil, false) if targetObject and IsEntityAttachedToAnyVehicle(targetObject) then -- Get control of the entities. This makes it so you dont need to sit in the vehicle first local netId = NetworkGetNetworkIdFromEntity(targetObject) RequestNetworkControlOfObject(netId, targetObject) Wait(500) DetachEntity(targetObject, true, false) isUnTowingSelectModeEnabled = false Notify("Vehicle detached", "success", 3000) elseif targetObject then Notify("Vehicle is not attached to anything", "error", 3000) end -- Listen for keypress of exit vehicle selection key to exit out of the loop if (IsControlJustPressed(0, Config.ExitVehicleSelectionKey)) then -- Exit out of the thread and reset any variables isUnTowingSelectModeEnabled = false end DrawScaleformMovieFullscreen(instructionalButtons, 255, 255, 255, 255, 0) Wait(1) end end) end RegisterNetEvent("wp-hauling:client:startTowSelection", function() startTowingSelectMode() end) RegisterNetEvent("wp-hauling:client:startUntowSelection", function() startUnTowSelectMode() end)