Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cc-scripts.com/llms.txt

Use this file to discover all available pages before exploring further.

The Control Panel isn’t a minigame — it’s a long-lived NUI surface for showing a heist schematic with clickable doors and cameras. It lives in cc_minigames because it shares the same NUI shell, design tokens, and SFX as the minigames. The caller drives it by passing callbacks and updating state with the ControlSet* exports.
The panel and a minigame can not be open at the same time on a given client — both compete for NUI focus. Use IsActive() / ControlIsOpen() to coordinate.

Open the panel

exports.cc_minigames:ControlOpen({
    schematic = 'paleto_bank',                 -- string (loads schematics/<name>.json) or inline table
    title     = 'Paleto Bank',                 -- header title
    subtitle  = 'Security Control',            -- header subtitle
    doors     = { front_door = 'locked' },     -- initial door states
    cameras   = { lobby = 'active' },          -- initial camera states

    onDoorClick = function(id, state)
        -- player clicked a door icon. Update server-side, then call ControlSetDoor.
    end,

    onCameraClick = function(id, state)
        -- cycle / hack / disable a camera as you see fit.
    end,

    onPasscodeVerify = function(id, code)
        -- return true to unlock a code-locked door, false to shake-and-clear the keypad.
        return code == '4815'
    end,

    onDoorRelocked = function(id)
        -- the panel demoted a code-unlocked door back to 'locked' after the timeout.
    end,

    onClose = function()
        -- cleanup
    end,
})
schematic accepts either an inline table or a string. When a string is passed, the resource looks up schematics/<name>.json inside cc_minigames (schematics/paleto_bank.json ships out of the box).

Update state from the callbacks

The panel doesn’t change state on its own — clicks fire your handlers, your handlers decide what to do, then you push the new state with these exports:
ExportDescription
ControlSetDoor(id, state)Replace one door’s state. Accepts a string ('locked', 'unlocked', …) or a table for richer states (e.g. { state = 'unlocked', expiresIn = 5000 }).
ControlSetCamera(id, state)Replace one camera’s state.
ControlClose()Close the panel. Fires onClose.
ControlIsOpen()Returns whether the panel is currently rendered.

Door state shape

FormExampleEffect
String'locked', 'unlocked'Plain state.
Table{ state = 'unlocked', expiresIn = 5000 }Time-limited unlock. The UI computes expiresAt against its own clock, and demotes the door back to plain 'locked' when the window expires. Fires onDoorRelocked so you can persist the demotion.

Camera state cycling

The shipped test command uses a three-state cycle (active → disabled → hacked → active) but nothing about the panel forces that — the strings are just labels you push. Use whatever vocabulary fits your heist resource.

Test command

/control [schematic]    # default: paleto_bank
/control close          # close the panel
/control paleto_bank opens the bundled schematics/paleto_bank.json and wires up a stub handler that:
  • Unlocks any clicked door for 5 seconds, then re-locks it.
  • Cycles cameras active → disabled → hacked → active on click.
  • Logs every state transition to the F8 console.
This is exactly the loop a heist resource will write — it’s a useful blueprint when you’re building the real one.

Cross-resource gotcha

Don’t pass closure callbacks via the :Function() colon-export syntax across resources unless you know what you’re doing. The Control panel uses pcall and a callable check that handles both plain functions and fxv2-wrapped callables, but if you see attempt to call a table value in your console it means the callback didn’t survive the export boundary. Use the standard exports.cc_minigames:ControlOpen({...}) form.