Deleting a frozen object left a stale rendered mesh on screen until the area re-streamed. Unfreezing right before DeleteObject clears it immediately, matching how safe:despawn already looked to behave.
304 lines
9.9 KiB
Lua
304 lines
9.9 KiB
Lua
local ox_target = exports.ox_target
|
|
local ox_inventory = exports.ox_inventory
|
|
|
|
local spawnedSafes = {}
|
|
|
|
-- Debug helper
|
|
local function dbg(...)
|
|
if Config and Config.Debug then
|
|
print('[0ixb-stashes CLIENT]', ...)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------
|
|
-- CHECAR COLISÃO
|
|
-------------------------------------------------------
|
|
local function isPlaceable(coords, radius)
|
|
local ped = PlayerPedId()
|
|
|
|
for _, obj in ipairs(GetGamePool('CObject')) do
|
|
if DoesEntityExist(obj) and #(GetEntityCoords(obj) - coords) < radius then
|
|
return false
|
|
end
|
|
end
|
|
|
|
for _, veh in ipairs(GetGamePool('CVehicle')) do
|
|
if DoesEntityExist(veh) and #(GetEntityCoords(veh) - coords) < radius then
|
|
return false
|
|
end
|
|
end
|
|
|
|
for _, p in ipairs(GetGamePool('CPed')) do
|
|
if p ~= ped and DoesEntityExist(p) and #(GetEntityCoords(p) - coords) < radius then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------
|
|
-- SPAWN SAFE
|
|
-------------------------------------------------------
|
|
RegisterNetEvent('safe:spawn', function(safeData)
|
|
if not safeData or not safeData.type or not Config.SafeTypes[safeData.type] then
|
|
dbg('safe:spawn invalid safeData', safeData)
|
|
return
|
|
end
|
|
|
|
local model = Config.SafeTypes[safeData.type].prop
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do Wait(0) end
|
|
|
|
local obj = CreateObject(
|
|
model,
|
|
safeData.coords.x,
|
|
safeData.coords.y,
|
|
safeData.coords.z,
|
|
false, false, false
|
|
)
|
|
|
|
--------------------------------------------------------------------
|
|
-- APLICAR ROTAÇÃO
|
|
-- PRIORIDADE:
|
|
-- 1) rx, ry, rz (se existir)
|
|
-- 2) heading antigo (fallback)
|
|
--------------------------------------------------------------------
|
|
if safeData.coords.rx and safeData.coords.ry and safeData.coords.rz then
|
|
-- rotação completa (verdadeira 3D)
|
|
SetEntityRotation(obj, safeData.coords.rx, safeData.coords.ry, safeData.coords.rz, 2, true)
|
|
else
|
|
-- fallback (como antes)
|
|
SetEntityHeading(obj, safeData.coords.h or 0.0)
|
|
end
|
|
|
|
FreezeEntityPosition(obj, true)
|
|
|
|
spawnedSafes[safeData.id] = obj
|
|
dbg('Spawning safe id', safeData.id, 'type', safeData.type, 'entity', obj)
|
|
|
|
--------------------------------------------------------------------
|
|
-- TARGET (mantido exatamente como estava)
|
|
--------------------------------------------------------------------
|
|
ox_target:addLocalEntity(obj, {
|
|
{
|
|
label = "» Abrir SafeBox",
|
|
icon = "fa-solid fa-lock",
|
|
onSelect = function()
|
|
dbg('Player clicou Abrir Safe', safeData.id)
|
|
local pin = lib.inputDialog('PIN do Safe', { 'Digite o PIN' })
|
|
if pin and pin[1] then
|
|
dbg('Player digitou PIN', pin[1])
|
|
TriggerServerEvent('safe:open', safeData.id, tostring(pin[1]))
|
|
else
|
|
dbg('Player cancelou input do PIN')
|
|
end
|
|
end
|
|
},
|
|
{
|
|
label = "» Eliminar SafeBox",
|
|
icon = "fa-solid fa-triangle-exclamation",
|
|
onSelect = function()
|
|
dbg('Player pediu Mostrar Alerta', safeData.id)
|
|
local result = lib.alertDialog({
|
|
header = 'CONFIRMAR REMOÇÃO DO SAFE',
|
|
content = 'Antes de prosseguir, certifique-se de que o safe está totalmente vazio.',
|
|
centered = true,
|
|
cancel = true,
|
|
size = 'lg'
|
|
})
|
|
|
|
if result == 'confirm' then
|
|
TriggerServerEvent('safe:pickup', safeData.id)
|
|
else
|
|
lib.notify({ type = 'inform', description = 'Operação cancelada.' })
|
|
end
|
|
end
|
|
}
|
|
})
|
|
end)
|
|
|
|
|
|
-------------------------------------------------------
|
|
-- DESPAWN SAFE
|
|
-------------------------------------------------------
|
|
RegisterNetEvent('safe:despawn', function(id)
|
|
local ent = spawnedSafes[id]
|
|
if ent and DoesEntityExist(ent) then
|
|
pcall(function() ox_target:removeLocalEntity(ent) end)
|
|
FreezeEntityPosition(ent, false)
|
|
DeleteObject(ent)
|
|
end
|
|
spawnedSafes[id] = nil
|
|
end)
|
|
|
|
-------------------------------------------------------
|
|
-- ABRIR INVENTÁRIO
|
|
-------------------------------------------------------
|
|
RegisterNetEvent('safe:openInventory', function(data)
|
|
if not data or not data.id then return end
|
|
|
|
ox_inventory:openInventory('stash', {
|
|
id = "safe_" .. tostring(data.id),
|
|
slots = data.slots,
|
|
weight = data.weight
|
|
})
|
|
end)
|
|
|
|
-------------------------------------------------------
|
|
-- FUNÇÕES DE CAMERA
|
|
-------------------------------------------------------
|
|
local function RotToDir(rot)
|
|
local z = math.rad(rot.z)
|
|
local x = math.rad(rot.x)
|
|
local num = math.abs(math.cos(x))
|
|
return vector3(-math.sin(z) * num, math.cos(z) * num, math.sin(x))
|
|
end
|
|
|
|
local function RayCastFromCam(distance)
|
|
local camRot = GetGameplayCamRot(2)
|
|
local camCoord = GetGameplayCamCoord()
|
|
local dir = RotToDir(camRot)
|
|
local dest = camCoord + dir * distance
|
|
local ray = StartShapeTestRay(camCoord.x, camCoord.y, camCoord.z, dest.x, dest.y, dest.z, -1, -1, 1)
|
|
local _, hit, endCoords, surfaceNormal, entity = GetShapeTestResult(ray)
|
|
return hit, endCoords, surfaceNormal, entity, camCoord, dir
|
|
end
|
|
|
|
-------------------------------------------------------
|
|
-- PLACEMENT / GIZMO
|
|
-------------------------------------------------------
|
|
RegisterNetEvent('safe:use', function(type)
|
|
if not Config.SafeTypes[type] then
|
|
exports.ox_lib:notify({ type='error', description = 'Tipo de safe inválido.' })
|
|
return
|
|
end
|
|
|
|
local ped = PlayerPedId()
|
|
local model = Config.SafeTypes[type].prop
|
|
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do Wait(0) end
|
|
|
|
local startPos = GetOffsetFromEntityInWorldCoords(ped, 0.0, 1.5, 0.0)
|
|
local startHeading = GetEntityHeading(ped)
|
|
|
|
local obj = CreateObject(model, startPos.x, startPos.y, startPos.z, false, false, false)
|
|
SetEntityHeading(obj, startHeading)
|
|
FreezeEntityPosition(obj, true)
|
|
|
|
SetNuiFocus(false, false)
|
|
Wait(50)
|
|
|
|
-- abrir gizmo
|
|
local ok, result = pcall(function()
|
|
return exports.object_gizmo:useGizmo(obj)
|
|
end)
|
|
|
|
if not ok or not result then
|
|
exports.ox_lib:notify({ type='error', description = 'Erro ao abrir gizmo.' })
|
|
if DoesEntityExist(obj) then DeleteObject(obj) end
|
|
return
|
|
end
|
|
|
|
-- posição final
|
|
local pos = result.position or GetEntityCoords(obj)
|
|
|
|
-- rotação TOTAL
|
|
local rot = result.rotation or GetEntityRotation(obj, 2)
|
|
|
|
local heading = result.heading or rot.z
|
|
|
|
-------------------------------------------------------
|
|
-- PEDIR PIN
|
|
-------------------------------------------------------
|
|
SetNuiFocus(false, false)
|
|
local pin = exports.ox_lib:inputDialog('PIN do Safe', {'Digite um PIN'})
|
|
SetNuiFocus(false, false)
|
|
|
|
if not pin or not pin[1] then
|
|
SetNuiFocus(false, false)
|
|
SetNuiFocusKeepInput(false)
|
|
SetCursorLocation(0.5, 0.5)
|
|
|
|
exports.ox_lib:notify({ type='error', description = 'Você cancelou a operação.' })
|
|
|
|
if DoesEntityExist(obj) then DeleteObject(obj) end
|
|
return
|
|
end
|
|
|
|
-- ENVIAR POSIÇÃO + ROTAÇÃO COMPLETA PARA O SERVIDOR
|
|
TriggerServerEvent('safe:place', type, {
|
|
x = pos.x,
|
|
y = pos.y,
|
|
z = pos.z,
|
|
h = heading,
|
|
rx = rot.x,
|
|
ry = rot.y,
|
|
rz = rot.z
|
|
}, tostring(pin[1]))
|
|
|
|
if DoesEntityExist(obj) then DeleteObject(obj) end
|
|
end)
|
|
|
|
|
|
-------------------------------------------------------
|
|
-- COMANDO DEBUG
|
|
-------------------------------------------------------
|
|
RegisterCommand('colocarsafe', function(_, args)
|
|
local typ = args[1]
|
|
if not typ or not Config.SafeTypes[typ] then
|
|
exports.ox_lib:notify({ type='error', description = 'Uso: /colocarsafe [small|medium|large]' })
|
|
return
|
|
end
|
|
TriggerEvent('safe:use', typ)
|
|
end)
|
|
|
|
-------------------------------------------------------
|
|
-- ITEM USO
|
|
-------------------------------------------------------
|
|
for type, data in pairs(Config.SafeTypes) do
|
|
if data.item then
|
|
pcall(function()
|
|
ox_inventory:useItem(data.item, function()
|
|
dbg('Player usou item', data.item)
|
|
TriggerEvent('safe:use', type)
|
|
end)
|
|
end)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------
|
|
-- CLEANUP
|
|
-------------------------------------------------------
|
|
Citizen.CreateThread(function()
|
|
while true do
|
|
Wait(1000)
|
|
for id, ent in pairs(spawnedSafes) do
|
|
if ent and not DoesEntityExist(ent) then
|
|
spawnedSafes[id] = nil
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
AddEventHandler('onClientResourceStart', function(resourceName)
|
|
if resourceName ~= GetCurrentResourceName() then return end
|
|
TriggerServerEvent('safe:requestSync')
|
|
end)
|
|
|
|
-- Roda ANTES do ambiente do script ser destruído, diferente de onClientResourceStart
|
|
-- (nesse ponto spawnedSafes já estaria vazio, pois o script já teria reiniciado)
|
|
AddEventHandler('onClientResourceStop', function(resourceName)
|
|
if resourceName ~= GetCurrentResourceName() then return end
|
|
|
|
for id, ent in pairs(spawnedSafes) do
|
|
if ent and DoesEntityExist(ent) then
|
|
pcall(function() ox_target:removeLocalEntity(ent) end)
|
|
FreezeEntityPosition(ent, false)
|
|
DeleteObject(ent)
|
|
end
|
|
spawnedSafes[id] = nil
|
|
end
|
|
end)
|