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