OPX//77
opx_infinitydoorlock

doorlock

Last updated on

Locked world doors opened by job, gang, grade, key item, character or code — a port of ox_doorlock, with a staff panel.

A port of ox_doorlock to Open77's world doors: a door has ox's fields under ox's names, is filed under ox's integer id, opens by ox's rules and is edited in a panel built like ox's. The native door keeps its own animation and sound; this module only moves its lock. The server decides every turn: it reads the player's position, bucket, job and bag itself.

Sideboth
Requiresnone
Optionalcharacter (groups, characters), inventory (keys, lockpicks), target (eye rows), prompts (key row), form (code), progress (lockpick), downed
Contractdoorlock v1 — server, client
ExportsDoors — GetDoor, SetDoorState, CreateDoor…
Configconfig/doorlock.lua (shared)
Dataopx77_doorlocks (one row per door, the door as JSON)
  • Without character only staff, key items and codes open a door with groups or characters (a group check closes on every doubt).
  • Without inventory no key or lockpick works.

At a door

WhatHow
Turn a doorStand within reach and press E (KEY) — the prompt strip reads Lock {door} / Unlock {door} — or use the eye row Lock door / Unlock door. Away from a managed door E does nothing and says nothing. E is shared with garages, dealers, fitting rooms, lifts and teleports: when a door and another spot are both in reach, the nearer one acts and only its prompt row is drawn (key contest).
A door with a codeA form asks for the code, then the request is sent again with it. A wrong guess costs 3 s.
A keyA door_key item whose metadata type matches the door's item row. Staff cut one with /opx.doorlock.key.
Pick a lockEye row Pick lock on a pickable door, with a LOCKPICK.ITEMS item in the bag. One progress bar per step; the server times them and rolls each step. A pick can break; a pick that opens the lock and breaks says both. While a pick runs, E belongs to it (rank PICK): it neither turns the door nor opens a nearer garage. Lockpicking uses the same REQUEST_MS floor as turning a door.
AutolockA door with autolock locks itself that many seconds after it was unlocked.
SoundsThe door's lock / unlock sound plays for everyone within 20 m.

hideUi hides the prompt row, not the lock. A refusal is a toast naming why (see codes).

Who may turn a door

ox's isAuthorised, in this order:

  1. Staff holding the plain right opx.doorlock.bypass, only when STAFF_BYPASS is on — or the ACL entry doorlock.<id>. No code asked.
  2. A listed character (citizen id). No code asked.
  3. The groups: any one job or gang at its minimum grade. Then the code, if the door has one.
  4. Else the items: any one held. Then the code.
  5. A door with only a code opens for whoever knows it.

A door with none of these opens for staff only. opx.doorlock.bypass is not a command. grant, so an admin role's command.* does not carry it: by default an admin is asked for a key or a code like anyone.

A door, as the panel and CreateDoor write it
{
  name = 'NCPD front desk',
  native = '0xFC85EAE29622BAC2',        -- or doors = { { native, coords }, { native, coords } }
  coords = { x = -1234.5, y = 456.7, z = 12.0 }, bucket = 0,
  state = 1,                            -- 1 locked, 0 unlocked
  groups = { ncpd = 0 },                -- job or gang -> minimum grade
  items = { { name = 'door_key', metadata = 'ncpd_front', remove = false } },
  characters = { 'ABC12345' },
  passcode = '1234',
  autolock = 10, maxDistance = 2.0,
  lockpick = true, lockpickDifficulty = { 'easy', 'hard' },
  auto = false, hideUi = false, holdOpen = false, onDuty = false,
  lockSound = '', unlockSound = '',
}
  • native: the door's opaque id, 0x + 16 hex digits (the panel's Pick in world reads it). Two leaves make a double door; its coords default to the middle.
  • items[].metadata: matches the item's metadata type. remove = true spends one on use.
  • maxDistance: metres, default USE_RADIUS, clamped to MAX_REACH.
  • lockpickDifficulty: steps, each 'easy' | 'medium' | 'hard' or { areaSize, speedMultiplier }. None: LOCKPICK.DEFAULT.
  • auto: an automatic door (garage, shutter). holdOpen: stays open while unlocked. onDuty: a job group only counts on duty.
  • Limits: name 64 characters, 16 groups, 8 items, 32 characters, code 32 characters, autolock 0..3600 s, 512 doors.

