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

# Registering contracts

> How a heist resource hooks into cc_heistcontracts.

`cc_heistcontracts` doesn't ship any heists — it just runs the marketplace, slot economy, and dashboard around them. The whole integration lives in **one `RegisterContract` call**. No separate exports need to live on the heist resource.

<Warning>This page is for developers implementing their own heists. All of our heists already handles the registering of contracts.</Warning>

## The minimum integration

For a custom heist you control:

```lua theme={null}
exports.cc_heistcontracts:RegisterContract({
    id          = 'cool_bank',
    title       = 'Cool Bank Heist',
    description = "It's a heist. It's cool.",
    difficulty  = 'medium',
    weight      = 1.0,
    levelRequired = 5,
    price       = 5000,
    crewSize    = { min = 2, max = 4 },
    duration    = 1800,
    reward      = { vice = 3000, cash = 25000, xp = 500 },
    locations   = { { id = 'main', label = 'Vinewood Branch' } },
}, function(runtimeData)
    -- Start your heist. runtimeData carries:
    --   contractId, acquiredAt, startedBy, slotIndex, group, location?
    exports.cool_bank:Start({ group = runtimeData.group, location = runtimeData.location })

    -- Listen for the heist's completion event. Registered inside entry_point
    -- so the closure can match against this run's group, and removed after
    -- it fires so handlers don't stack across heists.
    local handler
    handler = AddEventHandler('cool_bank:complete', function(group, success)
        if group ~= runtimeData.group then return end
        RemoveEventHandler(handler)
        exports.cc_heistcontracts:FinishHeist(group, success)
    end)
end, {
    -- Optional. Fires when a player presses Give Up on the dashboard.
    cancel = function(ctx)
        -- ctx = { src, group, contractId, slotIndex }
        -- Tear the heist down for ctx.group. Slots are freed automatically.
        exports.cool_bank:Abort(ctx.group)
    end,
})
```

That's the whole integration. Three exports total: `RegisterContract` (once), `FinishHeist` (whenever the heist ends, fired from inside the handler above), and optionally `ApplyCooldown` if the contract has a `cooldown` set.

<Note>
  Why register the completion handler **inside `entry_point`** rather than at the top of the file?

  * It only listens while a heist is actually running.
  * The handler closes over `runtimeData.group`, so it knows which run the event belongs to. A top-level handler would have to figure that out from the event payload alone.

  Removing the handler on first fire (`RemoveEventHandler(handler)`) prevents the listener list from growing every time the heist starts. The wider pattern is "one handler per run, scoped to that run."
</Note>

## The three exports

| Export                                                                 | When                                                                              |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `exports.cc_heistcontracts:RegisterContract(data, entry_point, opts?)` | On resource start. Registers the contract so it can drip into the marketplace.    |
| `exports.cc_heistcontracts:FinishHeist(groupName, success)`            | When the heist ends. Loops every member of `groupName` and finishes their slot.   |
| `exports.cc_heistcontracts:FinishContract(src, success)`               | Single-player variant of `FinishHeist`. Use when you already have a specific src. |

And one optional helper:

| Export                                                | When                                                                        |
| ----------------------------------------------------- | --------------------------------------------------------------------------- |
| `exports.cc_heistcontracts:ApplyCooldown(contractId)` | When you want to lock the contract server-wide for the configured cooldown. |

## `RegisterContract`

### Signature

```lua theme={null}
local id = exports.cc_heistcontracts:RegisterContract(data, entry_point, opts?)
```

`data` is the contract table. `entry_point(runtimeData)` is called when a player starts the contract from a slot. `opts` is an optional table that currently accepts two callbacks:

```lua theme={null}
opts = {
    -- Fired when the contract's cooldown timer expires. Server-wide, fires
    -- once per cooldown cycle, no player context.
    reset  = function(ctx) ... end,    -- ctx = { contractId, firedAt }

    -- Fired when a player presses Give Up on the dashboard. Per-player, with
    -- the group context attached. cc_heistcontracts auto-finishes every group
    -- member's slot as failure after this callback returns.
    cancel = function(ctx) ... end,    -- ctx = { src, group, contractId, slotIndex }
}
```

