local animationDict = "pickup_object" local animation = "pickup_low" local isInPlaceItemMode = false -- Keeps track of the models that have already been set with target options, ensuring we don't create duplicate options for the same model -- In some frameworks like OX, if you create targetOptions on a model that already has it, it will append the options, whereas -- in QB it will override and only the last set of options will be used. We just need to add one option to the target model and then -- the pickup event will use the statebag to determine the correct item to give back to the player local targetModels = {} local function LoadPropDict(model) while not HasModelLoaded(GetHashKey(model)) do RequestModel(GetHashKey(model)) Wait(10) end end -- Gets the direction the camera is looking for the raycast function 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 -- Object passed in, is the current object that we want the raycast to ignore local function RayCastGamePlayCamera(distance, object, raycastDetectWorldOnly) 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, } -- Trace flag 4294967295 means the raycast will intersect with everything (including vehicles) -- Trace flag 1 means the raycast will only intersect with the world (ignoring other entities like peds, cars, etc) local traceFlag = 4294967295 if raycastDetectWorldOnly then traceFlag = 1 end local a, hit, coords, d, entity = GetShapeTestResult(StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, destination.x, destination.y, destination.z, traceFlag, object, 0)) return hit, coords, entity end -- Used to Draw the text on the screen local function Draw2DText(content, font, colour, scale, x, y) SetTextFont(font) SetTextScale(scale, scale) SetTextColour(colour[1], colour[2], colour[3], 255) SetTextEntry("STRING") SetTextDropShadow(0, 0, 0, 0, 255) SetTextDropShadow() SetTextEdge(4, 0, 0, 0, 255) SetTextOutline() AddTextComponentString(content) DrawText(x, y) end -- This handles placing the actual item that is network synced local function placeItem(item, coords, heading, shouldSnapToGround) local ped = PlayerPedId() local itemName = item.item local itemModel = item.model local shouldFreezeItem = item.isFrozen -- Cancel any active animation ClearPedTasks(ped) Progressbar("place_item", "Placing " .. item.label, 750, false, true, { disableMovement = false, disableCarMovement = false, disableMouse = false, disableCombat = true, }, { animDict = animationDict, anim = animation, flags = 0, }, nil, nil, function() -- Done -- Stop playing the animation StopAnimTask(ped, animationDict, animation, 1.0) -- Remove the item from the inventory TriggerServerEvent("wp-placeables:server:RemoveItem", itemName) LoadPropDict(itemModel) -- Spawn prop on ground at the provided coords and heading local obj = CreateObject(itemModel, GetEntityCoords(ped), true) if obj ~= 0 then SetEntityRotation(obj, 0.0, 0.0, heading, false, false) SetEntityCoords(obj, coords) if shouldFreezeItem then FreezeEntityPosition(obj, true) end -- Some items dont go to the ground properly with this, and it actually makes them hover if shouldSnapToGround then PlaceObjectOnGroundProperly(obj) end -- Use statebag property itemName to set the itemName on the entity. -- This value is used to grant the correct item back to the player when they pick it up. -- It also solves the issue of the same model being used for multiple items Entity(obj).state:set("itemName", itemName, true) CreateLog(itemName, true) end SetModelAsNoLongerNeeded(itemModel) end, function() -- Cancel StopAnimTask(ped, animationDict, animation, 1.0) Notify("Canceled..", "error") end) end -- Starts a thread that puts the player into item placement mode -- This will spawn a local object that only the player can see and move around to position it -- Once the player places the object it will delete the local one and then create a new network synced object local function startItemPlacementMode(item) -- This is to prevent entering place mode multiple times if its already active if isInPlaceItemMode then Notify("Already placing an item", "error", 5000) return end isInPlaceItemMode = true local ped = PlayerPedId() local itemModel = item.model -- Create a local object for only this client (not synced to network) and make it transparent local obj = CreateObject(itemModel, GetEntityCoords(ped), false, false) SetEntityAlpha(obj, 150, false) SetEntityCollision(obj, false, false) local zOffset = 0 -- This is used to determine if the raycast should only detect the world or if it should detect everything (including vehicles) local raycastDetectWorldOnly = true CreateThread(function() while isInPlaceItemMode do -- Use raycast based on where the camera is pointed local hit, coords, entity = RayCastGamePlayCamera(Config.ItemPlacementModeRadius, obj, raycastDetectWorldOnly) -- Move the object to the coords from the raycast SetEntityCoords(obj, coords.x, coords.y, coords.z + zOffset) -- Display the controls Draw2DText("[E] Place\n[Shift+E] Place on ground\n[Scroll Up/Down] Rotate\n[Shift+Scroll Up/Down] Raise/lower", 4, { 255, 255, 255, }, 0.4, 0.85, 0.85) Draw2DText("[Scroll Click] Change mode\n[Right Click / Backspace] Exit place mode", 4, { 255, 255, 255, }, 0.4, 0.85, 0.945) -- Handle various key presses and actions -- Controls for placing item -- Pressed Shift + E - Place object on ground if IsControlJustReleased(0, 38) and IsControlPressed(0, 21) then isInPlaceItemMode = false local objHeading = GetEntityHeading(obj) local snapToGround = true DeleteEntity(obj) placeItem(item, vector3(coords.x, coords.y, coords.z + zOffset), objHeading, snapToGround) -- Pressed E - Place object at current position elseif IsControlJustReleased(0, 38) then isInPlaceItemMode = false local objHeading = GetEntityHeading(obj) local snapToGround = false DeleteEntity(obj) placeItem(item, vector3(coords.x, coords.y, coords.z + zOffset), objHeading, snapToGround) end -- Controls for rotating item -- Mouse Wheel Up (and Shift not pressed), rotate by +10 degrees if IsControlJustReleased(0, 241) and not IsControlPressed(0, 21) then local objHeading = GetEntityHeading(obj) SetEntityRotation(obj, 0.0, 0.0, objHeading + 10, false, false) end -- Mouse Wheel Down (and shift not pressed), rotate by -10 degrees if IsControlJustReleased(0, 242) and not IsControlPressed(0, 21) then local objHeading = GetEntityHeading(obj) SetEntityRotation(obj, 0.0, 0.0, objHeading - 10, false, false) end -- Controls for raising/lowering item -- Shift + Mouse Wheel Up, move item up if IsControlPressed(0, 21) and IsControlJustReleased(0, 241) then zOffset = zOffset + 0.1 if zOffset > Config.maxZOffset then zOffset = Config.maxZOffset end end -- Shift + Mouse Wheel Down, move item down if IsControlPressed(0, 21) and IsControlJustReleased(0, 242) then zOffset = zOffset - 0.1 if zOffset < Config.minZOffset then zOffset = Config.minZOffset end end -- Mouse Wheel Click, change placement mode if IsControlJustReleased(0, 348) then raycastDetectWorldOnly = not raycastDetectWorldOnly end -- Right click or Backspace to exit out of placement mode and delete the local object if IsControlJustReleased(0, 177) then isInPlaceItemMode = false DeleteEntity(obj) end Wait(1) end end) end -- Handles picking up the prop, deleting it from the world and adding it to the players inventory local function pickUpItem(itemData) local ped = PlayerPedId() local itemEntity = itemData.entity local itemModel = itemData.itemModel -- When picking up the item, try to get the itemName from the statebag property first, else fallback to the itemName from the itemData provided by the target script -- Using the statebag property ensures we get the correct item name if the prop model is shared by multiple items.. local itemName = Entity(itemEntity).state.itemName or itemData.itemName if itemName then -- Cancel any active animation ClearPedTasks(ped) Progressbar("pickup_item", "Picking up item", 200, false, true, { disableMovement = false, disableCarMovement = false, disableMouse = false, disableCombat = true, }, { animDict = animationDict, anim = animation, flags = 0, }, nil, nil, function() -- Done -- Stop playing the animation StopAnimTask(ped, animationDict, animation, 1.0) -- Add the item to the inventory TriggerServerEvent("wp-placeables:server:AddItem", itemName) -- First request control of networkId and wait until have control of netId before deleting it -- Item will not properly delete if the client doesn't have control of the networkId local coords = GetEntityCoords(itemEntity) local netId = NetworkGetNetworkIdFromEntity(itemEntity) RequestNetworkControlOfObject(netId, itemEntity) SetEntityAsMissionEntity(itemEntity, true, true) DeleteEntity(itemEntity) local object = { coords = coords, model = itemModel, } TriggerServerEvent("wp-placeables:server:deleteWorldObject", object) CreateLog(itemName, false) end, function() -- Cancel StopAnimTask(ped, animationDict, animation, 1.0) Notify("Canceled..", "error") end) end end RegisterNetEvent("wp-placeables:client:placeItem", function(item) if not IsPedInAnyVehicle(PlayerPedId(), true) then startItemPlacementMode(item) else Notify("You cannot place items while in a vehicle", "error", 5000) end end) RegisterNetEvent("wp-placeables:client:pickUpItem", function(data) pickUpItem(data) end) -- Setup each placeable prop to use QB target -- Itemname is in the options so we know which item to give back when picked up for _, prop in pairs(Config.PlaceableProps) do local pickUpEvent = "wp-placeables:client:pickUpItem" if prop.customPickupEvent then pickUpEvent = prop.customPickupEvent end local targetOptions = { { event = pickUpEvent, icon = "fas fa-hand-holding", label = "Pick up", itemName = prop.item, itemModel = prop.model, }, } -- Add custom target options to the target options for this item prop if prop.customTargetOptions then for _, customOption in pairs(prop.customTargetOptions) do -- Stamp the itemName and itemModel onto the data so the custom events have access to this info customOption.itemName = prop.item customOption.itemModel = prop.model targetOptions[#targetOptions + 1] = customOption end end -- Make sure we only define the target options once for each model -- If you define the same model twice: -- In qb-target, it will override the options, and the last one defined is used -- In ox_target, it will append the options, resulting in N duplicate options if not targetModels[prop.model] then AddTargetModel(prop.model, { options = targetOptions, distance = 1.5, }) targetModels[prop.model] = true end end -- Delete the world object -- object = {coords = coords, model = itemModel} RegisterNetEvent("wp-placeables:client:deleteWorldObject", function(object) local entity = GetClosestObjectOfType(object.coords.x, object.coords.y, object.coords.z, 0.1, object.model, false, false, false) if DoesEntityExist(entity) then SetEntityAsMissionEntity(entity, 1, 1) DeleteObject(entity) SetEntityAsNoLongerNeeded(entity) end end) -- Runs a thread to loop through the deleted world objects table and removes the item if it exists -- This is to handle cases if the item were to have respawned -- Disabling this for now since we dont need to care if the item respawns -- There is currently an issue where this was being used for both player placed objects as well as world props -- If you placed an item, picked it up and placed the same item down again, the cleanup thread would delete it since its at the same coords. -- If we want to re-enable this, we need to find a way to only use this cleanup thread on world spawned props -- AddEventHandler('QBCore:Client:OnPlayerLoaded', function() -- QBCore.Functions.TriggerCallback('wp-placeables:server:GetDeletedWorldObjects', function(deletedObjects) -- objects = deletedObjects -- end) -- end) -- CreateThread(function() -- while true do -- for k = 1, #objects, 1 do -- v = objects[k] -- local entity = GetClosestObjectOfType(v.coords.x, v.coords.y, v.coords.z, 0.1, v.model, false, false, false) -- if DoesEntityExist(entity) then -- SetEntityAsMissionEntity(entity, 1, 1) -- DeleteObject(entity) -- SetEntityAsNoLongerNeeded(entity) -- end -- end -- Wait(10000) -- end -- end)