Commands

All restricted: the host checks command.<name>.

CommandAccessArgumentsEffect
/opx.doorlockACL command.opx.doorlock[closest]Opens the staff panel; closest opens the closest door's settings. From the console: prints the list.
/opx.doorlock.listACL command.opx.doorlock.list[door]Every door (id, name, leaves, bucket, position, state, origin, backend refusals), or one door as JSON without its code.
/opx.doorlock.lockACL command.opx.doorlock.lock<door> [on|off]Locks or unlocks from anywhere; flips when omitted.
/opx.doorlock.keyACL command.opx.doorlock.key<door> [player]Cuts a key: the door's first item row with metadata, given with { type = <metadata>, label = <door name> }. Default: yourself.
/opx.doorlock.removeACL command.opx.doorlock.remove<door>Deletes a door.

<door> is the id /opx.doorlock.list prints, or the key a seeded or carried-over door had. There is no save command: command.opx.doorlock.save gates the panel's Confirm door.

ACL entryOpens
command.opx.doorlockthe panel, the eye's Manage door, the F9 World → Door locks row
command.opx.doorlock.savecreate and edit; also sends the door's code to the panel
command.opx.doorlock.remove, .lock, .key, .listdelete, lock from anywhere, cut a key, list — in the panel and as commands
opx.doorlock.bypassturning any door without key or code, only with STAFF_BYPASS = true
doorlock.<id>turning that one door without key or code
command.opx.admin.player.tpthe panel's Teleport to door

Audit events: doorlock.create, .edit, .remove, .lock, .unlock, .key; security lines doorlock.staffDenied, doorlock.passcode, doorlock.pickEarly.

Staff panel

Opened by /opx.doorlock, the F9 World → Door locks row or the eye's Manage door (the door's settings when it is managed, a new door with that leaf taken when it is not).

  1. The table. Id, name, state, distance. Search, sortable columns, pages. Per row: Settings, Copy settings, Lock now / Unlock now, Teleport to door, Delete door (with a confirm). + starts a new door. Seeded doors are tagged config.
  2. Settings, General. Name, passcode, autolock, interact distance, the switches Locked, Double, Automatic, Lockpick, Hide UI, Hold open, On duty only, and the door itself with Pick in world.
  3. Characters, Groups, Items. Rows of citizen ids; group + grade; item + metadata type + Remove on use.
  4. Lockpick. One difficulty per row, or Custom with area size and speed multiplier. No row: LOCKPICK.DEFAULT.
  5. Sound. Lock and unlock sounds from SOUNDS.LIST.
  6. Pick in world. The panel hides; aim at the door and press E (twice for a double door), Escape cancels. A leaf another door manages and a lift door are refused. Confirm door on a door with no leaf starts this step and saves when it ends.
  7. Confirm door saves. Give me a key, copy / apply copied settings and delete sit on the same bar.
  • The code. Staff holding command.opx.doorlock.save see it in plain text in the settings; anyone else sees only that one is set. Left empty on save, it is kept; Clear the code removes it.
  • A button the ACL refuses is greyed, not hidden.
  • The server re-checks every save: an unknown group or grade, an unknown item, a leaf another door holds, or a new or moved door more than 60 m from the staff member is refused.

Backends

