Custom Integrations
Connect custom CAD, money, and inventory providers to FS-Robberies
Before you start
This guide was checked against the local FS-Robberies 1.3.1 source and built resource. The local release-version repository also lists 1.3.1. Compare these files with your installed download before applying an example.
Selecting custom chooses an empty adapter; it does not automatically discover your provider. Use a built-in integration when it already matches your server. Back up the files below before editing and reapply your small adapter changes when updating.
| Installed resource file | Side | Purpose |
|---|---|---|
config.lua | Shared | Select providers and reward behavior. Never put API secrets here. |
dist/server/cad.lua | Server | Edit CADHandlers["custom"]. |
dist/server/framework.lua | Server | Edit MoneyHandlers["custom"], InventoryHandlers["custom"], and InventoryRemoveHandlers["custom"]. |
dist/client/framework/inventory.lua | Client | Contains the custom item requirement check; see the escrow caveat below. |
For source builds, the server files are src/server/cad.lua and src/server/framework.lua; build through the project's normal release process. Editing a source file does not change an already installed dist file. These tables are local to FS-Robberies: another resource cannot replace them with an export or register a handler dynamically.
The manifest explicitly excludes the two server files and config.lua from escrow. It lists dist/client/framework.lua, which does not match the actual dist/client/framework/inventory.lua path. Do not assume that the client inventory adapter is editable in an escrowed download. If locked, request a release with that path exposed; changing a local manifest cannot unlock encrypted code.
Custom CAD or dispatch
Keep the existing CAD configuration and change these fields:
Config.CAD.Enabled = true
Config.CAD.CADSystem = 'custom'Replace only the custom entry inside CADHandlers in dist/server/cad.lua. The dispatcher supplies these arguments in order:
| Argument | Contract |
|---|---|
src | Triggering player's server ID, captured from the event's source. |
callType | Normal alarm categories: register, atm, safe, vault, panic. Treat unknown values defensively. |
street, crossStreetName, zoneName | Location strings; normalize missing values before passing them on. |
postal | Postal value, possibly absent; convert to a string if your provider requires one. |
description | Alarm description. |
callerCoords | Coordinate value with x, y, z fields. |
No return value or completion callback is consumed. Handle provider failure inside your adapter. The following is a generic diagnostic template, not a named-provider integration: it runs and logs valid calls but deliberately creates no CAD incident.
["custom"] = function(src, callType, street, crossStreetName, zoneName, postal, description, callerCoords)
if not callerCoords then return end
local x = tonumber(callerCoords.x)
local y = tonumber(callerCoords.y)
local z = tonumber(callerCoords.z)
if not x or not y or not z then return end
local call = {
playerId = src,
category = tostring(callType or 'robbery'),
street = tostring(street or ''),
crossStreet = tostring(crossStreetName or ''),
zone = tostring(zoneName or ''),
postal = postal ~= nil and tostring(postal) or nil,
description = tostring(description or ''),
coords = { x = x, y = y, z = z },
}
print(('[FS-Robberies custom CAD] %s'):format(json.encode(call)))
-- Replace this diagnostic with your provider's documented server API.
end,The call keys above belong to this example, not to any CAD API. Map them to the documented payload for your provider and installed version. Verify whether its API is a server export, a client export, or HTTP. A server script cannot call a client export directly. If the provider requires a client event, target the intended player and use the provider's documented event and payload; do not broadcast duplicate incident creation to all clients. Keep HTTP credentials in server-only configuration and log status codes without credentials.
Built-in providers and LUX CAD
The inspected CAD table contains sonorancad, imperialcad, night_shifts, nd_mdt, lb_tablet, dusa_dispatch, lux_cad, and custom. Those are configuration selectors, not necessarily resource names. For example, lb_tablet calls resource lb-tablet, and imperialcad calls ImperialCAD.
The local lux_cad branch uses server-side HTTP and reads the server convar lux_api_key; it also uses Config.CAD.LuxCAD for incident codes and the call prefix. Its existence in source is not verification of the current external LUX API, credentials, or your community's incident codes. This guide supplies no plug-and-play LUX recipe. Verify the official API contract and your installed release before enabling it. The generic template above is not a LUX payload. Do not confuse robbery panic incidents with a provider's separate system panic workflow.
Custom money
Config.MoneyFramework.Enabled = true
Config.MoneyFramework.Framework = 'custom'Implement MoneyHandlers["custom"] in dist/server/framework.lua:
| Argument | Contract |
|---|---|
src | Player receiving the reward. Use this server-derived ID. |
amount | Positive payout passed by the robbery flow. Legacy payouts are converted with tonumber; authoritative rewards are also floored. |
plrServerID | Legacy client-supplied ID; authoritative reward calls pass src. Do not use it to select another recipient. |
The caller ignores the handler's return value. There is no success callback, automatic retry, or rollback. Record provider failures and reconcile them before retrying to avoid double credit. Numeric validation in an adapter does not itself prove that a robbery reward was earned.
Verified FS-Money example
For ordinary FS-Money payouts, select Framework = 'fs-money' and leave the built-in handler in place. For a custom handler with a distinct ledger source and reason, this example uses the locally verified FS-Money server export:
["custom"] = function(src, amount, _)
amount = tonumber(amount)
if not amount or amount ~= amount or amount == math.huge or amount <= 0 then return end
if not GetPlayerName(src) then return end
if GetResourceState('FS-Money') ~= 'started' then
print('[FS-Robberies] FS-Money unavailable; payout not attempted.')
return
end
local ok, credited = pcall(function()
return exports['FS-Money']:addMoney('cash', src, amount, 'FS-Robberies', 'Stolen Cash', true)
end)
if not ok or credited ~= true then
print(('[FS-Robberies] Check payout for player %s; do not retry blindly.'):format(src))
end
end,FS-Money's signature is addMoney(moneyType, source, amount, sourceTag, reason, sendNotify) and it returns a boolean. The fourth parameter is the ledger source tag, not the reason. Ensure the player's money account has been initialized and check the actual balance/ledger; a notification alone is not proof of payment. For a different provider, replace the export using its verified server API and success contract.
Why cash may not be paid
If Config.Inventory.Enabled is true and the selected robbery's Rewards.AsItem and Rewards.ItemName are set, the item handler runs instead of the money handler. The amount is the reward amount used as an item count, not automatically one item worth that amount. Configure quantities deliberately.
Reward keys are case-sensitive: Config.Robbery.Register, ATM, Safe, and Vault. Requirement keys are lower-case under Config.Inventory.Requirements. Vault rewards include separate deposit-box, cash-pile, and cart ranges; keep their existing configuration.
Custom inventory
Set Config.Inventory.Enabled = true and Config.Inventory.Framework = 'custom' only after implementing the relevant hooks:
| Hook | Side and return contract |
|---|---|
InventoryHandlers.custom(src, itemName, amount, robberyType) | Server reward grant. Return ignored. Add the requested quantity using your provider and handle failures locally. |
InventoryRemoveHandlers.custom(src, itemName, amount, robberyType) | Server consumption after configured failures. Return ignored. Remove from src, not a client-selected player. |
InventoryHandlers.custom.hasItem(requirement) in the client inventory file | Client requirement check. Must synchronously return boolean true to allow the action; nil and false reject it. The requirement contains Required, Item, and Amount. |
The client check is a user-interface gate, not authority to grant rewards. If your inventory only offers asynchronous queries, do not return before a callback completes and expect the result to be used later. Implement a provider-supported synchronous/cached check and enforce any required authoritative checks server-side in your integration.
The inspected reward table implements only ox_inventory and custom; the removal and client requirement tables also contain QB, PS, and TGIANN adapters. A selectable inventory framework therefore does not guarantee item-reward support. The stock ox_inventory client check checks for at least one item, while other requirement adapters receive requirement.Amount. Test quantities greater than one explicitly.
Events and internal functions
FS-Robberies:createCall(street, crossStreetName, zoneName, postal, description, callerCoords, callType) is an existing client-to-server network event; its parameter order differs from the handler's. FS-Robberies:payMoney(plrServerID, collectedAmount, robberyType) is a legacy client-to-server payout event. Neither is a custom-provider registration API. Do not add commands or network relays that expose arbitrary rewards through it.
FSRGrantServerReward(src, amount, robberyType, rewardCfg) is an internal server function, not an export. Its boolean result means that a matching handler was invoked, not that an external provider confirmed the credit. Do not call it from a separate resource as though it were an export. Supported vault-control exports remain documented under Exports.
Start order and verification
Start each selected provider and its dependencies before FS-Robberies. For the FS-Money example, after installing FS-Money's own dependencies:
ensure FS-Lib
ensure FS-Money
ensure FS-RobberiesAdd your CAD resource before FS-Robberies when its API uses a resource. The inspected manifest also imports @rcore_cam/include/anim.lua and @ND_Core/init.lua unconditionally. If either import fails at startup, resolve the installed package's dependency/setup requirements before debugging your custom handler; selecting another money framework does not remove a manifest import.
- Enable
Config.Debugtemporarily and restart FS-Robberies after adapter/config changes. - Trigger one real robbery alarm in a test server. Check the server diagnostic, then the provider's actual incident list. Test register, ATM, safe, vault, and permitted/denied panic paths separately.
- Complete one robbery and compare the player's balance or item count before/after. Test cash and item modes independently, and verify that exactly one reward arrives.
- Test a stopped provider, unavailable player/account, and full inventory. Verify failure reporting without inventing a successful payout or automatically repeating an uncertain transaction.
- Restart the provider, then FS-Robberies, and repeat. Disable debug logging after testing.
No dispatch appears
Check Config.CAD.Enabled, exact selector spelling, the server console, provider start order, and whether the custom entry is still only a print statement. A diagnostic confirms handler execution, not CAD acceptance. Check provider response/error handling next.
Robbery completes without a reward
Check money enablement, the selected custom handler, ACE permissions, account initialization, and item-mode routing. An unknown inventory framework can skip the item grant without falling back to cash. A custom handler's return value cannot change that behavior.
Changes disappear after an update
Keep a backup or patch of edited adapter entries, compare the new signatures, and reapply only your integration changes. Do not replace the new file wholesale with an old version. Recheck escrow exclusions and repeat the verification steps after every update.