Skip to content

Latest commit

 

History

History
399 lines (324 loc) · 12.5 KB

File metadata and controls

399 lines (324 loc) · 12.5 KB

API Reference

Complete reference for VZU Circuit Hack public API.

Table of contents

Client exports

Start (v1)

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)

StartAdvanced (v2)

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 boolean

options 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).

Canonical reference: vehicle lockpick

The bundled examples/02_vehicle_lockpick.lua is the canonical reference integration. It demonstrates every important pattern:

  • ox_target prompt registration via addBoxZone (NOT addLocalEntity on 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_vehiclekeysqb-vehiclekeys)
  • pcall around 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.

Full example (StartAdvanced)

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,
    },
})

IsActive

local active = exports['vzu_circuit_hack']:IsActive()
-- returns: boolean

true while a hack is open for the local player. Use it to gate other UIs and prevent double-opening.

GetCurrentSession

local session = exports['vzu_circuit_hack']:GetCurrentSession()
-- returns: table | nil

Shape (when active):

{
    sessionId  = 'AbCdEf1234567890',
    difficulty = 'medium',
    config     = { gridSize = 6, timeLimitMs = 30000, ... },
    context    = { type = 'atm', ... },
    startedAt  = 1730000000000,  -- GetGameTimer() at start
    callbacks  = { ... },
}

Server exports

Stats

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 hack
exports.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
}

Cooldown helpers

-- 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.

Server events

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.

Result reasons

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

Cross-resource note (function references)

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
end

NUI messages

For 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 definitions (TypeScript-style)

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;
  };
}