BACKENDWho holds the lockStrength
networkedThe platform's open77_doors: each leaf is registered to this resource and its lock is the platform's server state. A sweep every 30 s re-adopts doors the service forgot after a restart.Strong: a locked door refuses every player.
localThe server broadcasts the state per bucket; each client locks its streamed doors with Open77.doors.setLocked and refuses vanilla interaction on a locked one.Weak: a modified client can open its own copy of a door.
auto (default)networked when open77_doors is running, else local.

open77_doors depends on open77_elevators, which takes the cabins away from the elevators module; servers that need job-gated lifts run local. BACKEND = 'networked' without the service falls back to local and logs an error. /opx.doorlock.list and the panel name the running backend.

Migration from the first table

The first version of this module (PR #70) stored doors in opx77_doorlock (no s). On every boot each of its rows is inserted once into opx77_doorlocks under legacy:<key>; nothing is lost and nothing is written back. Drop the old table by hand once the doors are checked.

  • Groups { kind, name, grade } become ox's map { [name] = grade }; locked becomes state.
  • A BOUND key item becomes a metadata match on the old key. Keys already cut (door = <old key>) keep opening their door.
  • A row whose key a config door also used is skipped, as before.
  • The old key still works in every command and export.

DOORS blocks changed shape too:

First versionNow
X, Y, ZCOORDS = { x, y, z }
LOCKED = trueSTATE = 1
GROUPS = { { JOB = 'ncpd', GRADE = 0 } }GROUPS = { ncpd = 0 }
ITEMS = { { NAME = 'door_key', BOUND = true } }ITEMS = { { NAME = 'door_key', METADATA = '<tag>' } }
DIFFICULTY = 'hard'LOCKPICK_DIFFICULTY = { 'hard' }
LOCKPICK.ITEMLOCKPICK.ITEMS (a list)
LOCKPICK.DEFAULT_DIFFICULTYLOCKPICK.DEFAULT (a sequence)
LOCKPICK.BREAK_CHANCE = 0.35LOCKPICK.BREAK_CHANCE = { SUCCESS, FAIL }
SOUNDS = { LOCK, UNLOCK }SOUNDS = { LIST, DEFAULT = { LOCK, UNLOCK } }
STAFF_BYPASS = true, grant command.opx.doorlock.bypassSTAFF_BYPASS = false, plain right opx.doorlock.bypass

Configuration

config/doorlock.lua sets OPX.Config.MODULES.doorlock. Shared script.

KeyDefaultEffect
enabledtruefalse switches the module off.
BACKEND'auto''auto', 'networked' or 'local'. See Backends.
USE_RADIUS2.0Reach in metres for a door with no maxDistance.
MAX_REACH8.0Ceiling on a door's own maxDistance (1..12).
SLACK1.0Metres added on the server for latency.
SCAN_MS500How often the client looks for the nearest door and, on local, re-applies locks.
SCAN_RADIUS40.0Metres around the player streamed doors are read on local.
REQUEST_MS600Floor between two door requests from one player, turns and lockpick attempts alike.
MEMBERSHIP'any''any' counts every job and gang membership; 'primary' only the worked job and primary gang.
STAFF_BYPASSfalsetrue lets holders of opx.doorlock.bypass turn any door without key or code.
NOTIFYtrueToast after a success. Refusals are always shown.
KEY{ ID = 'opx.doorlock.use', NAME = 'doorlock.key.use', DEFAULT = 'E' }The use key, rebindable. DEFAULT = false declares none (the eye row stays). Also confirms Pick in world.
KEY_ITEM'door_key'The key item name.
LOCKPICKsee belowLockpick items, steps and break odds.
SOUNDS{ LIST = {}, DEFAULT = { LOCK = '', UNLOCK = '' } }Wwise events the panel offers, and what a door with none plays ('' = nothing).
DOORS{}Doors inserted once under config:<key>, then owned by the panel. Deleting one leaves a tombstone so it is not seeded again.
config/doorlock.lua — LOCKPICK
LOCKPICK = {
  ITEMS = { 'lockpick' },            -- one is needed; a break takes one
  CAN_PICK_UNLOCKED = false,         -- may a pick also lock?
  DEFAULT = { 'easy', 'easy', 'medium' },
  DIFFICULTY = {                     -- one progress bar per step, rolled by the server
    easy   = { DURATION_MS = 2000, CHANCE = 0.85 },
    medium = { DURATION_MS = 2500, CHANCE = 0.6 },
    hard   = { DURATION_MS = 3000, CHANCE = 0.4 },
  },
  BREAK_CHANCE = { SUCCESS = 0.01, FAIL = 0.2 },
},
  • Every step must pass. A custom step { areaSize, speedMultiplier } reads as CHANCE = areaSize / 60 / speedMultiplier (0.05..0.95) and DURATION_MS = 2500 / speedMultiplier.
config/doorlock.lua — one DOORS block
DOORS = {
  ncpd_front = {
    NAME = 'NCPD front desk',
    DOORS = { '0xFC85EAE29622BAC2' },
    COORDS = { x = -1234.5, y = 456.7, z = 12.0 }, BUCKET = 0,
    STATE = 1,
    GROUPS = { ncpd = 0 },
    ITEMS = { { NAME = 'door_key', METADATA = 'ncpd_front' } },
    PASSCODE = '1234', AUTOLOCK = 10, MAX_DISTANCE = 2.0,
    LOCKPICK = true, LOCKPICK_DIFFICULTY = { 'easy', 'hard' },
  },
},

The fields are the door's, upper-cased (CHARACTERS, AUTO, HIDE_UI, HOLD_OPEN, ON_DUTY, LOCK_SOUND, UNLOCK_SOUND too). A block that fails validation is logged as [doorlock] config: DOORS.<key> refused: <code>.

Refusal codes

Shown as toasts, locale key doorlock.error.<code>.

CodeMeaning
too_fastUnder REQUEST_MS since the last request, or a code guess within 3 s.
unknown_doorNo such door.
no_position, wrong_bucket, too_farThe server could not read you, or you are not at the door.
player_downYou are down.
not_allowedNot listed, or vetoed by the doorlock:authorise hook.
no_keyNo matching item (or it could not be spent).
job_required, grade_too_low, off_duty, job_stale, no_characterThe group check failed.
passcode_required, wrong_passcodeThe door needs its code.
not_pickable, no_lockpick, pick_failed, pick_broke, already_unlockedLockpick.
not_permittedStaff: the ACL refuses that action.
not_loaded, save_failed, too_many_doorsStaff save: doors still loading, database error, 512 doors.
busyStaff write: another create, edit, remove or panel save did not finish within 5 s (Another door change is being saved. Try again in a moment.). Writes run one at a time and re-read the doors when their turn comes; a turn held 30 s is let go.
unknown_group, unknown_item, door_takenStaff save: a group, grade or item nobody defined, or a leaf another door holds.
bad_door, bad_doors, bad_door_id, duplicate_door_id, bad_position, bad_bucket, bad_groups, bad_group, bad_items, bad_item, bad_characters, bad_character, bad_passcode (also a numeric code of 2^63 or more), bad_autolock, bad_difficulty, bad_distance, bad_sound, bad_idStaff save: a malformed field.
bad_fieldEdit: a field of a different type from the stored one (detail names it). Replaces the per-field bad_<field> codes.
no_key_item, no_inventory, key_refused, no_player/opx.doorlock.key.
lift_doorPick in world on a lift's door.

Successes: locked, unlocked, picked, picked_broke (opened, and the lockpick broke), already_locked, already_unlocked (doorlock.answer.<code>).

The contract and the door exports answer a write refusal (create, edit, remove) as doorlock.error.<code>, the same keys as these toasts. Their own codes doorlock.unknownDoor, doorlock.badState, doorlock.noDoor and doorlock.notStaff now have sentences too.

On this page