EndCore Framework

en-doorlock

Server-authoritative door locks with job, group and item access, passcodes, lockpicking, auto-relock and an in-game editor.

en-doorlock locks map doors, single or double, and the server decides who gets through. A locked door opens for players who meet its rules: a job or player group at or above a grade, or carrying any one of a set of items, such as a keycard.

A door can also have a passcode keypad with a cooldown after wrong guesses, a lockpick difficulty (or be unpickable), and an automatic relock timer. Near a door, players see a padlock icon and an E prompt to lock or unlock it.

Admins build doors in game with /doorlock. Aim at a door and press E (press again on the second half of a double door), then Enter, and fill in the settings dialog.

At a glance

Depends onoxmysql, en-core, en-ui
Start afterAll of the above, plus en-inventory for item rules and lockpicking
Database tablesencore_doors
Config filesconfig/shared.lua
Key bindsen_doorlock_use (E)
Target optionsen-doorlock:pick on door models (needs a lockpick; door locked and pickable)

How it works

Access rules

When you press E on a locked door, the server checks, in order:

  1. If Config.adminBypass is on and you have the command.doorlock ACE, the door opens.
  2. Each entry in the door's groups: a job name with a minimum grade, group:<id> for a specific player group at a minimum grade, or the bare key group for membership of any player group at that grade.
  3. Each item in items: carrying any one of them is enough.

If none match and the door has a passcode, you're asked for it. A wrong code locks you out for Config.passcodeCooldown seconds.

Lockpicking

Lockpicking uses encore.minigame.lockpick with your lockpick skill bonus from en-skills. A success always unlocks the door and never locks it. The server requires at least 1.5 s and that you are close to the door. A failure uses up the pick with probability loseOnFail. Picking a door awards thievery XP.

Door sources

Doors come from three places:

SourceIdEditable in gameSaved
Config.doorsconfig:<index>NoConfig file
/doorlock editorDatabase idYesencore_doors, including lock state
AddDoor exportYour idNoRuntime only

Door state is broadcast to clients by net events and applied with DoorSystemSetDoorState. Door system hashes are joaat('en-doorlock:<id>:<index>').

Configuration

KeyDefaultWhat it does
Config.keys.use'E'Use key
Config.adminPrincipal'group.admin'ACE principal allowed to use /doorlock
Config.adminBypassfalseIf true, players allowed command.doorlock open every door
Config.distance2.0Default reach (each door is clamped to 1–8)
Config.showLocktrueDraw a padlock icon on nearby doors
Config.lockpickitem = 'lockpick', difficulty = 'medium', loseOnFail = 0.35, xp = { thievery = 12 }Lockpicking settings
Config.passcodeCooldown5Seconds locked out after a wrong code
Config.doors{}Doors defined in config

Door fields

These fields apply to config doors, editor doors and the AddDoor export.

FieldWhat it does
nameDisplay name, up to 60 characters
doors{ { model = <hash>, coords = vec3 }, ... }, one or two parts. model must be a number, so use a backtick hash literal
lockedStarting state (default true)
distanceReach for this door
autolockSeconds after unlocking before it relocks; nil = never
groups{ jobName = minGrade, ['group:<id>'] = minGrade, group = minGrade }
items{ 'item', ... }; carrying any one opens the door
passcodeString, up to 16 characters
lockpick'easy', 'medium' or 'hard', or false for unpickable; nil uses Config.lockpick.difficulty

Adding a config door

lua
Config.doors = {
    {
        name = 'Checkpoint armoury',
        doors = { { model = `v_ilev_rc_door2`, coords = vec3(-252.1, -310.4, 21.6) } },
        locked = true,
        distance = 2.0,
        autolock = 30,
        groups = { ['group:12'] = 1 },
        items = { 'keycard_armoury' },
        passcode = '4471',
        lockpick = 'hard',
    },
}

Exports

Server

ExportArgumentsReturns
GetDooridPublic door { id, name, doors, locked, distance, passcode, lockpick } or nil. passcode and lockpick are booleans
GetDoorByNamenamePublic door or nil
SetDoorLockedid, lockedboolean (whether the door exists). Reports the change as 'script'
AddDoorid, dataDoor id or nil. Runtime only, not saved
RemoveDooridboolean

Public door data never includes the passcode or the access rules.

Tip

Prefix ids passed to AddDoor with your resource name, such as myheist:vault, so they can't clash with other resources.

Client

ExportReturns
GetClosestDoor()The nearby door within reach (on foot), or nil
UseClosestDoor()Tries to toggle the nearby door, prompting for a passcode if needed
PickClosestDoor()Starts lockpicking the nearby door

Commands

CommandRestrictedWhat it does
/doorlock [new|edit]Config.adminPrincipalnew (default) picks doors and creates a lock. edit changes or deletes the closest door
Note

Only editor-made doors can be edited or deleted in game. Change config and export doors where they're defined.

Events

EventSidePayload
en-doorlock:server:stateChangedServer-localsource?, doorId, locked, how

how is one of key, passcode, lockpick, auto or script. A player toggling a door they're authorised for, by group or item, reports key.

Database

encore_doors: id (INT auto-increment primary key), data (LONGTEXT JSON of the door fields). Lock state is written back on every change, so editor doors keep it across restarts.

Examples

Add a door from another resource and open it when a heist step completes:

lua
-- server
local doorId = exports['en-doorlock']:AddDoor('myheist:vault', {
    name = 'Bunker vault',
    doors = { { model = `v_ilev_bk_vaultdoor`, coords = vec3(255.2, 223.9, 102.4) } },
    locked = true,
    lockpick = false,
    items = { 'vault_keycard' },
})

AddEventHandler('myheist:server:hackComplete', function()
    exports['en-doorlock']:SetDoorLocked(doorId, false)
end)

Log every forced entry:

lua
AddEventHandler('en-doorlock:server:stateChanged', function(source, doorId, locked, how)
    if how == 'lockpick' then
        local door = exports['en-doorlock']:GetDoor(doorId)
        print(('%s picked %s'):format(GetPlayerName(source), door and door.name or doorId))
    end
end)