Complete reference for VZU Circuit Hack public API.
- Client exports
- Server exports
- Server events
- Result reasons
- Cross-resource note (function references)
- Type definitions
Backwards-compatible simple API. Triggers a hack with a single difficulty key and a result-only callback.
exports['vzu_circuit_hack']:Start(difficulty, callback)| Param | Type | Required | Notes |
|---|---|---|---|
difficulty |
string |
yes | 'easy' | 'medium' | 'hard' (or any custom key in Config.Difficulties) |
callback |
function(success, reason) |
no | Fires on close. success is a boolean; reason is the result reason for non-success closes. |
Internally calls StartAdvanced with context.type = 'default'.
exports['vzu_circuit_hack']:Start('medium', function(success, reason)
if success then
TriggerServerEvent('myresource:rewardPlayer')
elseif reason == 'cooldown' then
-- caller's UI is responsible for the cooldown notice
else
print('Hack failed:', reason) -- 'timeout' / 'escape' / 'too_fast' / ...
end
end)Full-featured API with rich callbacks, context tracking, and per-call custom configs. Recommended for all new integrations.
exports['vzu_circuit_hack']:StartAdvanced(options) -- returns booleanoptions table:
| Field | Type | Default | Description |
|---|---|---|---|
difficulty |
string |
Config.DefaultDifficulty |
'easy', 'medium', 'hard', 'custom', or a custom-difficulty key |
customConfig |
table |
— | Required when difficulty == 'custom'. Same shape as a Config.Difficulties entry. |
context |
table |
{} |
Free-form metadata. Only context.type is consumed (drives Config.Cooldown.ByType). |
callbacks |
table |
{} |
Per-call callbacks. See below. |
context shape (all optional):
| Field | Type | Description |
|---|---|---|
type |
string |
Cooldown bucket: 'atm', 'vehicle', 'door', 'computer', 'safe', or any key you add to Config.Cooldown.ByType |
targetId |
string |
Free-form identifier for the hacked entity (passed through to callbacks) |
description |
string |
Human-readable context |
metadata |
table |
Anything else; passed through verbatim |
callbacks shape:
| Callback | Signature | Fires when |
|---|---|---|
onStart |
function(data) |
Hack opens (server hook + local) |
onSuccess |
function(data) |
Server validated success |
onFail |
function(data) |
Server rejected the submit / timeout / too-fast / etc. |
onCancel |
function(data) |
Player pressed ESC and EscapeCountsAsFail = false |
onCooldown |
function({remainingMs}) |
The cooldown gate refused the open |
data shape across callbacks:
{
sessionId = 'AbCdEf1234567890', -- 16-char alnum nonce
difficulty = 'medium',
context = { type = 'atm', ... },
timeMs = 9241, -- onSuccess / onFail only
clickCount = 14, -- onSuccess / onFail only
reason = 'timeout', -- onFail only
}Returns true if the hack was opened, false if it bailed
synchronously (disabled, already active, invalid difficulty, missing
custom config).
The bundled examples/02_vehicle_lockpick.lua is the canonical
reference integration. It demonstrates every important pattern:
ox_targetprompt registration viaaddBoxZone(NOTaddLocalEntityon a vehicle entity, which conflicts with bone-targeted options from vehicle-keys resources)- Network-ownership reclaim with retry loop before applying state
- Multi-channel unlock (
SetVehicleDoorsLocked+LockedForAllPlayers+LockedForPlayer+NeedsToBeHotwired+Alarm) - Cross-system keys delivery with fallback chain (
qbx_vehiclekeys→qb-vehiclekeys) pcallaround external resource exports so a missing resource doesn't break the flow
If your scenario looks like "player approaches X, sees a prompt, plays Circuit Hack, gets a state change on X" — copy this example.
exports['vzu_circuit_hack']:StartAdvanced({
difficulty = 'hard',
context = {
type = 'safe',
targetId = 'safe_pacific_vault',
description = 'Pacific Standard vault',
metadata = { value = 250000, tier = 'high' },
},
callbacks = {
onStart = function(data)
-- Trigger heist alarm 15s after hack begins.
SetTimeout(15000, function()
TriggerServerEvent('police:alarm', 'pacific_bank')
end)
end,
onSuccess = function(data)
print(('Vault cracked in %dms with %d clicks'):format(data.timeMs, data.clickCount))
TriggerServerEvent('myheist:reward', data.context.metadata.value)
end,
onFail = function(data)
print('Hack failed:', data.reason)
TriggerServerEvent('police:silentAlert', GetEntityCoords(PlayerPedId()))
end,
onCancel = function(data)
print('Player cancelled')
end,
onCooldown = function(cd)
local minutes = math.ceil(cd.remainingMs / 60000)
lib.notify({
description = ('Vault on cooldown: %d min'):format(minutes),
type = 'error',
})
end,
},
})local active = exports['vzu_circuit_hack']:IsActive()
-- returns: booleantrue while a hack is open for the local player. Use it to gate other
UIs and prevent double-opening.
local session = exports['vzu_circuit_hack']:GetCurrentSession()
-- returns: table | nilShape (when active):
{
sessionId = 'AbCdEf1234567890',
difficulty = 'medium',
config = { gridSize = 6, timeLimitMs = 30000, ... },
context = { type = 'atm', ... },
startedAt = 1730000000000, -- GetGameTimer() at start
callbacks = { ... },
}local stats = exports.vzu_circuit_hack:GetPlayerStats(source)Returns nil if the player has no record (yet). Otherwise:
{
totalAttempts = 47,
successCount = 32,
failCount = 12,
cancelCount = 3,
totalTimeMs = 854000, -- sum across successful hacks only
lastAttempt = {
result = 'success', -- 'success' | 'fail' | 'cancel'
timeMs = 18000,
timestamp = 1730000000, -- unix seconds
},
}local session = exports.vzu_circuit_hack:GetActiveSession(source)
-- nil if the player is not currently in a hackexports.vzu_circuit_hack:ClearPlayerStats(source)
-- wipes the in-memory record (and the DB row if persistence is on)local top = exports.vzu_circuit_hack:GetTopPlayers(limit, sortBy)| Param | Type | Default | Description |
|---|---|---|---|
limit |
number |
10 |
Max rows |
sortBy |
string |
'successCount' |
'successCount', 'totalAttempts', or 'avgTime' |
Returns an array of:
{
source = 5, -- nil if the player is offline
name = 'PlayerName',
stats = { totalAttempts, successCount, ... },
avgTime = 18500, -- ms across successful hacks
}-- ms remaining for (source × contextType). 0 = no cooldown.
local remaining = exports.vzu_circuit_hack:GetCooldownRemaining(source, 'atm')
-- shortcut for `remaining == 0`
local canStart = exports.vzu_circuit_hack:CanStartHack(source, 'atm')
-- manually stamp a cooldown (e.g. after a non-hack action)
exports.vzu_circuit_hack:SetCooldown(source, 'atm', { result = 'success' })
-- wipe ALL cooldowns for a source
exports.vzu_circuit_hack:ClearCooldowns(source)
-- wipe one (source, contextType) pair
exports.vzu_circuit_hack:ClearCooldownType(source, 'atm')The third arg to SetCooldown is the lastAttempt table. When provided,
it drives Config.Cooldown.ReductionOnSuccess /
Config.Cooldown.PenaltyOnFail. Pass nil to skip those modifiers.
AddEventHandler('vzu-circuit-hack:onStart', function(source, data) ... end)
AddEventHandler('vzu-circuit-hack:onSuccess', function(source, data) ... end)
AddEventHandler('vzu-circuit-hack:onFail', function(source, data) ... end)
AddEventHandler('vzu-circuit-hack:onCancel', function(source, data) ... end)See EVENTS.md for full payloads, ordering guarantees, and common integration patterns.
Possible values for data.reason on onFail / Start callback's second
arg:
| Reason | Meaning |
|---|---|
disabled |
Config.Enabled = false |
already_active |
Another hack is open for this player |
invalid_difficulty |
Bad difficulty arg, no matching key in Config.Difficulties |
missing_custom_config |
difficulty = 'custom' but no customConfig table |
cooldown |
The cooldown gate refused — only seen via Start callback's reason; StartAdvanced calls onCooldown directly |
timeout |
The puzzle timer expired |
escape |
Player pressed ESC (and Config.EscapeCountsAsFail = true) |
too_fast |
Submit faster than Config.AntiCheat.minTimeMs |
rate_limited |
Hit Config.AntiCheat.maxAttemptsPerMinute |
bad_payload |
Malformed submit (likely tampering) |
session_mismatch |
Submit for a session id that doesn't belong to this player |
When you call exports['vzu_circuit_hack']:StartAdvanced({callbacks = {...}})
from another resource, FiveM serialises the table across the
resource boundary. Lua functions inside the table become CFX function
references — their type() is 'table' (with a __call
metamethod), NOT 'function'.
Earlier versions of callIfFn rejected anything not strictly typed as
'function'. The current version (v1.0+) tries pcall(fn, ...) directly
so refs and plain functions both work.
If you're forking or vendoring older code, the safe shape is:
local function callIfFn(fn, ...)
if fn == nil then return end
local ok, err = pcall(fn, ...)
if not ok then print('callback error:', err) end
endFor deep customization, the NUI accepts these messages from Lua. You
shouldn't normally call these directly — StartAdvanced builds the
payload — but they're documented for completeness.
SendNUIMessage({
action = 'open',
data = {
sessionId = '...',
difficulty = 'medium',
difficultyData = { gridSize = 6, timeLimitMs = 30000, ... },
difficulties = Config.Difficulties,
allowDifficultyPick = false,
context = { ... },
config = { lockInDuration, allowEscape, allowReverse, ... },
locale = { ... }, -- full strings table
localeKey = 'en',
sounds = { enabled, masterVolume, volumes, allowMute },
},
})
SendNUIMessage({ action = 'close' })type Difficulty = 'easy' | 'medium' | 'hard' | 'custom' | string;
type FailReason =
| 'disabled' | 'already_active' | 'invalid_difficulty' | 'missing_custom_config'
| 'cooldown' | 'timeout' | 'escape' | 'too_fast' | 'rate_limited'
| 'bad_payload' | 'session_mismatch';
interface Context {
type?: string;
targetId?: string;
description?: string;
metadata?: Record<string, unknown>;
}
interface CustomConfig {
gridSize: number; // 4-10
timeLimitMs: number;
minClicks: number;
maxClicks: number;
label?: string;
}
interface SessionData {
sessionId: string;
difficulty: Difficulty;
context: Context;
startedAt: number; // GetGameTimer() at start
}
interface ResultData extends SessionData {
timeMs: number;
clickCount: number;
reason?: FailReason;
}
interface StartAdvancedOptions {
difficulty?: Difficulty;
customConfig?: CustomConfig;
context?: Context;
callbacks?: {
onStart?: (data: SessionData) => void;
onSuccess?: (data: ResultData) => void;
onFail?: (data: ResultData) => void;
onCancel?: (data: ResultData) => void;
onCooldown?: (data: { remainingMs: number }) => void;
};
}