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