0ixb-stashes/client.lua
Bxio cfe36cb3b2 Despawn safes via server onResourceStop instead of client-side stop
onClientResourceStop doesn't fire reliably for a resource's own
client scripts when it stops itself, so props never got cleaned up.
The server now broadcasts safe:despawn (the same path already used
for pickup) for every known safe on onResourceStop, right before the
resource actually goes down.

Bump to 1.1.6.
2026-08-03 12:01:58 +00:00

289 lines
9.3 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)