Volver al blog
Tutorial11 min de lectura

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.

El script adecuado

Listo para tu servidor, con actualizaciones incluidas.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.