Skip to the content
Docs

Hooks

On this page8

bridge/hooks.lua is an open file. It ships with a safe default body for every hook; you edit the body to change what the script does.

It is loaded on both sides — the server hooks and the client hooks live in the same file, each in its own block.

The rules

  1. Keep the signature and the documented return values. The core never checks types.
  2. Never yield. No Wait, no MySQL.await, no awaited callback. Hooks run while a scan is being worked out and while the resource starts. Do slow work in a CreateThread and return now.
  3. Call only RecogCore.*. Those are the only core functions a bridge file may call — see Bridges.
  4. Returning nil where "nil = use the built-in behaviour" is documented means "I did not decide". The core then does what it would have done anyway.

Server: permissions

Each returns allowed plus an optional locale key for the refusal message.

HookSignatureDefault
CanStartScan(src, targetSrc)true
CanUseDevice(src, kind)true
CanStartImplant(src)true
CanIntroduce(src, targetSrc, name)true
CanReadAsPolice(src, reportId)nil — use the normal police check
ShouldProcessDeath(src, data)true

kind is 'voice_changer', 'hidden_camera' or 'magnetic_scanner'.

lua
-- no scanning inside the arena
function Hooks.CanStartScan(src, targetSrc)
    if exports['my_arena']:IsInArena(src) then
        return false, 'scan_blocked'
    end
    return true
end

-- paintball deaths are not a New Life Rule event
function Hooks.ShouldProcessDeath(src, data)
    return not exports['my_paintball']:IsPlaying(src)
end

ShouldProcessDeath returning false is the right way to keep arenas, paintball and minigames out of the New Life Rule entirely.

Server: money

HookSignatureReturn
ChargeImplant(src, price)true charged · false refused · nil use the framework
RefundImplant(src, price, account)true · false · nil use the framework
StandaloneCharge(src, amount, reason)true charged · false refused
lua
-- pay for the implant in dirty money
function Hooks.ChargeImplant(src, price)
    return exports['my_blackmoney']:Remove(src, price)
end

StandaloneCharge exists for servers with no framework at all. By default it returns true when Config.Implant.StandaloneFree is on, and refuses otherwise.

Server: identity

HookSignatureReturn
GetCustomCharacterId(src)string | nil
GetCustomJob(src){name, label?, grade?, onduty?, type?} | nil

GetCustomCharacterId is what makes Config.Framework = 'custom' work. The key must be stable for the life of the character — everything the script stores hangs off it.

GetCustomJob is how a standalone or custom-framework server gets police at all, so that report reading, the hint line and the MDT push know who is an officer.

Server: observers

These never block anything. Return values are ignored.

HookSignature
OnScanFinished(src, targetSrc, info)
OnRecognized(src, targetSrc, info)
OnIdentityReset(charKey, src, info)
OnProof(snapshot)
OnDeathSessionClosed(session)
OnLabIdentified(officerSrc, targetKey, info)

They carry the same data as the matching public events. Use the events from another resource; use these hooks when you would rather keep the logic in the bridge file.

Server: formatting

HookSignatureReturn
FormatAutoLabel(parts)string (cut to 64 characters) · nil for the built-in format. Only the clothing words admins see: players read the memory line (where and when you met), which this hook does not change
FilterDiscordExport(payload)the payload, edited or not · nil/false to cancel
lua
-- admin panel / Discord only: "red jacket, male" instead of the built-in clothing words
function Hooks.FormatAutoLabel(parts)
    if not parts.clothing then return nil end
    return ('%s, %s'):format(parts.clothing, parts.gender or 'unknown')
end

FilterDiscordExport is the last look at a Discord payload before it is sent. Returning nil or false cancels the export with the code export_blocked.

Client hooks

HookSignatureReturn
CanShowPrompt(targetServerId)boolean — false = no arrow over this person
IsPlayerBusy()boolean
OnHudVisibility(visible)ignored
OnScanStateChanged(scanning)ignored

CanShowPrompt is called while the script looks for people in front of you, in every start mode. Keep it cheap — it runs several times a second while somebody is in front of you.

IsPlayerBusy returning true hides the arrows and the prompt and blocks a scan start. It is the right place to wire up your phone, your menus, being cuffed, or a cutscene:

lua
function Hooks.IsPlayerBusy()
    return LocalPlayer.state.inv_busy
        or exports['my_phone']:IsOpen()
        or false
end

OnHudVisibility is a convenience for moving your own UI out of the way; the public client event onHudVisibilityChanged does the same from another resource.

RecogCore

Bridges may call only RecogCore.*, and only from inside a handler or a thread. The full list is on Bridges.

See also