Both are optional. The Give Up button on the dashboard is **hidden** when `cancel` isn't set — the integration shouldn't offer a control that does nothing.

`RegisterContract` returns the contract `id`.

### Field reference

| Field           | Type                     | Default                | Notes                                                                                                                                                                                           |
| --------------- | ------------------------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | string                   | —                      | Stable key. **Required** unless you pass `name` (then `id = name`). Don't change after release — players' slot rows reference it.                                                               |
| `name`          | string                   | falls back to `id`     | Legacy display field, kept for back-compat.                                                                                                                                                     |
| `title`         | string                   | falls back to `name`   | Header shown in the dashboard.                                                                                                                                                                  |
| `description`   | string                   | `''`                   | One- or two-line pitch.                                                                                                                                                                         |
| `difficulty`    | string                   | `'medium'`             | Free-form; the dashboard renders it as a chip. Common values: `easy`, `medium`, `hard`.                                                                                                         |
| `weight`        | number                   | `1.0`                  | Relative probability of being picked when the marketplace drips a new listing. Higher = more common. Set to `0` to disable from marketplace generation entirely.                                |
| `levelRequired` | int                      | `1`                    | Server enforces on purchase. The dashboard greys out the buy button below the threshold.                                                                                                        |
| `price`         | int                      | `0`                    | VICE 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.                                                                                                             |
| `duration`      | int (seconds)            | `0`                    | Display-only.                                                                                                                                                                                   |
| `reward`        | `{ vice, cash, xp }`     | `{}`                   | `vice` and `xp` are **awarded automatically** by `FinishContract`/`FinishHeist` on success. `cash` is display-only — your entry point handles cash and item payouts.                            |
| `itemsRequired` | array of `{ id, qty }`   | `{}`                   | Display-only. The dashboard shows whether the player has them; nothing is consumed at start.                                                                                                    |
| `locations`     | array of `{ id, label }` | `{}`                   | If non-empty and the caller didn't pick one, the server rolls a random entry and threads it into `runtimeData.location`.                                                                        |
| `objectives`    | array of strings         | `{}`                   | Display-only checklist in the dashboard.                                                                                                                                                        |
| `cooldown`      | int (seconds)            | `0`                    | Server-wide cooldown duration. Stamped only when you call `ApplyCooldown`. While active, `Purchase.Start` rejects with `'on_cooldown'` for everyone. `0` disables. See [Cooldowns](#cooldowns). |

### What happens after registration

<Steps>
  <Step title="Contract enters the registry">
    Stored in memory, keyed by `id`. Survives until the resource restarts.
  </Step>

  <Step title="Marketplace drips a listing">
    On the next `dripIntervalMs` tick, the marketplace may pick your contract (weighted by `weight`) and add a new listing. The same contract can be listed multiple times in parallel.
  </Step>

  <Step title="Player buys a listing">
    The server validates VICE balance, level, and slot availability, debits the price, fills a slot, and removes the listing.
  </Step>

  <Step title="Player starts a slot">
    The server checks the contract isn't on cooldown and the player isn't already in another heist, marks the slot active, rolls a random `location` if you didn't pass one, and calls **your `entry_point` function**.
  </Step>

  <Step title="Heist runs">
    Your code runs. The dashboard, if reopened, shows an "in progress" panel with elapsed time and (if you provided `opts.cancel`) a Give Up button.
  </Step>

  <Step title="Heist ends">
    Your code calls `FinishHeist(group, success)` for the whole group, or `FinishContract(src, success)` for one player. cc\_heistcontracts consumes each player's slot, awards rewards on success, records stats, and pushes a fresh profile to their dashboard.
  </Step>
</Steps>

## `FinishHeist` and `FinishContract`

```lua theme={null}
exports.cc_heistcontracts:FinishHeist(groupName, success)
exports.cc_heistcontracts:FinishContract(src, success)
```

Two ways to signal completion:

* `FinishHeist(groupName, success)` — call once with a `cc_lib.Groups` name. Walks every member and finishes their slot.
* `FinishContract(src, success)` — single-player variant. Use when you already have a specific source id and don't want to walk a group.

Both pass `true` for success or `false` for failure.

**What happens on success:**

* The player's active slot is consumed (gone after this call).
* `reward.vice` is credited to their VICE balance with a ledger entry.
* `reward.xp` is credited to their profile, advancing level if a threshold is crossed.
* `stats.completed` and `stats.byContract[id].completed` increment.
* `stats.favoriteContract` updates if this contract is now their most-completed.
* A fresh profile snapshot is pushed to the player's dashboard.

**What happens on failure:**

* The slot is consumed.
* `stats.failed` and `stats.byContract[id].failed` increment.
* No rewards.

**Idempotent.** A second call for the same player while no heist is active returns `(false, 'not_active')`. Safe to call defensively from multiple code paths.

### Picking your finish moment

Where you call `FinishHeist` defines what "the heist is over" means for your players. Typical patterns:

* **Objective-driven**: when the last objective is completed (`cc_lib.TaskUI.Create` accepts an `onComplete` callback that fires when every task hits `'complete'`).
* **Escape-driven**: when the player crosses an escape boundary or hands the goods off.
* **Timer-driven**: when the heist's overall timer runs out — call with `success = false`.

`FinishHeist` is a pure server-side state mutation — players don't see anything happen visually from it alone. Your heist resource is still responsible for the in-world feedback (notifications, animations, world reset).

## `opts.cancel` — handling Give Up

When a player presses Give Up on the dashboard, cc\_heistcontracts:

1. Calls your `opts.cancel(ctx)` callback. Your job here is to tear the heist down for `ctx.group` (notify players, hide UI, reset world state).
2. After the callback returns, **automatically finishes every member of `ctx.group`** as failure. You don't need to call `FinishContract` yourself for cancellation.

```lua theme={null}
opts = {
    cancel = function(ctx)
        -- ctx.src         — the player who pressed Give Up
        -- ctx.group       — the cc_lib.Groups name for the active heist
        -- ctx.contractId  — id of the contract being cancelled
        -- ctx.slotIndex   — 1-based slot the contract was in

        -- Tell everyone, then tear the in-world heist down.
        local cc = exports.cc_lib:GetLib()
        for _, member in pairs(cc.Groups.GetMembers(ctx.group)) do
            cc.Notification.Info(member, 'Heist cancelled')
        end
        exports.cool_bank:Abort(ctx.group)
    end,
}
```

**If you don't provide `opts.cancel`**, the Give Up button is hidden on the dashboard's active-heist panel. The player can still finish the heist normally, or wait it out — they just can't bail mid-run. This is the right default for heists that don't have a clean abort path.

### Why this isn't an export on your resource

`opts.cancel` is a closure registered alongside your contract data. It lets a third-party heist integration live entirely inside a single wrapper file — the wrapper doesn't need to add an export to the escrowed heist resource (which you couldn't modify anyway).

