Files
community_bridge/lib/placers/client/placeable_object.lua
T
gononono64 3fa933fab1 Cleanup and placeable_object replacement
Added cleaned up and improved functionality for placeable object.
Example is in unit tests.

Cleanup whitespace and removed accessibility as not used/not documented and doesnt appear to be useful with our current work flow
2025-06-04 15:29:21 -05:00

579 lines
22 KiB
Lua

Scaleform = Scaleform or Require("lib/scaleform/client/scaleform.lua")
Utility = Utility or Require("lib/utility/client/utility.lua")
Raycast = Raycast or Require("lib/raycast/client/raycast.lua")
Language = Language or Require("modules/locales/shared.lua")
PlaceableObject = PlaceableObject or {}
-- Local variables
local isPlacing = false
local currentEntity = nil
local currentMode = 'normal' -- 'normal' or 'movement'
local currentPromise = nil -- Store the current promise
-- Utility functions
local function eyetrace(depth, disableSphere)
local screenX = GetDisabledControlNormal(0, 239)
local screenY = GetDisabledControlNormal(0, 240)
local world, normal = GetWorldCoordFromScreenCoord(screenX, screenY)
local playerPos = GetEntityCoords(PlayerPedId())
local target = playerPos + normal * depth
if not disableSphere then
DrawSphere(target.x, target.y, target.z, 0.5, 255, 0, 0, 0.5)
end
return target
end
local function inBoundary(pos, boundary)
if not boundary then return true end
local x, y, z = table.unpack(pos)
local minX, minY, minZ = table.unpack(boundary.min)
local maxX, maxY, maxZ = table.unpack(boundary.max)
return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
end
local function checkBoundaryAndMaterials(entity, settings)
local inBounds = true
if settings.allowedMats or settings.boundary then
CreateThread(function()
while isPlacing do
local hit, _, coords, _, materialHash
if currentMode == 'movement' then
local entityCoords = GetEntityCoords(entity)
local destination = entityCoords - vector3(0, 0, settings.maxDepth or 10)
hit, _, coords, _, materialHash = Raycast.ToCoords(entityCoords, destination, 1, 4)
else
hit, _, coords, _, materialHash = Raycast.FromCamera(1, 4)
end
local validMaterial = not settings.allowedMats or settings.allowedMats[materialHash]
local validBoundary = inBoundary(GetEntityCoords(entity), settings.boundary)
if hit and hit ~= 1 and validMaterial and validBoundary then
if not inBounds then
inBounds = true
SetEntityDrawOutlineColor(0, 255, 0, 255)
SetEntityDrawOutline(entity, true)
end
elseif inBounds then
inBounds = false
SetEntityDrawOutline(entity, false)
end
Wait(500)
end
SetEntityDrawOutline(entity, false)
end)
end
return function() return inBounds end
end
local function setupInstructionalButtons(settings)
local buttons = {
{type = "CLEAR_ALL"},
{type = "SET_CLEAR_SPACE", int = 200},
}
-- Common buttons
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.place_object?.name or 'Place Object:', keyIndex = settings.config?.place_object?.key or {223}, int = 5})
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.cancel_placement?.name or 'Cancel Placement:', keyIndex = settings.config?.cancel_placement?.key or {222}, int = 4})
if currentMode == 'normal' then
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.snap_to_ground?.name or 'Snap to Ground:', keyIndex = settings.config?.snap_to_ground?.key or {19}, int = 1})
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.rotate?.name or 'Rotate:', keyIndex = settings.config?.rotate?.key or {14, 15}, int = 2})
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.distance?.name or 'Distance:', keyIndex = settings.config?.distance?.key or {14,15,36}, int = 3})
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.toggle_placement?.name or 'Toggle Placement:', keyIndex = settings.config?.toggle_placement?.key or {199}, int = 0})
-- Add vertical controls if allowed
if settings.allowVertical then
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move Up/Down:', keyIndex = {85, 48}, int = 8})
end
if settings.allowMovement then
table.insert(buttons, {type = "SET_DATA_SLOT", name = settings.config?.movement?.name or 'Toggle Movement Mode:', keyIndex = settings.config?.movement?.key or {38}, int = 6})
end
elseif currentMode == 'movement' then
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move Forward/Back:', keyIndex = {32, 33}, int = 1})
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move Left/Right:', keyIndex = {34, 35}, int = 2})
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move Up/Down:', keyIndex = {85, 48}, int = 3})
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate Left/Right:', keyIndex = {174, 175}, int = 4})
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Snap to Ground:', keyIndex = {19}, int = 5})
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Confirm (Enter):', keyIndex = {191}, int = 6})
if settings.allowNormal then
table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Normal Mode:', keyIndex = {38}, int = 7})
end
end
table.insert(buttons, {type = "DRAW_INSTRUCTIONAL_BUTTONS"})
table.insert(buttons, {type = "SET_BACKGROUND_COLOUR"})
return Scaleform.SetupInstructionalButtons(buttons)
end
local function normalPlacementLoop(entity, settings, getBoundsStatus)
-- Initialize or retrieve from settings to maintain state
settings.currentDepth = settings.currentDepth or settings.depth or 10.0
settings.currentHeading = settings.currentHeading or -GetEntityHeading(PlayerPedId())
settings.currentHeight = settings.currentHeight or 0.0 -- Add height tracking
settings.placeOnGround = settings.placeOnGround ~= nil and settings.placeOnGround or not settings.allowVertical -- Default based on allowVertical
settings.paused = settings.paused or false
local depth = settings.currentDepth
local heading = settings.currentHeading
local height = settings.currentHeight
local placeOnGround = settings.placeOnGround
local paused = settings.paused
local pos
-- Don't use while loop here, just process one frame
if not isPlacing or currentMode ~= 'normal' then
return
end
if paused then
if IsControlJustPressed(0, 199) then -- home
paused = not paused
settings.paused = paused
end
return
end
-- Disable controls
DisableControlAction(0, 24, true) -- Attack
DisableControlAction(0, 25, true) -- Aim
DisableControlAction(0, 36, true) -- INPUT_DUCK
-- Place object (left click)
if IsDisabledControlJustPressed(0, 223) then
if not getBoundsStatus() then
print(Language.Locale("placeable_object.cant_place_here"))
else
pos = GetEntityCoords(entity)
isPlacing = false
if currentPromise then
currentPromise:resolve({
entity = entity,
position = pos,
heading = heading
})
currentPromise = nil
end
DeleteEntity(entity)
return
end
end
-- Cancel (right click)
if IsDisabledControlJustPressed(0, 25) then
isPlacing = false
if currentPromise then
currentPromise:resolve(nil) -- Return nil for cancel
currentPromise = nil
end
DeleteEntity(entity)
return
end
-- Toggle snap to ground (Alt)
if IsControlJustPressed(0, 19) then
if settings.allowVertical then
placeOnGround = not placeOnGround
settings.placeOnGround = placeOnGround
if placeOnGround then
height = 0.0 -- Reset height when snapping to ground
settings.currentHeight = height
end
else
-- Force snap to ground if vertical movement not allowed
PlaceObjectOnGroundProperly(entity)
end
end
-- Switch to movement mode
if settings.allowMovement and IsControlJustPressed(0, 38) then
currentMode = 'movement'
return
end
-- Vertical controls (only if allowVertical is enabled and not snapped to ground)
if settings.allowVertical and not placeOnGround then
if IsControlPressed(0, 85) then -- Q - Move up
height = height + (settings.heightStep or 0.1)
settings.currentHeight = height
elseif IsControlPressed(0, 48) then -- Z - Move down
height = height - (settings.heightStep or 0.1)
settings.currentHeight = height
end
end
-- Depth and rotation controls
if IsControlPressed(0, 224) then -- left ctrl
if IsControlJustPressed(0, 15) then -- D key
depth = math.min(depth + settings.depthStep, settings.depthMax)
elseif IsControlJustPressed(0, 14) then -- A key
depth = math.max(depth - settings.depthStep, settings.depthMin)
end
else
if IsControlJustPressed(0, 15) then -- D key
heading = (heading + settings.rotationStep) % 360
elseif IsControlJustPressed(0, 14) then -- A key
heading = (heading - settings.rotationStep) % 360
end
end
-- Update position
pos = eyetrace(depth, settings.disableSphere)
-- Add height offset if not snapping to ground
if settings.allowVertical and not placeOnGround then
pos = pos + vector3(0, 0, height)
end
if entity then
SetEntityCoords(entity, pos.x, pos.y, pos.z)
SetEntityHeading(entity, heading)
if placeOnGround then
PlaceObjectOnGroundProperly(entity)
pos = GetEntityCoords(entity)
end
end
-- Store updated values back to settings
settings.currentDepth = depth
settings.currentHeading = heading
settings.currentHeight = height
settings.placeOnGround = placeOnGround
end
local function movementPlacementLoop(entity, settings, getBoundsStatus)
-- Don't use while loop here, just process one frame
if not isPlacing or currentMode ~= 'movement' or not DoesEntityExist(entity) then
return
end
if IsEntityAPed(entity) then
SetEntityAlpha(entity, 200)
end
-- Disable movement controls to prevent player from moving
DisableControlAction(0, 30, true) -- Move Left/Right
DisableControlAction(0, 31, true) -- Move Forward/Back
DisableControlAction(0, 36, true) -- INPUT_DUCK
DisableControlAction(0, 21, true) -- Sprint
DisableControlAction(0, 22, true) -- Jump
SetEntityCollision(entity, false, false)
FreezeEntityPosition(entity, false)
-- Get current entity position and rotation
local coords = GetEntityCoords(entity)
local heading = GetEntityHeading(entity)
local moved = false
-- Movement speed settings
local moveSpeed = 0.1
local fastMoveSpeed = 0.5
local rotateSpeed = 2.0
-- Check if shift is held for faster movement
local isFastMode = IsControlPressed(0, 21) -- Left Shift
local currentMoveSpeed = isFastMode and fastMoveSpeed or moveSpeed
-- Get camera rotation for relative movement
local camRot = GetGameplayCamRot(2)
local camHeading = math.rad(camRot.z)
-- Calculate camera-relative directions
local camForward = vector3(-math.sin(camHeading), math.cos(camHeading), 0)
local camRight = vector3(math.cos(camHeading), math.sin(camHeading), 0)
-- WASD movement (relative to camera direction)
if IsControlPressed(0, 32) then -- W - Move forward relative to camera
coords = coords + camForward * currentMoveSpeed
moved = true
elseif IsControlPressed(0, 33) then -- S - Move backward relative to camera
coords = coords - camForward * currentMoveSpeed
moved = true
end
if IsControlPressed(0, 34) then -- A - Move left relative to camera
coords = coords - camRight * currentMoveSpeed
moved = true
elseif IsControlPressed(0, 35) then -- D - Move right relative to camera
coords = coords + camRight * currentMoveSpeed
moved = true
end
-- Up/Down movement (always world-relative)
if settings.allowVertical and IsControlPressed(0, 85) then -- Q - Move up
coords = coords + vector3(0, 0, currentMoveSpeed)
moved = true
elseif settings.allowVertical and IsControlPressed(0, 48) then -- Z - Move down
coords = coords + vector3(0, 0, -currentMoveSpeed)
moved = true
end
-- Rotation controls
if IsControlPressed(0, 174) then -- Left Arrow - Rotate left
heading = heading - rotateSpeed
moved = true
elseif IsControlPressed(0, 175) then -- Right Arrow - Rotate right
heading = heading + rotateSpeed
moved = true
end
-- Alternative rotation with mouse wheel
if IsControlJustPressed(0, 241) then -- Mouse wheel up
heading = heading + 15.0
moved = true
elseif IsControlJustPressed(0, 242) then -- Mouse wheel down
heading = heading - 15.0
moved = true
end
-- Apply movement if any occurred
if moved then
SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false)
SetEntityHeading(entity, heading)
end
-- Confirm placement (Enter)
if IsControlJustPressed(0, 191) then
if not getBoundsStatus() then
print(Language.Locale("placeable_object.cant_place_here"))
return
end
isPlacing = false
if currentPromise then
currentPromise:resolve({
entity = entity,
position = GetEntityCoords(entity),
heading = GetEntityHeading(entity),
rotation = GetEntityRotation(entity)
})
currentPromise = nil
end
DeleteEntity(entity)
return
end
-- Cancel placement (Backspace)
if IsControlJustPressed(0, 202) then
isPlacing = false
if currentPromise then
currentPromise:resolve(nil) -- Return nil for cancel
currentPromise = nil
end
DeleteEntity(entity)
return
end
-- Switch to normal mode (E)
if settings.allowNormal and IsControlJustPressed(0, 38) then
currentMode = 'normal'
local playerPos = GetEntityCoords(PlayerPedId())
local entityPos = GetEntityCoords(entity)
local depth = #(playerPos - entityPos)
settings.currentDepth = depth
return
end
-- Snap to ground (Alt)
if IsControlJustPressed(0, 19) then
PlaceObjectOnGroundProperly(entity)
end
FreezeEntityPosition(entity, false)
end
local function drawBoundaryBox(boundary)
if not boundary then return end
local min = boundary.min
local max = boundary.max
-- Define the 8 corners of the box
local corners = {
vector3(min.x, min.y, min.z), -- 1: bottom front left
vector3(max.x, min.y, min.z), -- 2: bottom front right
vector3(max.x, max.y, min.z), -- 3: bottom back right
vector3(min.x, max.y, min.z), -- 4: bottom back left
vector3(min.x, min.y, max.z), -- 5: top front left
vector3(max.x, min.y, max.z), -- 6: top front right
vector3(max.x, max.y, max.z), -- 7: top back right
vector3(min.x, max.y, max.z), -- 8: top back left
}
-- Draw the box faces as triangles (2 triangles per face)
local alpha = 50 -- Semi-transparent
local r, g, b = 0, 255, 0 -- Green color
-- Bottom face (looking up from below)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[2].x, corners[2].y, corners[2].z, corners[3].x, corners[3].y, corners[3].z, r, g, b, alpha)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[3].x, corners[3].y, corners[3].z, corners[4].x, corners[4].y, corners[4].z, r, g, b, alpha)
-- Top face (looking down from above)
DrawPoly(corners[5].x, corners[5].y, corners[5].z, corners[7].x, corners[7].y, corners[7].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, alpha)
DrawPoly(corners[5].x, corners[5].y, corners[5].z, corners[8].x, corners[8].y, corners[8].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, alpha)
-- Front face (min Y)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[5].x, corners[5].y, corners[5].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, alpha)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[6].x, corners[6].y, corners[6].z, corners[2].x, corners[2].y, corners[2].z, r, g, b, alpha)
-- Back face (max Y)
DrawPoly(corners[4].x, corners[4].y, corners[4].z, corners[7].x, corners[7].y, corners[7].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, alpha)
DrawPoly(corners[4].x, corners[4].y, corners[4].z, corners[3].x, corners[3].y, corners[3].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, alpha)
-- Left face (min X)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[4].x, corners[4].y, corners[4].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, alpha)
DrawPoly(corners[1].x, corners[1].y, corners[1].z, corners[8].x, corners[8].y, corners[8].z, corners[5].x, corners[5].y, corners[5].z, r, g, b, alpha)
-- Right face (max X)
DrawPoly(corners[2].x, corners[2].y, corners[2].z, corners[6].x, corners[6].y, corners[6].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, alpha)
DrawPoly(corners[2].x, corners[2].y, corners[2].z, corners[7].x, corners[7].y, corners[7].z, corners[3].x, corners[3].y, corners[3].z, r, g, b, alpha)
end
-- Main placement function - now uses proper promise pattern
function PlaceableObject.Create(model, settings)
if isPlacing then
return nil -- Already placing
end
settings = settings or {}
-- Default settings
local config = {
depthMin = settings.depthMin or 2.0,
depthMax = settings.depthMax or 10.0,
rotationStep = settings.rotationStep or 15.0,
depthStep = settings.depthStep or 1.0,
heightStep = settings.heightStep or 0.1, -- New height step setting
disableSphere = settings.useSphere and false or true,
depth = settings.depth or 10.0,
allowMovement = settings.allowMovement ~= false, -- default true
allowNormal = settings.allowNormal ~= false, -- default true
allowVertical = settings.allowVertical ~= false, -- default true - allow vertical movement in normal mode
startMode = settings.startMode or 'normal', -- 'normal' or 'movement'
allowedMats = settings.allowedMats,
boundary = settings.boundary,
config = settings.config or {},
maxDepth = settings.maxDepth or 10,
showInstructionalButtons = settings.showInstructionalButtons
}
-- Create the promise and store it globally
currentPromise = promise.new()
isPlacing = true
currentMode = config.startMode
-- Create entity if model provided
if model then
local heading = -GetEntityHeading(PlayerPedId())
local pos = eyetrace(config.depth, config.disableSphere)
local obj = Utility.CreateProp(model, pos, vector3(0, 0, heading), nil)
currentEntity = obj
else
currentEntity = settings.entity
end
if not currentEntity then
isPlacing = false
if currentPromise then
currentPromise:resolve(nil)
currentPromise = nil
end
return nil
end
-- Setup entity
SetEntityCollision(currentEntity, false, false)
SetEntityAlpha(currentEntity, 150, false)
SetPedConfigFlag(PlayerPedId(), 146, true)
SetCanClimbOnEntity(currentEntity, false)
SetEntityCompletelyDisableCollision(currentEntity, true, false)
SetEntityNoCollisionEntity(PlayerPedId(), currentEntity, false)
-- Setup boundary checking
local getBoundsStatus = checkBoundaryAndMaterials(currentEntity, config)
-- Setup instructional buttons only if enabled
local scaleform = nil
if config.showInstructionalButtons then
scaleform = setupInstructionalButtons(config)
end
-- Main loop
CreateThread(function()
local lastMode = currentMode
while isPlacing do
-- Update instructional buttons if mode changed and buttons are enabled
if config.showInstructionalButtons and (not lastMode or lastMode ~= currentMode) then
scaleform = setupInstructionalButtons(config)
lastMode = currentMode
end
-- Draw instructional buttons only if enabled
if config.showInstructionalButtons and scaleform then
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
end
-- Draw boundary box if defined
if config.boundary then
drawBoundaryBox(config.boundary)
end
if currentMode == 'normal' and config.allowNormal then
normalPlacementLoop(currentEntity, config, getBoundsStatus)
elseif currentMode == 'movement' and config.allowMovement then
movementPlacementLoop(currentEntity, config, getBoundsStatus)
end
Wait(0)
end
-- Cleanup
if DoesEntityExist(currentEntity) then
if IsEntityAPed(currentEntity) then
SetEntityAlpha(currentEntity, 255)
end
SetEntityDrawOutline(currentEntity, false)
end
currentEntity = nil
end)
-- Return Citizen.Await on the promise
return Citizen.Await(currentPromise)
end
-- Stop placement function
function PlaceableObject.Stop()
if not isPlacing then return false end
isPlacing = false
if currentEntity and DoesEntityExist(currentEntity) then
DeleteEntity(currentEntity)
end
return true
end
-- Get current status
function PlaceableObject.IsPlacing()
return isPlacing
end
function PlaceableObject.GetCurrentEntity()
return currentEntity
end
function PlaceableObject.GetCurrentMode()
return currentMode
end
return PlaceableObject