Compare commits
No commits in common. "978bf68745abec36f1504820a4dfcc8a24eded6b" and "8c06997192559131b564a1601f4b9c9119d92994" have entirely different histories.
978bf68745
...
8c06997192
297
client.lua
297
client.lua
@ -1,297 +0,0 @@
|
|||||||
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
|
|
||||||
lib.notify({ type = 'success', description = 'Safe recolhido com sucesso!' })
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
TriggerServerEvent('safe:requestSync')
|
|
||||||
end)
|
|
||||||
61
config.lua
61
config.lua
@ -1,61 +0,0 @@
|
|||||||
Config = {}
|
|
||||||
|
|
||||||
-- Habilita prints de debug
|
|
||||||
Config.Debug = true
|
|
||||||
|
|
||||||
-- Tempo de animação ao colocar o safe (ms)
|
|
||||||
Config.PlaceTime = 2500
|
|
||||||
|
|
||||||
-- Segurança do PIN
|
|
||||||
Config.PinMaxAttempts = 3 -- tentativas erradas de PIN antes de bloquear o jogador
|
|
||||||
Config.PinBlockTime = 30 -- segundos de bloqueio após exceder as tentativas
|
|
||||||
|
|
||||||
-- Distância máxima (metros) para abrir ou recolher um safe
|
|
||||||
Config.SafeInteractDistance = 3.0
|
|
||||||
|
|
||||||
-- Sistema de update via Gitea (ativo apenas quando Config.Debug = true)
|
|
||||||
Config.Update = {
|
|
||||||
giteaUrl = "https://gitea.zol.oixb.run", -- URL base do seu Gitea
|
|
||||||
owner = "oixb.run", -- dono/organização do repositório
|
|
||||||
repo = "0ixb-stashes", -- nome do repositório
|
|
||||||
branch = "main", -- branch a ser usada
|
|
||||||
checkCommand = "oixbupdate", -- comando para checar se há update
|
|
||||||
upgradeCommand = "oixbupgrade", -- comando para baixar e aplicar o update
|
|
||||||
files = { "server.lua", "client.lua", "config.lua", "fxmanifest.lua" }
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Tipos de safes
|
|
||||||
Config.SafeTypes = {
|
|
||||||
small = {
|
|
||||||
label = "Small Safe",
|
|
||||||
prop = "prop_ld_int_safe_01", -- modelo de prop do GTA
|
|
||||||
item = "safe_small", -- item que o jogador precisa para colocar
|
|
||||||
slots = 30,
|
|
||||||
weight = 100000,
|
|
||||||
radius = 1.2
|
|
||||||
},
|
|
||||||
medium = {
|
|
||||||
label = "Medium Safe",
|
|
||||||
prop = "p_v_43_safe_s",
|
|
||||||
item = "safe_medium",
|
|
||||||
slots = 50,
|
|
||||||
weight = 250000,
|
|
||||||
radius = 1.5
|
|
||||||
},
|
|
||||||
large = {
|
|
||||||
label = "Large Safe",
|
|
||||||
prop = "xm3_prop_xm3_safe_01a",
|
|
||||||
item = "safe_large",
|
|
||||||
slots = 100,
|
|
||||||
weight = 500000,
|
|
||||||
radius = 2.0
|
|
||||||
},
|
|
||||||
dev = {
|
|
||||||
label = "Dev Safe",
|
|
||||||
prop = "m24_1_prop_m24_1_carrier_cargo_02a", -- modelo de prop do GTA
|
|
||||||
item = "safe_dev", -- item que o jogador precisa para colocar
|
|
||||||
slots = 5,
|
|
||||||
weight = 1000000,
|
|
||||||
radius = 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
fx_version 'cerulean'
|
|
||||||
game 'gta5'
|
|
||||||
|
|
||||||
author 'SeuNome'
|
|
||||||
description 'Sistema de safes com ox_inventory e ox_target'
|
|
||||||
version '1.1.0'
|
|
||||||
|
|
||||||
-- Dependências
|
|
||||||
dependencies {
|
|
||||||
'ox_inventory',
|
|
||||||
'ox_target',
|
|
||||||
'ox_lib',
|
|
||||||
'oxmysql'
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Shared / Config
|
|
||||||
shared_script '@ox_lib/init.lua'
|
|
||||||
shared_script 'config.lua'
|
|
||||||
|
|
||||||
|
|
||||||
-- Scripts do servidor
|
|
||||||
server_scripts {
|
|
||||||
'@oxmysql/lib/MySQL.lua', -- garante que o oxmysql esteja carregado
|
|
||||||
'server.lua'
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Scripts do cliente
|
|
||||||
client_scripts {
|
|
||||||
'client.lua'
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Comandos do console / debug
|
|
||||||
escrow_ignore {
|
|
||||||
'config.lua',
|
|
||||||
'client.lua',
|
|
||||||
'server.lua'
|
|
||||||
}
|
|
||||||
351
server.lua
351
server.lua
@ -1,351 +0,0 @@
|
|||||||
local ox_inventory = exports.ox_inventory
|
|
||||||
local safes = {}
|
|
||||||
|
|
||||||
-- Distância máxima (metros) entre o jogador e a posição enviada em safe:place
|
|
||||||
local PLACE_MAX_DISTANCE = 5.0
|
|
||||||
|
|
||||||
-- Tentativas de PIN por jogador: { [src] = { count = 0, blockedUntil = 0 } }
|
|
||||||
local pinAttempts = {}
|
|
||||||
|
|
||||||
-- Debug helper
|
|
||||||
local function dbg(...)
|
|
||||||
if Config and Config.Debug then
|
|
||||||
print('[0ixb-stashes SERVER]', ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Verifica se o jogador está fisicamente perto do safe
|
|
||||||
local function isNearSafe(src, safe, maxDist)
|
|
||||||
local ped = GetPlayerPed(src)
|
|
||||||
local playerCoords = GetEntityCoords(ped)
|
|
||||||
local safeCoords = vector3(safe.coords.x, safe.coords.y, safe.coords.z)
|
|
||||||
return #(playerCoords - safeCoords) <= (maxDist or Config.SafeInteractDistance)
|
|
||||||
end
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- CARREGAR SAFES DO DB AO INICIAR O RECURSO
|
|
||||||
-------------------------------------------------------
|
|
||||||
AddEventHandler('onResourceStart', function(resourceName)
|
|
||||||
if resourceName ~= GetCurrentResourceName() then return end
|
|
||||||
|
|
||||||
dbg('Resource started, carregando safes do DB...')
|
|
||||||
exports.oxmysql:fetch('SELECT * FROM safes', {}, function(result)
|
|
||||||
|
|
||||||
for _, v in ipairs(result) do
|
|
||||||
|
|
||||||
-- coords agora pode conter: x,y,z,h OU x,y,z,rx,ry,rz
|
|
||||||
local decoded = json.decode(v.coords)
|
|
||||||
|
|
||||||
local safeData = {
|
|
||||||
id = v.id,
|
|
||||||
owner = v.owner,
|
|
||||||
type = v.type,
|
|
||||||
coords = {
|
|
||||||
x = decoded.x,
|
|
||||||
y = decoded.y,
|
|
||||||
z = decoded.z,
|
|
||||||
h = decoded.h, -- fallback caso seja safe antigo
|
|
||||||
rx = decoded.rx, -- rotação completa (se existir)
|
|
||||||
ry = decoded.ry,
|
|
||||||
rz = decoded.rz
|
|
||||||
},
|
|
||||||
pin = v.pin
|
|
||||||
}
|
|
||||||
|
|
||||||
safes[v.id] = safeData
|
|
||||||
|
|
||||||
-- Registrar stash no Ox
|
|
||||||
local cfg = Config.SafeTypes[safeData.type]
|
|
||||||
if cfg then
|
|
||||||
exports.ox_inventory:RegisterStash(
|
|
||||||
"safe_"..safeData.id,
|
|
||||||
cfg.label,
|
|
||||||
cfg.slots,
|
|
||||||
cfg.weight,
|
|
||||||
safeData.owner
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Spawnar para quem está online
|
|
||||||
for _, pid in ipairs(GetPlayers()) do
|
|
||||||
for _, data in pairs(safes) do
|
|
||||||
TriggerClientEvent('safe:spawn', tonumber(pid), data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
dbg('Safes carregados e spawnados:', #result)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- SINCRONIZAR SAFE POR PEDIDO
|
|
||||||
-------------------------------------------------------
|
|
||||||
RegisterNetEvent('safe:requestSync', function()
|
|
||||||
local src = source
|
|
||||||
dbg('Player', src, 'pedindo sync de safes')
|
|
||||||
|
|
||||||
for _, data in pairs(safes) do
|
|
||||||
TriggerClientEvent('safe:spawn', src, data)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- COLOCAR SAFE (AGORA COM ROTAÇÃO COMPLETA)
|
|
||||||
-------------------------------------------------------
|
|
||||||
RegisterNetEvent('safe:place', function(type, coords, pin)
|
|
||||||
local src = source
|
|
||||||
local cfg = Config.SafeTypes[type]
|
|
||||||
if not cfg then return end
|
|
||||||
|
|
||||||
dbg('safe:place chamado por', src, 'type', type)
|
|
||||||
|
|
||||||
-- validar distância (evita coords arbitrárias vindas de um cliente modificado)
|
|
||||||
local ped = GetPlayerPed(src)
|
|
||||||
local playerCoords = GetEntityCoords(ped)
|
|
||||||
local placeCoords = vector3(coords.x, coords.y, coords.z)
|
|
||||||
|
|
||||||
if #(playerCoords - placeCoords) > PLACE_MAX_DISTANCE then
|
|
||||||
dbg('safe:place REJEITADO - distância suspeita', src, #(playerCoords - placeCoords))
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Posição inválida.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- remover item
|
|
||||||
local removed = exports.ox_inventory:RemoveItem(src, cfg.item, 1)
|
|
||||||
if not removed then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Você não tem esse item.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- obter identifier
|
|
||||||
local identifier = GetPlayerIdentifier(src, 1)
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
-- PREPARA COORDENADAS PARA SALVAR NO DB
|
|
||||||
-- AGORA SEMPRE SALVA: x,y,z,h,rx,ry,rz
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
local coordsToSave = json.encode({
|
|
||||||
x = coords.x,
|
|
||||||
y = coords.y,
|
|
||||||
z = coords.z,
|
|
||||||
h = coords.h, -- fallback
|
|
||||||
rx = coords.rx,
|
|
||||||
ry = coords.ry,
|
|
||||||
rz = coords.rz
|
|
||||||
})
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
-- SALVAR NO DB
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
exports.oxmysql:insert(
|
|
||||||
'INSERT INTO safes (owner, type, coords, pin) VALUES (?, ?, ?, ?)',
|
|
||||||
{ identifier, type, coordsToSave, pin },
|
|
||||||
function(id)
|
|
||||||
|
|
||||||
if not id then
|
|
||||||
exports.ox_inventory:AddItem(src, cfg.item, 1)
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Erro ao salvar o safe.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
-- GUARDAR NA MEMÓRIA DO SERVIDOR
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
local safeData = {
|
|
||||||
id = id,
|
|
||||||
owner = identifier,
|
|
||||||
type = type,
|
|
||||||
coords = coords,
|
|
||||||
pin = pin
|
|
||||||
}
|
|
||||||
|
|
||||||
safes[id] = safeData
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
-- REGISTRAR STASH
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
exports.ox_inventory:RegisterStash(
|
|
||||||
"safe_"..id,
|
|
||||||
cfg.label,
|
|
||||||
cfg.slots,
|
|
||||||
cfg.weight,
|
|
||||||
identifier
|
|
||||||
)
|
|
||||||
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
-- ENVIAR PARA TODOS
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
TriggerClientEvent('safe:spawn', -1, safeData)
|
|
||||||
TriggerClientEvent('ox_lib:notify', src, {type='success', description='Safe colocado.'})
|
|
||||||
|
|
||||||
dbg('Safe colocado id', id, 'por', identifier)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- ABRIR SAFE
|
|
||||||
-------------------------------------------------------
|
|
||||||
RegisterNetEvent('safe:open', function(id, pin)
|
|
||||||
local src = source
|
|
||||||
local now = os.time()
|
|
||||||
local attempt = pinAttempts[src]
|
|
||||||
|
|
||||||
if attempt and now < attempt.blockedUntil then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Muitas tentativas erradas. Aguarde.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
local safe = safes[id]
|
|
||||||
|
|
||||||
if not safe then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Safe inexistente.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
if not isNearSafe(src, safe) then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Você está muito longe do safe.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
if tostring(pin) ~= tostring(safe.pin) then
|
|
||||||
attempt = attempt or { count = 0, blockedUntil = 0 }
|
|
||||||
attempt.count = attempt.count + 1
|
|
||||||
|
|
||||||
if attempt.count >= Config.PinMaxAttempts then
|
|
||||||
attempt.blockedUntil = now + Config.PinBlockTime
|
|
||||||
attempt.count = 0
|
|
||||||
dbg('Jogador', src, 'bloqueado por tentativas de PIN erradas')
|
|
||||||
end
|
|
||||||
|
|
||||||
pinAttempts[src] = attempt
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='PIN incorreto.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
pinAttempts[src] = nil
|
|
||||||
|
|
||||||
local cfg = Config.SafeTypes[safe.type]
|
|
||||||
TriggerClientEvent('safe:openInventory', src, {id=id, slots=cfg.slots, weight=cfg.weight})
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- RECOLHER SAFE
|
|
||||||
-------------------------------------------------------
|
|
||||||
RegisterNetEvent('safe:pickup', function(id)
|
|
||||||
local src = source
|
|
||||||
local safe = safes[id]
|
|
||||||
|
|
||||||
if not safe then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Safe inexistente.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
local identifier = GetPlayerIdentifier(src, 1)
|
|
||||||
if safe.owner ~= identifier then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Você não é o dono.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
if not isNearSafe(src, safe) then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='Você está muito longe do safe.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
local cfg = Config.SafeTypes[safe.type]
|
|
||||||
|
|
||||||
-- Bloqueia o recolhimento se ainda houver itens dentro (evita perda de itens)
|
|
||||||
-- Nota: confirme se "GetInventoryItems" é o nome do export na sua versão do ox_inventory
|
|
||||||
local items = exports.ox_inventory:GetInventoryItems("safe_"..id)
|
|
||||||
if items and next(items) then
|
|
||||||
return TriggerClientEvent('ox_lib:notify', src, {type='error', description='O safe ainda tem itens dentro.'})
|
|
||||||
end
|
|
||||||
|
|
||||||
exports.oxmysql:execute('DELETE FROM ox_inventory WHERE name=?', {'safe_'..id})
|
|
||||||
exports.oxmysql:execute('DELETE FROM safes WHERE id=?', {id})
|
|
||||||
safes[id] = nil
|
|
||||||
|
|
||||||
exports.ox_inventory:ClearInventory("safe_"..id)
|
|
||||||
TriggerClientEvent('safe:despawn', -1, id)
|
|
||||||
|
|
||||||
exports.ox_inventory:AddItem(src, cfg.item, 1)
|
|
||||||
TriggerClientEvent('ox_lib:notify', src, {type='success', description='Safe recolhido e item devolvido.'})
|
|
||||||
end)
|
|
||||||
|
|
||||||
-------------------------------------------------------
|
|
||||||
-- UPDATE CHECKER / UPGRADE (via Gitea)
|
|
||||||
-- Só existe enquanto Config.Debug = true. Em produção, com
|
|
||||||
-- Config.Debug = false, este bloco inteiro nem registra os comandos.
|
|
||||||
-------------------------------------------------------
|
|
||||||
if Config.Debug and Config.Update then
|
|
||||||
local function giteaRawUrl(file)
|
|
||||||
return ('%s/api/v1/repos/%s/%s/raw/%s?ref=%s'):format(
|
|
||||||
Config.Update.giteaUrl, Config.Update.owner, Config.Update.repo, file, Config.Update.branch
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function checkForUpdate(cb)
|
|
||||||
PerformHttpRequest(giteaRawUrl('version.json'), function(status, body)
|
|
||||||
if status ~= 200 or not body then
|
|
||||||
return cb(false, ('não foi possível contatar o Gitea (status %s)'):format(tostring(status)))
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, remote = pcall(json.decode, body)
|
|
||||||
if not ok or not remote or not remote.version then
|
|
||||||
return cb(false, 'resposta inválida em version.json')
|
|
||||||
end
|
|
||||||
|
|
||||||
cb(true, remote.version)
|
|
||||||
end, 'GET', '', { ['Content-Type'] = 'application/json' })
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterCommand(Config.Update.checkCommand, function(src)
|
|
||||||
local current = GetResourceMetadata(GetCurrentResourceName(), 'version', 0)
|
|
||||||
|
|
||||||
checkForUpdate(function(ok, result)
|
|
||||||
if not ok then
|
|
||||||
dbg('Erro ao checar update:', result)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if result ~= current then
|
|
||||||
dbg(('Nova versão disponível: %s (atual: %s). Use /%s para atualizar.')
|
|
||||||
:format(result, current, Config.Update.upgradeCommand))
|
|
||||||
else
|
|
||||||
dbg('Já está na versão mais recente ('..current..').')
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end, true)
|
|
||||||
|
|
||||||
RegisterCommand(Config.Update.upgradeCommand, function(src)
|
|
||||||
local resourceName = GetCurrentResourceName()
|
|
||||||
local files = Config.Update.files
|
|
||||||
local pending = #files
|
|
||||||
local failed = false
|
|
||||||
|
|
||||||
dbg('Iniciando upgrade, baixando', pending, 'arquivo(s)...')
|
|
||||||
|
|
||||||
for _, file in ipairs(files) do
|
|
||||||
PerformHttpRequest(giteaRawUrl(file), function(status, body)
|
|
||||||
if status == 200 and body and #body > 0 then
|
|
||||||
SaveResourceFile(resourceName, file, body, #body)
|
|
||||||
dbg('Baixado:', file)
|
|
||||||
else
|
|
||||||
failed = true
|
|
||||||
dbg(('Falha ao baixar %s (status %s)'):format(file, tostring(status)))
|
|
||||||
end
|
|
||||||
|
|
||||||
pending = pending - 1
|
|
||||||
if pending == 0 then
|
|
||||||
if failed then
|
|
||||||
dbg('Upgrade incompleto — algum arquivo falhou. Resource NÃO foi reiniciado.')
|
|
||||||
else
|
|
||||||
dbg('Upgrade concluído. Reiniciando resource...')
|
|
||||||
ExecuteCommand('restart '..resourceName)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end, 'GET')
|
|
||||||
end
|
|
||||||
end, true)
|
|
||||||
|
|
||||||
dbg('Update checker ativo (Config.Debug = true). Comandos: /'..Config.Update.checkCommand..' /'..Config.Update.upgradeCommand)
|
|
||||||
end
|
|
||||||
Loading…
x
Reference in New Issue
Block a user