The cleanup loop ran on onClientResourceStart, but spawnedSafes is already a fresh empty table by then, so it never actually deleted anything. Props from before a restart stayed orphaned in the world and got duplicated by the requestSync on start. Moved the cleanup to onClientResourceStop, which runs while the entities are still known.
302 lines
9.8 KiB
Lua
302 lines
9.8 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)
|
|
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)
|
|
DeleteObject(ent)
|
|
end
|
|
spawnedSafes[id] = nil
|
|
end
|
|
end)
|