## Granting rewards

`reward.vice` and `reward.xp` from your contract definition are **credited automatically** on success. You don't need to call `Profile.AddVice` or `Profile.AddXp` yourself for those.

`reward.cash` is **not** handled by `cc_heistcontracts` — it's there for the dashboard's display only. Cash and inventory payouts are your responsibility:

```lua theme={null}
AddEventHandler('cool_bank:complete', function(group, success)
    if success then
        local cc = exports.cc_lib:GetLib()
        for _, member in pairs(cc.Groups.GetMembers(group)) do
            -- Cash + items: your code, your core.
            cc.Core.AddMoney(member, 'cash', 12000)
            cc.Inventory.AddItem(member, 'cash_stack', 1)
        end
    end
    -- VICE + XP + stats: cc_heistcontracts handles them.
    exports.cc_heistcontracts:FinishHeist(group, success)
end)
```

## Cooldowns

Cooldowns are **server-wide per contract**, not per player. Set `cooldown = <seconds>` on the contract and call `ApplyCooldown` from your entry point when the heist starts (or when it ends — your choice). While the cooldown is active, `Purchase.Start` rejects with `'on_cooldown'` for every player. Buying is **not** blocked — players can still acquire a slot during cooldown, they just can't start it until the timer ends.

```lua theme={null}
exports.cc_heistcontracts:RegisterContract({
    id = 'paleto_bank',
    -- ...
    cooldown = 3600, -- 1 hour, server-wide
}, function(runtimeData)
    exports.cc_heistcontracts:ApplyCooldown(runtimeData.contractId)
    -- ...run the heist...
end, {
    reset = function(ctx)
        -- ctx = { contractId, firedAt }
        -- Fires once, server-wide, when the cooldown ends. Use this to
        -- restore world state for the next run (respawn props, reset NPCs,
        -- clear pending alerts, etc.).
    end,
})
```

