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.
The minimum integration
For a custom heist you control:RegisterContract (once), FinishHeist (whenever the heist ends, fired from inside the handler above), and optionally ApplyCooldown if the contract has a cooldown set.
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.
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.”The three exports
And one optional helper:
RegisterContract
Signature
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:
cancel isn’t set — the integration shouldn’t offer a control that does nothing.
RegisterContract returns the contract id.
Field reference
What happens after registration
1
Contract enters the registry
Stored in memory, keyed by
id. Survives until the resource restarts.2
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.3
Player buys a listing
The server validates VICE balance, level, and slot availability, debits the price, fills a slot, and removes the listing.
4
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.5
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.6
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.FinishHeist and FinishContract
FinishHeist(groupName, success)— call once with acc_lib.Groupsname. 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.
true for success or false for failure.
What happens on success:
- The player’s active slot is consumed (gone after this call).
reward.viceis credited to their VICE balance with a ledger entry.reward.xpis credited to their profile, advancing level if a threshold is crossed.stats.completedandstats.byContract[id].completedincrement.stats.favoriteContractupdates if this contract is now their most-completed.- A fresh profile snapshot is pushed to the player’s dashboard.
- The slot is consumed.
stats.failedandstats.byContract[id].failedincrement.- No rewards.
(false, 'not_active'). Safe to call defensively from multiple code paths.
Picking your finish moment
Where you callFinishHeist defines what “the heist is over” means for your players. Typical patterns:
- Objective-driven: when the last objective is completed (
cc_lib.TaskUI.Createaccepts anonCompletecallback 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:
- Calls your
opts.cancel(ctx)callback. Your job here is to tear the heist down forctx.group(notify players, hide UI, reset world state). - After the callback returns, automatically finishes every member of
ctx.groupas failure. You don’t need to callFinishContractyourself for cancellation.
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:
Cooldowns
Cooldowns are server-wide per contract, not per player. Setcooldown = <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.
ApplyCooldown export
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.
resetfires exactly once per cooldown cycle, even ifApplyCooldownis called multiple times during the cycle. - No automatic apply.
cooldownon the contract definition is just a duration — nothing happens until your entry point callsApplyCooldown. - 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:
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 itmyserver_heists) with a single Lua file:
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: