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.

cc_heistcontracts doesn’t ship any heists — it just runs the marketplace, slot economy, and dashboard around them. Your heist resource registers itself by calling RegisterContract on the server.

The RegisterContract export

local cc = exports.cc_heistcontracts

local id = cc:RegisterContract({
    id = 'fleeca_heist',
    name = 'Fleeca Heist',
    title = 'Fleeca Branch Heist',
    description = 'Hit a Fleeca branch. Low risk, low reward.',
    difficulty = 'easy',
    levelRequired = 1,
    price = 2500,
    crewSize = { min = 2, max = 4 },
    duration = 1200,
    reward = { vice = 1500, cash = 12000, xp = 250 },
    itemsRequired = {
        { id = 'lockpick', qty = 1 },
    },
    locations = {
        { id = 'pinkcage', label = 'Pinkcage Branch' },
        { id = 'rockford', label = 'Rockford Branch' },
    },
    objectives = {
        'Hack the electrical box',
        'Crack the vault',
        'Escape the police',
    },
}, function(runtimeData)
    -- Your heist runs from here. runtimeData includes:
    --   contractId  — the id you registered
    --   acquiredAt  — os.time() when the player bought the slot
    --   startedBy   — server source id of the player who started
    --   plus anything the dashboard threaded through (group name, location id, …)
end)
RegisterContract returns the contract id. Call it once per contract from your heist resource’s server boot — typically inside a delayed thread so dependencies are up first:
CreateThread(function()
    Wait(2000)
    exports.cc_heistcontracts:RegisterContract({ ... }, entryPoint)
end)

Field reference

FieldTypeDefaultNotes
idstringStable key. Required unless you pass name (then id = name). Don’t change after release — players’ slot rows reference it.
namestringfalls back to idLegacy display field, kept for back-compat.
titlestringfalls back to nameHeader shown in the dashboard.
descriptionstring''One- or two-line pitch.
difficultystring'medium'Free-form; the dashboard renders it as a chip. Common values: easy, medium, hard.
levelRequiredint1Server enforces this on purchase. The dashboard greys out the buy button below the threshold.
priceint0VICE cost to acquire a slot.
crewSize{ min, max }{ min = 1, max = 4 }Display-only today. Your entry point is responsible for enforcing actual crew size.
durationint (seconds)0Display-only.
reward{ vice, cash, xp }{}Display-only. Your entry point is responsible for actually crediting the player on success — call back into Profile.AddVice / AddXp (see below).
itemsRequiredarray of { id, qty }{}Display-only. The dashboard shows whether the player has them in their inventory; nothing is consumed at start.
locationsarray of { id, label }{}Surfaced to the dashboard so the player can pick which variant of the heist to run. The chosen location is threaded into runtimeData.location.
objectivesarray of strings{}Display-only checklist in the dashboard.
Anything outside this list is silently dropped by the normalizer — see server/registry.lua:normalize for the full list.

What happens after registration

1

Contract enters the registry

Stored in memory, keyed by id. Survives until the resource restarts.
2

Rotation re-rolls

The first registration ever triggers a one-shot re-roll so dev runs see the contract immediately. Otherwise you wait for the next tick of the rotation timer — config.rotation.intervalMs from boot.
3

Player buys it

They open the dashboard, see your contract in the rotation, click buy. The server validates VICE balance, level, and slot availability, debits the price, fills a slot.
4

Player starts it

They click Start in the slot card. The server clears the slot and calls your entry_point function with the runtimeData table.

Granting rewards from your heist

reward = { vice, cash, xp } on the contract definition is display only. Your entry point is the one that actually credits the player when the heist completes. The recommended way:
function entryPoint(runtimeData)
    -- ...run the heist...

    if heistSucceeded then
        local Profile = _G.HeistProfile
        if Profile then
            Profile.AddVice(runtimeData.startedBy, 1500, 'Fleeca heist completed')
            Profile.AddXp(runtimeData.startedBy, 250)
            Profile.RecordCompletion(runtimeData.startedBy, runtimeData.contractId, 1500)
        end
    else
        local Profile = _G.HeistProfile
        if Profile then
            Profile.RecordFailure(runtimeData.startedBy, runtimeData.contractId)
        end
    end
end
The _G.HeistProfile global is set by server/profile.lua and exposes the full mutator surface. Cash payouts use whatever your core’s money API is (e.g. cc_lib.Core or qbx_core directly) — cc_heistcontracts does not handle cash.
XP additions automatically advance the level if the new total crosses level thresholds. The dashboard reflects new XP / level on its next sync (the helper _G.HeistSync.SendProfile(src) triggers an immediate push).

The legacy StartContract export

exports.cc_heistcontracts:StartContract('fleeca_heist', { group = 'test', location = { id = 'pinkcage' } })
This is the old-shape entry — directly invokes a contract’s entry point, bypassing the slot/purchase/VICE flow. It still works (the seed test commands /heist:test and /heist:test2 use it), but new integrations should use the dashboard-driven slot flow.

Picking up cross-resource events

A few server events are useful for cross-resource integration:
EventWhen it firesUse case
cc_heistcontracts:rotationChangedServer-local; fired right after the rotation re-rolls.Notify a Discord webhook, refresh a separate UI, etc.
For player-action wiring (purchases, transfers, training unlocks) you generally don’t need to listen — the slot-fill / VICE debit is already routed through Profile, which you can read at any time via _G.HeistProfile.GetBySrc(src).