### `ApplyCooldown` export

```lua theme={null}
exports.cc_heistcontracts:ApplyCooldown(contractId)
```

Stamps the configured cooldown on `contractId` and schedules the `reset` callback. Returns the unix-seconds expiry timestamp, or `0` if the contract has no `cooldown` configured (or the id is unknown). Calling it again before the timer expires re-stamps from "now" — the previously-scheduled reset will silently no-op.

### Behaviour notes

* **In-memory only.** Cooldowns do not persist across resource restarts.
* **Single timer per contract.** `reset` fires exactly once per cooldown cycle, even if `ApplyCooldown` is called multiple times during the cycle.
* **No automatic apply.** `cooldown` on the contract definition is just a duration — nothing happens until your entry point calls `ApplyCooldown`.
* **Surfaced in the rotation payload.** Active cooldowns ship to the dashboard as `rotation.cooldowns = { [contractId] = expiresAtUnixSeconds }`, so the UI can grey out cooldowned cards.

## `reset` vs. `cancel`

Both are callbacks you can register in `opts`. They fire on different events and have different jobs:

|                 | `reset`                                                               | `cancel`                                                                    |
| --------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Trigger**     | Cooldown expiry timer                                                 | Player pressed Give Up                                                      |
| **Scope**       | Server-wide, world state                                              | Per-player, the active run                                                  |
| **Fires per**   | Cooldown cycle (once)                                                 | Cancellation (once per Give Up)                                             |
| **Context**     | `{ contractId, firedAt }`                                             | `{ src, group, contractId, slotIndex }`                                     |
| **Typical job** | Respawn props, re-arm alarms, reset NPCs so the next run starts fresh | Notify the crew, hide the task UI, abort the in-world heist for `ctx.group` |

A heist can have both, one, or neither.

## Integrating a third-party (escrowed) heist

The same shape works for heists you didn't write — including ones whose source is escrowed. Create a small wrapper resource (call it `myserver_heists`) with a single Lua file:

```lua theme={null}
-- myserver_heists/server/cool_bank.lua
CreateThread(function()
    Wait(2000) -- let cc_heistcontracts boot

    exports.cc_heistcontracts:RegisterContract({
        id    = 'cool_bank',
        title = 'Cool Bank Heist',
        -- ...the contract fields...
    }, function(runtimeData)
        -- Start the escrowed heist however its docs tell you to.
        exports.cool_bank:Start({ group = runtimeData.group })

        -- Hook the heist's completion event for this run, then drop the
        -- handler once it fires so listeners don't pile up across heists.
        local handler
        handler = AddEventHandler('cool_bank:complete', function(group, success)
            if group ~= runtimeData.group then return end
            RemoveEventHandler(handler)
            exports.cc_heistcontracts:FinishHeist(group, success)
        end)
    end, {
        cancel = function(ctx)
            -- Call whatever abort/cancel the escrowed heist exposes.
            if exports.cool_bank.Abort then
                exports.cool_bank:Abort(ctx.group)
            end
        end,
    })
end)
```

That's the entire wrapper. **No edits to the escrowed heist itself.**

**What if the escrowed heist doesn't expose an abort?** Omit `opts.cancel`. The Give Up button will be hidden on the dashboard — players have to finish or wait it out.

**What if the escrowed heist doesn't fire a completion event?** That's the one case you genuinely can't wrap around. Players' slots will get stuck "active" because nothing ever calls `FinishHeist`. Ask the escrowed heist's author to add an event.

Add `cc_heistcontracts` and `cc_lib` to your wrapper's `fxmanifest.lua` dependencies so boot order is correct:

```lua theme={null}
dependencies {
    'ox_lib',
    'cc_heistcontracts',
    'cc_lib',
}
```
