Skip to content

Contract App

Full documentation for the tablet Contracts app — a contract board where players buy and start job/heist contracts with global locking, stock management, and a queue system.

Overview

The Contracts app lets players browse, purchase, and start activity contracts (heists, robberies, jobs). Each contract type has a global stock (shared across all players) and a per-player purchase cap. Only ONE active instance per contract type can exist server-wide — if Group A starts a gruppe6_heist contract, no other group can start it until Group A completes or fails. Other groups join a start queue and are notified when the contract becomes available.

Architecture

┌──────────────────────────────────────────────────────────────┐
│                     Contract App (React)                      │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │ Browse   │  │ My Contracts │  │ Queue                   │ │
│  │ (cards)  │  │ (owned list) │  │ (start queue positions) │ │
│  └────┬─────┘  └──────┬───────┘  └───────────┬────────────┘ │
│       └───────────────┼──────────────────────┘               │
│                       │ nuiFetch                             │
├───────────────────────┼──────────────────────────────────────┤
│                client/contracts.lua                          │
│  RegisterNUICallback('contracts:*')                          │
│    → TriggerServerEvent('wtf_group:server:contracts:*')      │
├───────────────────────┼──────────────────────────────────────┤
│                server/contracts.lua                          │
│  ContractMgr:                                                 │
│    ├── GetTypes() → stock, playerOwned, isActive, queueLen   │
│    ├── Buy() → money check, stock check, player cap          │
│    ├── Start() → global lock, group check, consume, trigger  │
│    ├── Complete() → unlock, cooldown, process queue          │
│    ├── JoinStartQueue() / LeaveStartQueue()                  │
│    ├── RestockLoop() → periodic stock restoration            │
│    └── 7 Exports for external resources                      │
├───────────────────────┼──────────────────────────────────────┤
│                MySQL (oxmysql)                                │
│  wtf_contract_stock         — global stock per type          │
│  wtf_contract_active        — global lock (1 per type)       │
│  wtf_contract_inventory     — player-owned contracts          │
│  wtf_contract_start_queue   — FIFO start queue               │
│  wtf_contract_history       — purchase/start log             │
└──────────────────────────────────────────────────────────────┘

Files

FilePurpose
server/contracts.luaServer logic: buy, start, complete, queue, stock, exports (~350 lines)
client/contracts.luaNUI callback bridge + event listeners (~90 lines)
ui/src/apps/Contracts/index.jsxReact UI: browse, inventory, queue views (~500 lines)
config/config.luaConfig.Contracts section
server/database.lua5 new tables in ensureTables()
ui/src/lib/appRegistry.jscontracts app entry
ui/src/lib/icons.jsxContractsIcon SVG component

Config

lua
Config.Contracts = {
    enabled = true,
    maxPerPlayer = 3,              -- max contracts of each type per player
    restockInterval = 3600,        -- seconds between stock restocks
    restockAmount = 5,             -- how many to add per restock
    requireGroup = true,           -- must be in a group to buy/start
    requireLeaderToStart = true,   -- only leader can start
    cooldownActivity = 'contract', -- cooldown key for wtf_group
    showStock = true,
    showPlayerCount = true,
    globalLock = true,             -- ONE active instance per type server-wide

    categories = {
        { id = 'all', label = 'All', icon = '📋' },
        { id = 'heist', label = 'Heists', icon = '💀' },
        { id = 'robbery', label = 'Robberies', icon = '🔫' },
        { id = 'job', label = 'Jobs', icon = '💼' },
    },

    contracts = {
        gruppe6_heist = {
            name = 'Gruppe6 Armored Truck',
            description = 'Rob a Gruppe6 Stockade armored truck.',
            category = 'heist',
            price = 50000,
            initialStock = 10,
            cooldown = 3600,         -- 1 hour
            failCooldown = 600,      -- 10 minutes
            minPlayers = 1,
            minLevel = 1,
            enabled = true,
            integration = {
                resource = 'gruppe6-heist',
                startEvent = 'gruppe6_heist:client:startFromContract',
                startTarget = 'client',
            },
        },
    },
}

Database Tables

wtf_contract_stock

Global stock per contract type.

ColumnTypeDescription
contract_idVARCHAR(50) PKContract type ID
current_stockINTCurrent available stock
max_stockINTMaximum stock (initial stock)
last_restock_atBIGINTLast restock timestamp

