qb-storerobbery
qb-storerobbery handles small shop robberies: cash registers and the back-room safe. Registers
give markedbills and sometimes a sticky note carrying the safe code. Safes give more
markedbills plus a chance at rolex and goldbar.
It covers convenience stores only. Banks are in qb-bankrobbery, jewelry in qb-jewelery, residential in qb-houserobbery.
The manifest at the commit linked above declares version 1.5.0.
Dependencies and start order
The manifest declares no dependency entry and loads @qb-core/shared/locale.lua as a shared
script. The implementation goes further than the manifest admits. At this commit the server script
calls:
exports['qb-core']:GetCoreObject,GetPlayer, andGetShared('Items')exports['qb-inventory']:AddItemandRemoveItemTriggerClientEvent('qb-inventory:client:ItemBox', ...)TriggerClientEvent('qb-phone:client:addPoliceAlert', ...)Lang:t(...)from the core locale helper
So the real dependency chain is qb-core, qb-inventory, and
qb-phone for the alert. Start those first. If you run a different phone
resource, the alert call is a no-op and the police are never notified.
Configuration
config.lua is a shared script.
| Key | Default | Meaning |
|---|---|---|
Config.minEarn | 100 | Lower bound used to seed Config.RegisterEarnings |
Config.maxEarn | 450 | Upper bound for the same |
Config.RegisterEarnings | math.random(minEarn, maxEarn) | Evaluated once at load, not per robbery |
Config.MinimumStoreRobberyPolice | 2 | On-duty police required |
Config.resetTime | (60 * 1000) * 30 | Register cooldown in milliseconds, 30 minutes |
Config.tickInterval | 1000 | Server cooldown tick in milliseconds |
Config.stickyNoteChance | 10 | Percent chance a register also drops the safe code |
Config.Registers | table | Register locations with their safeKey and camId |
Config.Safes | table | Safe locations, their type, and camId |
The actual payout is not driven by Config.RegisterEarnings. server.lua carries its own local
cashA = 250 and cashB = 450 and uses those for the markedbills worth. Editing the config
values changes nothing about what players receive. Edit the locals in server.lua as well, or your
economy tuning will silently have no effect.
Safe codes are regenerated server side every 40 minutes. Keypad safes get a four digit code, dial safes get an array of angles that the client converts to numbers.
Server events and callbacks
| Handler | Kind | Arguments | Validation at this commit |
|---|---|---|---|
qb-storerobbery:server:takeMoney | net event | register, isDone | Distance to the register above 3.0, or an inconsistent robbed state, ends in DropPlayer |
qb-storerobbery:server:SafeReward | net event | safe | Distance above 3.0, or an already robbed safe, ends in DropPlayer |
qb-storerobbery:server:setRegisterStatus | net event | register | None |
qb-storerobbery:server:setSafeStatus | net event | safe | None |
qb-storerobbery:server:callCops | net event | type, safe, streetLabel, coords | None |
qb-storerobbery:server:removeAdvancedLockpick | net event | none | Player must exist |
qb-storerobbery:server:removeLockpick | net event | none | Player must exist |
qb-storerobbery:server:isCombinationRight | callback | safe | Returns the code |
qb-storerobbery:server:getPadlockCombination | callback | safe | Returns the code |
qb-storerobbery:server:getRegisterStatus | callback | none | Returns Config.Registers |
qb-storerobbery:server:getSafeStatus | callback | none | Returns Config.Safes |
Server-side validation pattern
The two reward events are worth reading as a template, because they show the minimum a money-granting
event should do. Simplified from server.lua at this commit:
RegisterNetEvent('qb-storerobbery:server:SafeReward', function(safe)
local src = source
local Player = exports['qb-core']:GetPlayer(src)
if not Player then return end
local playerCoords = GetEntityCoords(GetPlayerPed(src))
if #(playerCoords - Config.Safes[safe][1].xyz) > 3.0 or Config.Safes[safe].robbed then
return DropPlayer(src, 'Attempted exploit abuse')
end
-- only now grant the reward
end)Three properties make this safe enough to copy:
sourceis captured on the server, never taken from an argument.- Position is read from the server’s own view of the ped, not sent by the client.
- The reward amount comes from server-side constants, not from a client argument.
Contrast this with qb-pawnshop and qb-hotdogjob, where the price is accepted from the client.
The general form of these three properties, and the review questions that go with them, are in Safe server events and the server-authority checklist.
Events that are not validated
setRegisterStatus, setSafeStatus, and callCops are registered with RegisterNetEvent and
perform no source or state checks at this commit. A client can mark any register or safe robbed for
everyone, or trigger a police alert at arbitrary coordinates. Neither grants items, so the impact is
griefing rather than economy damage, but plan for it if you run a public server.
The two safe-code callbacks return the current code to whichever client asks. The code reaches the client in order for the minigame to check it locally, so the code is not a secret from a modified client.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Players are dropped with “Attempted exploit abuse” during normal play | The distance check uses 3.0 units against the configured register coordinate. A misplaced coordinate, or a shell-based interior, puts legitimate players outside it |
| Payout does not match your config | The payout uses cashA and cashB in server.lua, not Config.minEarn and Config.maxEarn |
| No police alert | qb-phone:client:addPoliceAlert has no listener because you run a different phone resource |
| Rewards do not arrive | markedbills, stickynote, rolex, or goldbar is missing from your shared items |
| Registers never reset | The cooldown thread runs on Config.tickInterval. If the resource was restarted, in-memory robbed state resets to the file defaults instead |
Proposed manual smoke test
Not yet executed. On staging:
- Rob one register and confirm
markedbillsarrives with a sensibleworth. - Repeat until the sticky note drops, then confirm the code opens the matching safe.
- Stand outside 3.0 units and fire the reward event manually, confirm the drop happens.
- Confirm the register resets after
Config.resetTime. - Confirm the police alert reaches an on-duty officer.