Sistema de cerraduras para puertas de FiveM: programa puertas y llaves seguras
Añade cerraduras a tu servidor de FiveM. Configura llaves, puertas por trabajo y scripts de cerraduras para comisarías, casas y negocios.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Por qué importan las cerraduras de puertas en el roleplay
Los sistemas de cerraduras son uno de los componentes más infravalorados, pero esenciales, de cualquier servidor de roleplay serio de FiveM. Sin una gestión adecuada de las puertas, los jugadores pueden entrar sin restricciones en armerías policiales, salas del personal del hospital y escondites de bandas, lo que destruye por completo la inmersión. Un sistema de cerraduras bien implementado permite a los propietarios del servidor controlar el acceso a cualquier interior del mapa, desde edificios gubernamentales hasta negocios propiedad de jugadores. Crea límites naturales que impulsan interacciones de roleplay, como intercambios de llaves, situaciones de ganzuado y asaltos coordinados. En esta guía, te explicaremos cómo crear un sistema completo de cerraduras usando ox_doorlock como base y ampliarlo con mecánicas personalizadas de ganzuado, autenticación con tarjetas de acceso, grupos de puertas y control de acceso específico para negocios.
Configuración de ox_doorlock
El resource ox_doorlock es la solución de cerraduras más utilizada en el ecosistema de FiveM. Ofrece una API sencilla, una interfaz de administración integrada para colocar puertas y compatibilidad tanto con puertas dobles como con puertas animadas de garaje. Antes de escribir código personalizado, necesitas una base sólida. Instala ox_doorlock junto con ox_lib, del que depende para los componentes de interfaz y las funciones de utilidad. Tu server.cfg debe garantizar que los resources se inicien en el orden correcto, primero la base de datos, luego ox_lib y, por último, ox_doorlock. Una vez iniciado, el comando de administración integrado te permite acercarte a cualquier puerta del mundo del juego y registrarla con un clic.
-- server.cfg load order
ensure oxmysql
ensure ox_lib
ensure ox_doorlock
-- Grant admin access for door placement
add_ace group.admin command.doorlock allow
Después de registrar las puertas desde la interfaz de administración, ox_doorlock guarda cada entrada en la base de datos con las coordenadas, la orientación, el hash del modelo y el estado de bloqueo. Puedes consultar estos datos y modificarlos mediante código. El resource ofrece exports y eventos para activar o desactivar cerraduras, consultar su estado y gestionar el acceso. Comprender la estructura de los datos es fundamental antes de crear funciones personalizadas sobre esta base.
-- Checking if a specific door is locked
local doorId = 1
local isLocked = exports.ox_doorlock:getDoorState(doorId)
-- Toggle a door lock from server side
exports.ox_doorlock:setDoorState(doorId, not isLocked)
-- Listen for door state changes
AddEventHandler('ox_doorlock:stateChanged', function(id, state, source)
print(('Door %d changed to %s by player %s'):format(id, state and 'locked' or 'unlocked', source))
end)
Creación de una mecánica de ganzuado
El ganzuado añade una capa de juego que da sentido a las puertas cerradas para los personajes criminales. En lugar de ser barreras infranqueables, se convierten en desafíos basados en la habilidad. El mejor enfoque es un minijuego que requiera precisión y buen ritmo para que el resultado parezca merecido y no aleatorio. Usaremos el sistema skillcheck integrado de ox_lib para crear un proceso de ganzuado que requiera que el jugador tenga una ganzúa en el inventario, active una animación y complete una prueba de habilidad de varias etapas. Si no la supera, debería haber consecuencias, como que se rompa la ganzúa o que se alerte a la policía cercana mediante un evento de dispatch.
-- client/lockpicking.lua
local function AttemptLockpick(doorId)
local hasLockpick = exports.ox_inventory:Search('count', 'lockpick')
if hasLockpick < 1 then
lib.notify({ title = 'No Lockpick', type = 'error' })
return
end
-- Play lockpicking animation
local ped = PlayerPedId()
TaskPlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandlenry', 8.0, -8.0, -1, 1, 0, false, false, false)
-- Multi-stage skillcheck: easy, medium, hard
local success = lib.skillCheck({'easy', 'medium', 'hard'}, {'w', 'a', 's', 'd'})
ClearPedTasks(ped)
if success then
TriggerServerEvent('doorlock:lockpick:success', doorId)
lib.notify({ title = 'Lock Picked', description = 'The door clicks open.', type = 'success' })
else
TriggerServerEvent('doorlock:lockpick:fail', doorId)
lib.notify({ title = 'Failed', description = 'The lockpick snapped.', type = 'error' })
end
end
En el servidor, debes validar el intento de ganzuado, quitar la ganzúa si falla, desbloquear la puerta si tiene éxito y, opcionalmente, enviar una alerta policial. Es importante limitar la frecuencia para evitar intentos repetidos. Guarda un tiempo de espera por jugador y puerta para que no pueda volver a intentarlo inmediatamente después de fallar. El handler del servidor también debería verificar que el jugador esté realmente cerca de la puerta que dice estar forzando, para evitar exploits remotos.
-- server/lockpicking.lua
local cooldowns = {}
RegisterNetEvent('doorlock:lockpick:success', function(doorId)
local src = source
local playerCoords = GetEntityCoords(GetPlayerPed(src))
-- Verify proximity to door (anti-cheat)
local doorData = exports.ox_doorlock:getDoor(doorId)
if not doorData then return end
if #(playerCoords - doorData.coords) > 3.0 then return end
-- Check cooldown
local key = ('%s:%s'):format(src, doorId)
if cooldowns[key] and os.time() - cooldowns[key] < 30 then return end
exports.ox_doorlock:setDoorState(doorId, 0) -- Unlock
cooldowns[key] = os.time()
-- Auto-relock after 60 seconds
SetTimeout(60000, function()
exports.ox_doorlock:setDoorState(doorId, 1)
end)
end)
RegisterNetEvent('doorlock:lockpick:fail', function(doorId)
local src = source
exports.ox_inventory:RemoveItem(src, 'lockpick', 1)
-- Alert police dispatch
TriggerEvent('dispatch:alert', {
coords = GetEntityCoords(GetPlayerPed(src)),
message = 'Attempted break-in reported',
code = '10-31',
job = 'police'
})
end)
Implementación de un sistema de tarjetas de acceso
Las tarjetas de acceso ofrecen un mecanismo de control más estructurado que los objetos de llave simples. Funcionan bien para edificios corporativos, instalaciones gubernamentales y zonas restringidas donde el acceso debe depender de niveles de autorización específicos. El sistema asigna un nivel de seguridad del 1 al 5 a cada puerta, y los jugadores deben tener una tarjeta con un nivel de autorización igual o superior para desbloquearla. Esto crea una jerarquía natural en la que una tarjeta de nivel 3 abre todas las puertas de los niveles 1, 2 y 3, pero no permite acceder a las zonas restringidas de nivel 4 o 5. Las tarjetas pueden entregarse mediante sistemas de trabajo, encontrarse como botín o fabricarse mediante un sistema de progresión.
-- shared/config.lua
Config = {}
Config.KeycardLevels = {
{ name = 'keycard_1', label = 'Green Keycard', level = 1 },
{ name = 'keycard_2', label = 'Blue Keycard', level = 2 },
{ name = 'keycard_3', label = 'Yellow Keycard', level = 3 },
{ name = 'keycard_4', label = 'Red Keycard', level = 4 },
{ name = 'keycard_5', label = 'Black Keycard', level = 5 },
}
Config.DoorSecurity = {
[10] = { level = 1, name = 'Office Lobby' },
[11] = { level = 2, name = 'Server Room' },
[12] = { level = 3, name = 'Executive Floor' },
[13] = { level = 4, name = 'Vault Anteroom' },
[14] = { level = 5, name = 'Main Vault' },
}
-- client/keycard.lua
local function TryKeycardAccess(doorId)
local security = Config.DoorSecurity[doorId]
if not security then return false end
for _, card in ipairs(Config.KeycardLevels) do
if card.level >= security.level then
local count = exports.ox_inventory:Search('count', card.name)
if count > 0 then
-- Play card swipe animation
lib.requestAnimDict('anim@heists@keycard@')
TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
Wait(1200)
ClearPedTasks(PlayerPedId())
return true, card
end
end
end
return false
end
Grupos de puertas y acceso a negocios
Gestionar las puertas de forma individual deja de ser viable cuando tu servidor tiene cientos de puertas cerradas. Los grupos de puertas resuelven este problema: te permiten asignar varias puertas a un grupo con nombre y controlar el acceso a todas a la vez. Una comisaría podría tener 15 puertas cerradas a las que debería poder acceder el job de policía. En lugar de configurar cada puerta individualmente, las asignas todas al grupo "police_station" y concedes acceso según el nombre del job. Cuando un administrador contrata a un nuevo agente, este obtiene automáticamente acceso a todas las puertas del grupo, sin necesidad de configuración manual.
-- server/door_groups.lua
local DoorGroups = {
police_station = {
doors = {1, 2, 3, 4, 5, 6, 7, 8},
access = {
{ type = 'job', name = 'police', minGrade = 0 },
{ type = 'job', name = 'sheriff', minGrade = 0 },
}
},
pillbox_hospital = {
doors = {20, 21, 22, 23, 24},
access = {
{ type = 'job', name = 'ambulance', minGrade = 0 },
{ type = 'job', name = 'doctor', minGrade = 2 },
}
},
vangelico = {
doors = {30, 31},
access = {
{ type = 'job', name = 'jeweler', minGrade = 0 },
{ type = 'item', name = 'vangelico_key' },
}
},
}
function HasGroupAccess(source, groupName)
local group = DoorGroups[groupName]
if not group then return false end
for _, rule in ipairs(group.access) do
if rule.type == 'job' then
local job = GetPlayerJob(source)
if job and job.name == rule.name and job.grade >= (rule.minGrade or 0) then
return true
end
elseif rule.type == 'item' then
local count = exports.ox_inventory:GetItem(source, rule.name, nil, true)
if count and count > 0 then return true end
end
end
return false
end
Las puertas de negocios añaden otra dimensión al vincular el acceso a las puertas con los registros de propiedad. Cuando un jugador compra un negocio, debe obtener automáticamente el control de todas las puertas asociadas con esa propiedad. El sistema debe comprobar la titularidad del negocio en tiempo real, ya que las propiedades se pueden vender o transferir. Implementa un callback que consulte la tabla de negocios para verificar quién es el propietario actual y luego almacena el resultado en caché con un TTL breve para evitar sobrecargar la base de datos con cada interacción con una puerta.
Sincronizar los estados de las puertas entre clientes
La sincronización de puertas es uno de los aspectos más complicados de un sistema de cerraduras. Cuando un jugador abre una puerta, todos los jugadores cercanos deben verla abierta. Los controles nativos de puertas de FiveM funcionan por cliente, lo que significa que cada cliente gestiona los estados de las puertas de forma independiente. Sin una sincronización adecuada, un jugador verá la puerta abierta mientras que otro la verá cerrada. ox_doorlock gestiona la sincronización básica de estados, pero las extensiones personalizadas deben tener cuidado con la propagación de los estados. Usa state bags o eventos autoritativos del servidor para transmitir los cambios de estado de las puertas a todos los jugadores que estén dentro de un rango razonable. Evita sincronizar con todo el servidor cada vez que se cambia el estado de una puerta, ya que esto genera una carga de red innecesaria en servidores grandes con cientos de puertas.
-- server: broadcast door state to nearby players only
local function SyncDoorToNearby(doorId, state, coords, range)
range = range or 100.0
local players = GetPlayers()
for _, playerId in ipairs(players) do
local ped = GetPlayerPed(playerId)
if ped and DoesEntityExist(ped) then
local playerCoords = GetEntityCoords(ped)
if #(playerCoords - coords) <= range then
TriggerClientEvent('doorlock:sync', tonumber(playerId), doorId, state)
end
end
end
end
-- client: apply synced door state
RegisterNetEvent('doorlock:sync', function(doorId, state)
local door = exports.ox_doorlock:getDoor(doorId)
if door then
door.state = state
end
end)
Consideraciones de seguridad y medidas contra exploits
Los sistemas de cerraduras son un objetivo habitual de los jugadores que usan exploits, porque saltarse una puerta suele dar acceso a objetos, armas o dinero restringidos. Todas las solicitudes para cambiar el estado de una puerta deben validarse en el servidor. Nunca confíes en los ID de puertas enviados por el cliente sin verificar que el jugador esté cerca y tenga los permisos necesarios. Implementa un registro de cada cambio de estado de las puertas para que los admins puedan revisar patrones sospechosos, como que un jugador abra puertas de bóvedas a las que no debería tener acceso. Limita la frecuencia de todas las interacciones con puertas para evitar intentos de fuerza bruta contra el sistema de ganzuado. Considera añadir un registrador de eventos en el servidor que guarde el Steam ID del jugador, el ID de la puerta, la marca de tiempo y el método de acceso para mantener un registro de auditoría. Estos registros son muy valiosos al investigar exploits o resolver disputas entre jugadores sobre quién accedió a qué y cuándo.
Cómo integrar todos los componentes
Un sistema de cerraduras listo para producción combina todos estos componentes en un framework unificado. El punto de entrada es una interacción con un sistema de targeting, ya sea ox_target o qb-target, que detecta cuando un jugador mira una puerta registrada y muestra opciones contextuales según su nivel de acceso. Si tiene acceso por trabajo, muestra un botón para abrirla. Si tiene una tarjeta de acceso, muestra la opción de pasarla. Si tiene una ganzúa y la puerta está marcada como forzable, muestra la opción de usarla. Cada ruta lleva al handler correspondiente, que valida la acción en el servidor y sincroniza el resultado con los clientes cercanos. La configuración debe basarse en datos mediante entradas de la base de datos o archivos de configuración compartidos, para que los propietarios de servidores puedan añadir, quitar y modificar el acceso a las puertas sin tocar el código. Esta arquitectura modular permite cambiar el minijuego de ganzuado, añadir nuevos métodos de acceso como escáneres biométricos o integrarse con sistemas de inventario completamente distintos sin reescribir la lógica central.