wtf_contract_active

Global lock — only one row per contract type when in use.

ColumnTypeDescription
contract_idVARCHAR(50) PKContract type ID (locked)
citizenidVARCHAR(50)Player who started it
group_idVARCHAR(50)Group doing the contract
started_atBIGINTStart timestamp
mission_idVARCHAR(50)External mission ID

wtf_contract_inventory

Player-owned contracts.

ColumnTypeDescription
idINT PK AUTOAuto-increment
citizenidVARCHAR(50)Owner
contract_idVARCHAR(50)Contract type
purchased_atBIGINTPurchase timestamp
statusENUMactive, started, completed, failed
started_atBIGINTWhen started
mission_idVARCHAR(50)External mission reference

wtf_contract_start_queue

FIFO queue for locked contract types.

ColumnTypeDescription
idINT PK AUTOAuto-increment
citizenidVARCHAR(50)Player in queue
contract_idVARCHAR(50)Contract type
queued_atBIGINTJoin timestamp

wtf_contract_history

Purchase/start/complete log.

ColumnTypeDescription
idINT PK AUTOAuto-increment
citizenidVARCHAR(50)Player
contract_idVARCHAR(50)Contract type
actionENUMpurchased, started, completed, failed
price_paidINTMoney spent
group_idVARCHAR(50)Group during action
mission_idVARCHAR(50)External mission ID
timestampBIGINTAction timestamp

Global Contract Lock

When Config.Contracts.globalLock = true:

  • Only ONE active instance per contract type at any time server-wide
  • wtf_contract_active has PRIMARY KEY on contract_id — MySQL enforces uniqueness
  • If Player A starts gruppe6_heist → type is locked
  • Player B sees "In Use by Player A" → can join start queue
  • When Player A completes/fails → type unlocks → first in queue notified
  • If player disconnects while contract is active → forced complete, type unlocks

Start Queue

When a contract type is locked:

  1. Player clicks "Join Queue" → added to wtf_contract_start_queue
  2. Queue is FIFO by queued_at timestamp
  3. When active contract completes/fails:
    • First player in queue is removed and notified
    • ox_lib notification: "Gruppe6 Armored Truck is now available!"
    • NUI message: contracts:startReady triggers UI refresh
  4. Player can now start the contract normally

Data Flow

Buy Contract

React: nuiFetch('contracts:buy', { contractId })
  → client: TriggerServerEvent('wtf_group:server:contracts:buy', contractId)
  → server: ContractMgr.Buy(src, contractId)
    ├── Check player cap (dbCountOwned)
    ├── Check stock (StockCache)
    ├── Check money (Bridge.GetMoney)
    ├── Bridge.RemoveMoney
    ├── Decrement stock (dbUpdateStock)
    ├── Add to inventory (dbAddInventory)
    └── Log history (dbLogHistory)
  → TriggerClientEvent('wtf_group:client:contracts:buyResult', src, ok, msg, contractId)
  → React: toast + refresh types/inventory

Start Contract

React: nuiFetch('contracts:start', { contractId })
  → server: ContractMgr.Start(src, contractId)
    ├── Check player owns active contract
    ├── Check global lock (ActiveContracts[contractId])
    ├── Check group, leader, min players, min level
    ├── Check cooldown (Bridge.IsOnCooldown)
    ├── Lock group (Bridge.SetLocked)
    ├── Mark inventory as 'started'
    ├── Set ActiveContracts + dbSetActive
    ├── Log history
    └── Trigger integration (if configured)
  → React: toast + refresh

Complete Contract (from external resource)

External resource: exports.wtf_group:CompleteContract(src, contractId, missionId, success)
  → server: ContractMgr.Complete(src, contractId, missionId, success)
    ├── Update inventory status ('completed'/'failed')
    ├── Set cooldown (Bridge.SetCooldown)
    ├── Unlock group (Bridge.SetLocked)
    ├── Remove from ActiveContracts + dbRemoveActive
    ├── Log history
    ├── Notify player (contracts:completed)
    └── Process start queue (ContractMgr.ProcessStartQueue)

Player Disconnect

playerDropped event
  → ContractMgr.ForceComplete(contractId, false, 'Player disconnected')
    ├── Same as Complete but forced
    ├── Unlocks contract type
    └── Processes start queue

