Configuration
Configuration is split across the configs/ directory:
configs/config.lua— global settings: locale, the contact ped, the black-market shop, statistics, and per-crime level thresholds.configs/<crime>.lua— one file per crime (20 of them), each holding that crime's cooldown, tools, rewards, police alert, and minigame.
Every value can also be edited live from the in-game admin panel — those edits are stored as overrides and take precedence over the file defaults.
Global Config (config.lua)
Locale
Locale = 'en', -- Loads locales/<Locale>.json. Falls back to 'en' if missing.Logging & Menus
ShopLogging = true, -- Log shop buy/sell + ped actions to the history ledger.
IconColors = false, -- Allow per-item icon colors in the menus.Contact Ped
The black-market contact is a ped that spawns at one random location from the list each restart.
Ped = {
Location = {
{x = 366.44, y = -1250.83, z = 31.51, w = 321.98},
-- Add more {x,y,z,w} tables; one is chosen at random each start.
},
Model = "a_m_m_mlcrisis_01",
Interaction = { Icon = "fas fa-circle", Distance = 3.0 },
Scenario = "WORLD_HUMAN_STAND_IMPATIENT", -- Ambient animation. Scenario list: https://pastebin.com/6mrYTdQv
}Black-Market Shop
The contact buys and sells. Each BuyItems / SellItems row is a self-contained card.
Shop = {
Enable = { Buying = true, Selling = true },
BuyItems = {
{
Product = "lockpick", -- inventory item name
Price = 50, -- cost per unit
Label = "Lockpick", -- display title
Description = "Essential tool for breaking into mailboxes...",
IconName = "fas fa-key", -- FontAwesome icon
IconColor = "#FFD700" -- optional; omit for no color (needs IconColors = true)
},
-- screwdriver, crowbar, gloves, ...
},
SellItems = {
{ Product = "stolen_goods", Price = 100, Label = "Stolen Goods", Description = "...", IconName = "fas fa-box", IconColor = "#e56969" },
-- jewelry, electronics, ...
},
}TIP
This is where you sell the tool items from the installation page. Add screwdriver, multitool, powersaw, etc. as BuyItems rows so players can purchase the gear each crime needs (the vanilla WEAPON_* tools don't need selling).
Statistics
Stats = { Enable = true }, -- Track per-crime attempts, successes, cash, items.Shared Package Item
Mailbox break-ins, parcel theft, and smash & grab all hand out one shared, openable package item. Configure the item key and what opening it yields here — every source behaves identically.
Package = {
Item = 'package', -- Inventory item the crimes give. Must exist in your inventory.
RewardChance = 80, -- % chance an opened package isn't empty.
LootCount = { min = 1, max = 3 }, -- Distinct items rolled per successful open.
Rewards = { -- One flat weighted pool (chance = relative weight).
{ item = 'lockpick', chance = 25, min = 1, max = 2 },
{ item = 'phone', chance = 20, min = 1, max = 1 },
{ item = 'goldchain', chance = 20, min = 1, max = 2 },
-- ...
},
}| Field | Meaning |
|---|---|
Item | The shared item key. Crimes award it; using it opens it |
RewardChance | % chance an opened package contains loot (else empty) |
LootCount | How many distinct items are rolled per open |
Rewards | Flat weighted pool — chance is a relative weight, sampled without replacement |
While a player holds a package they carry it on their shoulder (controls disabled); the carry visuals and the open progress bar / skill-check live in configs/parceltheft.lua. Opening grants loot only — the crimes themselves award the XP for the rob/pickup.
Level Thresholds
Every crime has its own 3-level curve. xpRequired is the cumulative XP needed to reach that level. Higher levels unlock the richer reward tiers defined in each crime's config.
Levels = {
mailbox = {
[1] = { xpRequired = 0 }, -- starting level
[2] = { xpRequired = 150 },
[3] = { xpRequired = 300 },
},
pickpocket = {
[1] = { xpRequired = 0 },
[2] = { xpRequired = 100 },
[3] = { xpRequired = 200 },
},
-- ...one block per crime (payphone, parkingmeter, robaped, shoplift,
-- parceltheft, vending, catalytic, smashgrab, tiretheft, acstrip,
-- newsrack, atmskimmer, tireslash, brakecut, wheelloose, fuelsabotage,
-- signrob, brickgas)
}Per-Crime Config Anatomy
All crime configs share a common shape. Below is the prop-crime layout (mailbox, payphone, parking meter, news rack, vending, shoplift) — the others add a few fields on top, covered after.
return {
Cooldown = 2, -- Minutes before the same target is robbable again.
Items = { 'WEAPON_HAMMER' }, -- Tools — ANY ONE is enough. (see installation tool mapping)
Time = 15, -- Progress-bar duration in seconds.
BaseXP = 15, -- XP for a successful attempt.
GiveXPForItems = true, -- Add each rolled item's own xp on top of BaseXP.
Models = { -- World props the target option attaches to.
'prop_letterbox_01', 'prop_letterbox_02', 'prop_letterbox_03', 'prop_letterbox_04',
},
Logging = true, -- Write success/items/cash to the history ledger.
Minigame = { ... }, -- Skill check (see Minigames below).
PoliceAlert = { ... }, -- Dispatch hook (see Police Alerts below).
Rewards = { ... }, -- Weighted loot, keyed by player level.
}Rewards & Weighted Chance
Reward tables are keyed by player level ([1], [2], [3]). Each row's chance is a weight, not a percentage — think of it as the number of raffle tickets in the hat.
Rewards = {
[1] = { -- Level 1
{ item = "cash", chance = 15, min = 3, max = 8, xp = 0 },
{ item = "package", chance = 30, min = 1, max = 1, xp = 5 },
{ item = "metalscrap", chance = 25, min = 1, max = 2, xp = 2 },
},
[2] = { ... }, -- richer pool, unlocked at level 2
[3] = { ... },
}| Field | Meaning |
|---|---|
item | Inventory item name (cash pays out money) |
chance | Relative weight — higher = more likely within its level bucket |
min / max | Quantity range awarded |
xp | Bonus XP if this item is rolled (added when GiveXPForItems = true) |
Openable Packages
Mailbox break-ins and smash & grab include a package in their loot tables, and parcel theft hands one out on pickup. The package is a single shared, openable item — its loot pool lives in Shared Package Item above. The per-crime configs just list package as a reward; there's no per-crime package table.
Person Crimes (pickpocket, armed robbery)
Pickpocketing and armed ped robbery roll loot by map zone tier instead of a flat per-level table:
Cooldown = { Enable = false, Time = 30 }, -- Personal cooldown (seconds) between attempts.
Chance = 80, -- Overall % the target has anything on them.
Tiers = {
low = { cash = { min = 8, max = 25 }, [1] = { ... }, [2] = { ... }, [3] = { ... } },
medium = { cash = { min = 20, max = 50 }, ... },
high = { cash = { min = 40, max = 100 }, ... },
},
ZoneTiers = {
high = { 'AIRP', 'PBOX', 'DOWNT', ... }, -- GetNameOfZone() short codes
medium = { 'CHIL', 'ROCKF', 'BEACH', ... },
-- anything not listed falls through to the `low` tier
},Armed robbery (robaped.lua) adds a few control fields:
| Field | Default | Purpose |
|---|---|---|
RunawayChance | 10 | Per-second % a held-up ped breaks free if you stop aiming |
Key | 38 | Control input that commits the rob (38 = E) |
IgnoreWeapons | table | Weapons/groups that won't trigger the hold-up (unarmed, fire extinguisher, …) |
IgnoreModels | table | Ped models the option won't attach to (cops, medics, gang bosses, military) |
Vehicle Crimes
Vehicle crimes (catalytic, tiretheft, wheelloose, tireslash, brakecut, fuelsabotage, brickgas, smashgrab) follow the same skeleton with a few extras:
| Field | Purpose |
|---|---|
Cooldown | Per-plate or per-coords cooldown (minutes) before the same target can be hit again |
IgnoreClasses | Vehicle classes the option refuses (bikes, boats, aircraft, emergency) |
Locations / VehicleModels | For spawn-based crimes (e.g. smash & grab parks cars at fixed points) |
| Multi-stage times | Some crimes have a scope/setup stage and a commit stage with separate durations |
ATM skimming (atmskimmer.lua) is a two-phase, delayed-payoff crime:
HarvestDelay = 30, -- Minutes between installing a skimmer and the earliest harvest.
MaxPerPlayer = 5, -- Max simultaneously-installed skimmers per character.
InstallItem = 'skimmer', -- Consumed at install.
HarvestItem = 'card_data', -- Received at harvest.Parcel theft (parceltheft.lua) defines porch spawn Locations — append a row and it's picked up on next restart:
{ coords = vector3(123.45, 678.90, 12.34), heading = 90.0, distance = 25.0, prop = 'hei_prop_heist_box' },Minigames
TIP
For the complete reference — every minigame's parameters with defaults, how to swap them per crime, and using external minigame resources — see the dedicated Minigames page.
Every crime runs a skill-check before it commits. The minigame lives in a Minigame block in the crime's config:
Minigame = {
Enable = true, -- false = skip the minigame (auto-success).
Start = function()
-- Calls a built-in minigame and returns its pass/fail boolean.
return require('client.minigame').mash({
fillPerTap = 0.08, -- bar gained per E tap (0..1)
decayPerSec = 0.4, -- bar lost per second (0..1)
timeLimitSec = 6, -- seconds to fill it; 0 = no limit
}).success
end,
}Start can be any function that returns true/false — swap the built-in for a different one, change its tuning, or drop in ps-ui / lib.skillCheck / your own NUI minigame without touching the crime's code.
Built-In Library
Call any of these via require('client.minigame').<name>({ ...params }):
| Minigame | What it is | Key params |
|---|---|---|
pickpocket | Moving marker over item "safe zones"; grab each | greenArcDeg, rotationSpeedDegSec, speedUpMultiplier, mysteryChance, timeLimitSec |
lockpick | Rotating ring; press as the orb crosses each node | nodeCount, hitWindowDeg, rotationSpeedDegSec, speedUpMultiplier, timeLimitSec |
lockpickbar | Bar variant of lockpick | same as lockpick |
holdsteady | Hold E to keep a marker inside a drifting band | bandSize, holdDurationSec, gravity, thrust, timeLimitSec |
mash | Mash E to fill a bar before it decays | fillPerTap, decayPerSec, timeLimitSec |
stealth | Press E only while the NPC looks away | steps, safeDurationSec, watchDurationSec, jitterSec, timeLimitSec |
dial | Press E at each notch of a sweeping pointer | notches, hitWindowDeg, rotationSpeedDegSec, timeLimitSec |
wires | Cut the correct coloured wire(s) | wireCount, cutsNeeded, timeLimitSec |
sequence | Growing Simon — repeat the lengthening pattern | padCount, startLength, maxLength, growBy, timeLimitSec |
reaction | Hit key prompts in a row, window tightening | rounds, startWindowSec, windowShrink |
rhythm | Hit falling notes on the hit line (A S D F) | lanes, noteCount, fallSec, maxMisses, hitWindow |
tracking | Keep the cursor on a wandering target | catchRadius, holdDurationSec, targetSpeed, timeLimitSec |
tumbler | Set each pin as its marker crosses the sweet spot | pins, bandSize, speedSec, timeLimitSec |
trace | Trace a path without straying off it | tolerance, segments, wiggle, timeLimitSec |
code | Type the shown code in time | length, timeLimitSec |
safedial | Rotate a safe dial onto each mark in sequence | numbers, steps, toleranceDeg, timeLimitSec |
tuning | Drag sliders onto their target marks | sliders, tolerance, timeLimitSec |
spot | Find the matching icon in a grid | gridCount, rounds, timeLimitSec |
whack | Click targets before they expire | hits, maxMisses, targetLifeSec, spawnEverySec |
gauge | Hold to fill, release inside the green band | rounds, bandSize, fillSpeed, timeLimitSec |
pipes | Rotate pipe tiles to connect the flow | cols, rows, timeLimitSec |
Test any minigame
With the command.pettycrime_admin ACE, run /testminigame <name> to preview any minigame, /testminigame all to cycle through every one, or /testminigame with no argument to list them.
Police Alerts
Every crime has a PoliceAlert block. The default Send is a console print — replace it with your dispatch system.
PoliceAlert = {
Enable = true,
Chance = 25, -- 0-100 chance an alert fires on success.
Send = function()
local data = exports['cd_dispatch']:GetPlayerInfo()
TriggerServerEvent('cd_dispatch:AddNotification', {
job_table = { 'police' },
coords = data.coords,
title = '10-15 - Mail Theft',
message = ('A %s tampering with a mailbox at %s'):format(data.sex, data.street),
-- ...your dispatch fields...
})
end,
}Localisation
locales/en.json ships by default. To add a language:
- Copy
locales/en.jsontolocales/<code>.json(e.g.de.json). - Translate the values, keep the keys.
- Set
Locale = '<code>'inconfigs/config.lua.
Missing keys fall back to the raw key name in the UI, so untranslated entries are immediately visible.
Admin Panel
Players with the command.pettycrime_admin ACE can run /pettycrimeadmin (alias /pcadmin) to open the React NUI admin panel.
| Tab | What it does |
|---|---|
| Players | Paged, searchable list of every record. Inspect per-crime XP/level/attempts/cash/items; adjust XP inline; reset a single crime or wipe a record |
| History | Append-only ledger of every shop buy/sell, crime success (one row per item awarded), and admin override |
| Configuration | Every value in configs/*.lua as an editable field. Save persists an override; reset reverts to the file default |
How overrides work
Saved edits are stored in sd_pettycrime_admin as override rows. At boot, the admin module patches the live config tables before the crime modules load, so every consumer sees the merged values. Fields you never touch keep using their config defaults; reset removes the override and restores the boot-time default.
Grant the ACE in server.cfg:
add_ace group.admin command.pettycrime_admin allowFull Config Files
Prefer reading the raw files? Every config is mirrored under Full Config Files — the global config.lua plus one page per crime.
