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
- Keep the signature and the documented return values. The core never checks types.
- Never yield. No
Wait, noMySQL.await, no awaited callback. Hooks run while a scan is being worked out and while the resource starts. Do slow work in aCreateThreadand return now. - Call only
RecogCore.*. Those are the only core functions a bridge file may call — see Bridges. - Returning
nilwhere "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.
| Hook | Signature | Default |
|---|---|---|
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'.
-- 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)
endShouldProcessDeath returning false is the right way to keep arenas,
paintball and minigames out of the New Life Rule entirely.
Server: money
| Hook | Signature | Return |
|---|---|---|
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 |
-- pay for the implant in dirty money
function Hooks.ChargeImplant(src, price)
return exports['my_blackmoney']:Remove(src, price)
endStandaloneCharge exists for servers with no framework at all. By default it
returns true when Config.Implant.StandaloneFree is on, and refuses
otherwise.
Server: identity
| Hook | Signature | Return |
|---|---|---|
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.
| Hook | Signature |
|---|---|
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
| Hook | Signature | Return |
|---|---|---|
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 |
-- 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')
endFilterDiscordExport 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
| Hook | Signature | Return |
|---|---|---|
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:
function Hooks.IsPlayerBusy()
return LocalPlayer.state.inv_busy
or exports['my_phone']:IsOpen()
or false
endOnHudVisibility 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
- Bridges — the open files and how to add a system
- Events
- Writing a Custom Adapter