Exports (for Other Resources)

lua
-- Check if a contract type is available (not in use, has stock)
exports.wtf_group:IsContractAvailable(contractId) → boolean

-- Get active contract info for a type
exports.wtf_group:GetActiveContract(contractId) → { citizenid, groupId, startedAt } | nil

-- Get start queue length for a contract type
exports.wtf_group:GetContractQueueLength(contractId) → number

-- Report contract completion/failure
exports.wtf_group:CompleteContract(source, contractId, missionId, success) → boolean, string

-- Get all active contracts across the server
exports.wtf_group:GetActiveContracts() → { [contractId] = { ... } }

-- Get a player's contract inventory
exports.wtf_group:GetPlayerContracts(source) → { { id, contract_id, purchased_at, status } }

-- Check if a player owns an active contract
exports.wtf_group:HasActiveContract(source, contractId) → boolean

Usage Examples

lua
-- Another heist resource: report completion
RegisterNetEvent('my_heist:completed', function(success)
    local src = source
    exports.wtf_group:CompleteContract(src, 'my_heist', missionId, success)
end)

-- Check availability before allowing an action
if exports.wtf_group:IsContractAvailable('gruppe6_heist') then
    -- Contract is free and in stock
end

-- Get active contract info
local active = exports.wtf_group:GetActiveContract('gruppe6_heist')
if active then
    print('In use by: ' .. active.citizenid)
end

NUI Callbacks

CallbackDataDescription
contracts:getTypesGet all contract types with stock + player info
contracts:getInventoryGet player's owned contracts
contracts:buy{ contractId }Purchase a contract
contracts:start{ contractId }Start a contract
contracts:joinStartQueue{ contractId }Join start queue
contracts:leaveStartQueue{ contractId }Leave start queue
contracts:getStartQueue{ contractId }Get start queue data
contracts:getHistoryGet purchase history

NUI Messages (Received by React)

ActionDataDescription
contractsResponse + contracts:getTypes{ [id]: { ... } }Contract types data
contractsResponse + contracts:getInventory[{ id, contract_id, ... }]Player inventory
contractsResponse + contracts:buy{ success, message, contractId }Buy result
contractsResponse + contracts:start{ success, message, contractId }Start result
contractsResponse + contracts:startQueue{ success, message, contractId }Queue join result
contractsResponse + contracts:startReady{ contractId }Contract unlocked, you can start
contractsResponse + contracts:stockUpdate{ contractId, stock }Stock restocked
contractsResponse + contracts:completed{ contractId, success, missionId }Heist result
contractsResponse + contracts:getHistory[{ ... }]History data

UI Views

Browse View

  • Category tabs: All, Heists, Robberies, Jobs
  • Search bar for filtering
  • Contract cards: name, description, price, stock bar, owned count, status badge
  • Detail overlay: full info, buy/queue button

My Contracts View

  • List of owned contracts with status badges
  • Start button (leader only)
  • Confirmation modal before starting

Queue View

  • Lists contract types currently in use by other players
  • Join Queue button for each
  • Shows queue positions and player names

Integration with gruppe6_heist

The contract system integrates with gruppe6-heist via event triggering:

  1. Config: integration.startEvent = 'gruppe6_heist:client:startFromContract'
  2. Start: When a contract starts, the server fires this event on the player's client
  3. Heist runs: The gruppe6-heist resource handles the full heist lifecycle
  4. Completion: The external resource calls exports.wtf_group:CompleteContract() to report success/failure
  5. Contract cleanup: Status updated, cooldown set, group unlocked, start queue processed

Optional Integration

The integration config is optional. If not set, the contract system works independently:

  • Stock, inventory, and locking still work
  • The "Start" button triggers a generic event
  • Other resources can listen for wtf_group:server:contractStarted and handle it

Notes

  • Stock restocks periodically based on restockInterval
  • Global lock uses PRIMARY KEY constraint — race-condition safe
  • Player disconnect during active contract triggers forced completion
  • Cooldown uses wtf_group's existing cooldown system
  • Contracts are one-time use: consumed on start, even if heist fails
  • The contract system is framework-agnostic (uses Bridge for all framework calls)
  • History is retained for admin auditing

AIFAZI — FiveM Resources