OpsLab Books
Realplay Scripts /RPS BossMenu Manual SQL Installation
Help Sign in Sign up
Installation

rps_lib — Full Integration Guide

A complete reference for calling rps_lib from another resource: every exported function, every bridge function on both sides, real signatures pulled directly from source, and worked examples for each module.

local RPS_Lib = exports['rps_lib']:GetLibObject()

That one line is the entire integration surface. Everything below explains what it gives you and how to use each piece correctly.


Table of contents

  1. How rps_lib exposes itself
  2. Setup in your resource
  3. Detected module fields
  4. Framework & player data (client)
  5. Framework & player data (server)
  6. Permissions (server)
  7. Jobs, gangs & economy (server)
  8. Vehicle database & plates (server)
  9. Inventory (client)
  10. Inventory (server)
  11. Vehicle keys (client)
  12. Vehicle keys (server)
  13. Fuel (client)
  14. Garage (client)
  15. Garage (server)
  16. Target (client)
  17. Notify, progress, status (client)
  18. Stress — two APIs, read this carefully
  19. 3D text & UI helpers (client)
  20. Banking (server)
  21. Shared utilities (RPS table)
  22. Config reference
  23. Full worked example
  24. Common mistakes

1. How rps_lib exposes itself

rps_lib builds one table per side during its own startup — framework/client/init.lua builds the client's _Lib, and framework/server/init.lua builds the server's _Lib (two separate tables, same variable name).

As of this version, framework-specific code (ESX/QB/QBX) no longer lives inline inside those two loader files — each framework has its own folder:

framework/
├── esx/   client.lua   server.lua   events.lua
├── qb/    client.lua   server.lua   events.lua
├── qbx/   client.lua   server.lua   events.lua
├── client/init.lua   ← loader: detects framework, merges in the right folder
└── server/init.lua   ← loader: same, server side

Each framework/<name>/client.lua (and server.lua) sets a global slot when it loads — e.g. _G.RPS_Framework_ESX_Client — containing a table of that framework's implementations of GetPlayerData, GetJob, GetIdentifier, etc. FXServer has no conditional script loading, so all three framework folders load on every server, regardless of which framework is actually running — but only the active one's functions do anything meaningful (the other two's core-object lookups simply find nothing and their functions are never merged in). The loader (framework/client/init.lua / framework/server/init.lua) reads whichever slot matches the detected framework and merges only that one into _Lib.

Every other integration file (integration/client/inventory.lua, integration/server/stress.lua, etc.) then attaches its own functions directly onto that same _Lib table, so by the time rps_lib finishes loading, _Lib contains every detected-module field and every bridge function from every file — framework-specific and backend-agnostic alike.

You never need to look inside the framework/esx/qb/qbx folders to use rps_lib — this is purely an internal organization detail. Every function name and signature documented in this guide is identical regardless of which framework folder actually implements it underneath.

Three exports give you access to the merged result, declared identically on both sides:

exports {
    'GetLibObject',  -- returns the whole _Lib table
    'GetFramework',  -- returns _Lib.Framework as a string
    'GetVersion',    -- returns the RPS.Version string, e.g. '2.0.0'
}

server_exports {
    'GetLibObject',
    'GetFramework',
    'GetVersion',
}
-- Any of these work, client or server:
local RPS_Lib = exports['rps_lib']:GetLibObject()
local fw      = exports['rps_lib']:GetFramework()   -- 'esx' | 'qb' | 'qbx' | 'unknown'
local ver     = exports['rps_lib']:GetVersion()     -- '2.0.0'

In almost every case you want GetLibObject() — it's the only one that gives you the actual bridge functions.


2. Setup in your resource

-- your_script/fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

dependency 'rps_lib'

client_script 'client.lua'
server_script 'server.lua'
# server.cfg
ensure rps_lib
ensure your_script

dependency 'rps_lib' is a hard guarantee enforced by FXServer's resource graph — rps_lib will have finished its own startup (both framework/*/init.lua files run synchronously at the top level, no CreateThread delay for the core fields) before your resource's first tick. With that declared, this is always safe:

local RPS_Lib = exports['rps_lib']:GetLibObject()
print(RPS_Lib.Framework) -- never nil

If you can't declare the dependency for some reason, guard it instead:

local RPS_Lib

CreateThread(function()
    while GetResourceState('rps_lib') ~= 'started' do
        Wait(100)
    end
    RPS_Lib = exports['rps_lib']:GetLibObject()
end)

Store RPS_Lib once in a local at the top of each file — don't call GetLibObject() repeatedly. It's cheap (just returns a table reference), but there's no reason to call it more than once per file.


3. Detected module fields

Both _Lib tables (client and server) carry these fields, set once at startup by the Detect* functions in shared/core.lua:

Field Possible values
_Lib.Framework 'esx' | 'qb' | 'qbx' | 'unknown'
_Lib.Inventory 'ox_inventory', 'qb-inventory', 'qs-inventory', 'ps-inventory', 'lj-inventory', 'codem-inventory', 'origen_inventory', 'tgiann-inventory', 'core_inventory', 'jaksam_inventory', 'cheeza_inventory', 'ak47_inventory', 'ak47_qb_inventory', 'qb-inventory-old'
_Lib.Target 'ak47_target', 'ox_target', 'qb-target', 'qtarget', 'rps_target'
_Lib.Fuel 'LegacyFuel', 'ox_fuel', 'ps-fuel', 'lc_fuel', 'cdn-fuel', 'myFuel'
_Lib.VehicleKey 'ak47_vehiclekeys', 'ak47_qb_vehiclekeys', 'tgiann-hotwire', 'wasabi_carlock', 'qs-vehiclekeys', 'cd_garage', 'qb-vehiclekeys', 'qbx_vehiclekeys', 'vehiclekeys', 'rps_vehiclekeys', 'none'
_Lib.Garage 'rps_garage', 'jg-advancedgarages', 'cd_garage', 'qb-garages', 'none'
_Lib.Banking 'Renewed-Banking', 'okokBanking', 'qb-banking', 'none'
_Lib.Notify 'ox', 'esx', 'qb', 'qbx', 'okok', 'pnotify', 'native', 'custom'
_Lib.Progressbar 'ox', 'esx', 'qb', 'custom'
_Lib.Stress 'jg-stress-addon', 'ps-stress', 'qb', 'ox', 'none'

⚠️ Important inconsistency to know about: most bridge functions read the field (_Lib.Inventory, _Lib.VehicleKey, etc.), but a few older functions read Config.X directly instead (Config.Notify, Config.Progressbar, Config.FuelScript, Config.Stress). After auto-detection runs once at startup, both end up holding the same resolved value, so it doesn't matter which one you read for your own logic — but see §18 for the one place this actually causes a functional split, not just a naming inconsistency.

You don't need to branch on any of these yourself — call the generic bridge function and rps_lib routes internally. They're exposed mainly so you can show different UI text or log which backend is active.


4. Framework & player data (client)

All functions below are on the client _Lib. Most are implemented per- framework in framework/<esx|qb|qbx>/client.lua and merged in by framework/client/init.lua's loader (see §1); GetTargetMetaValue, AddStress, and RemoveStress are framework- agnostic and defined directly in the loader itself.

RPS_Lib.GetCoreConfig(key)

--- @param key string|nil  (Optional, ESX only) specific config key
--- @return table|any
Returns the active framework's core config table. Always returns {} on QBX — there's no GetConfig-style export and QBX's config lives in files that aren't exposed at runtime. On ESX, returns ESX.GetConfig() (or just one key if key is passed); on QB, returns QBCore.Config.

RPS_Lib.GetPlayerData()

--- @return table
Returns the raw framework player-data table for the local player — ESX GetPlayerData(), QBCore.Functions.GetPlayerData(), or exports.qbx_core:GetPlayerData() depending on framework. Shape differs per framework; this is the raw table, not normalized.

RPS_Lib.GetJob()

--- @return table { name, label, payment, isboss, grade = { name, level } }
Returns a normalized job table — same shape regardless of framework. Defaults to { name = 'unemployed', label = 'Unemployed', payment = 0, isboss = false, grade = { name = 'none', level = 0 } } if no job data is found.

local job = RPS_Lib.GetJob()
if job.name == 'police' and job.isboss then
    -- boss menu access
end

RPS_Lib.GetIdentifier()

--- @return string
Returns the local player's unique identifier — identifier on ESX, citizenid on QB/QBX.

RPS_Lib.GetCharacterName()

--- @return string
Returns "Firstname Lastname" for the local player, pulled from firstName/lastName (ESX) or charinfo.firstname/charinfo.lastname (QB/QBX).

RPS_Lib.GetTargetMetaValue(targetServerId, metaKey)

--- @param targetServerId number
--- @param metaKey string
--- @return any
Fetches a metadata value from another player via a server round-trip (rps_lib:getTargetMeta callback). Blocks for up to 1 second waiting for the response, then returns nil if nothing came back. Used internally by IsDead(target) / IsLastStand(target) in the status module — see §17.

RPS_Lib.AddStress(amount) / RPS_Lib.RemoveStress(amount)

--- @param amount number
⚠️ See §18 — these are a separate, older stress API from the one in integration/client/stress.lua. They read Config.Stress (not _Lib.Stress) and fire generic rps_lib:addStress/removeStress events rather than calling jg-stress-addon exports directly.


5. Framework & player data (server)

All functions below are on the server _Lib. Most are implemented per- framework in framework/<esx|qb|qbx>/server.lua and merged in by framework/server/init.lua's loader (see §1); framework-agnostic helpers (GetLicense, GetIdentifierByType, GetSourceFromIdentifier, HasPermission, GeneratePlate, etc.) are defined directly in the loader itself. Nearly everything here takes a source (player server ID) as its first argument.

RPS_Lib.GetPlayer(source)

--- @param source number
--- @return table|nil
Returns the raw framework player object — ESX.GetPlayerFromId(source), QBCore.Functions.GetPlayer(source), or exports.qbx_core:GetPlayer(source). Returns nil if the player doesn't exist. Every other server function in this section calls this internally — if it returns nil, the functions below it return their documented empty/zero default rather than erroring.

RPS_Lib.GetIdentifier(source)

--- @param source number
--- @return string
Returns identifier (ESX) or PlayerData.citizenid (QB/QBX). Empty string if the player isn't found.

RPS_Lib.GetName(source)

--- @param source number
--- @return string
Returns "Firstname Lastname", same normalization as the client's GetCharacterName.

RPS_Lib.GetPhoneNumber(source)

--- @param source number
--- @return string
ESX: p.get('phone_number'). QB/QBX: PlayerData.charinfo.phone.

RPS_Lib.GetLicense(source)

--- @param source number
--- @return string
Shorthand for GetIdentifierByType(source, 'license').

RPS_Lib.GetIdentifierByType(source, idtype)

--- @param source number
--- @param idtype string  e.g. 'steam', 'license', 'discord', 'fivem'
--- @return string|nil
Scans the player's native identifiers (GetPlayerIdentifier) for one matching the given prefix. This does not go through the framework at all — works regardless of _Lib.Framework.

RPS_Lib.GetSourceFromIdentifier(identifier)

--- @param identifier string
--- @return number|nil
Linear-scans all online players (GetPlayers()) comparing GetIdentifier(src) against the given string. O(n) on player count — fine for admin commands, avoid calling in a hot loop.

RPS_Lib.GetPlayerFromIdentifier(identifier)

--- @param identifier string
--- @return table|nil
GetSourceFromIdentifier + GetPlayer combined.

RPS_Lib.GetSource(Player)

--- @param Player table  a framework player object (from GetPlayer)
--- @return number
The inverse of GetPlayer — extracts the server ID back out of a player object you already have.

RPS_Lib.GetNameFromIdentifier(identifier)

--- @param identifier string
--- @return string
Database lookup (MySQL.scalar.await) for an offline player's name — queries the users table (firstname/lastname columns). Works regardless of framework since it's a raw SQL query, but the table/column names assume an ESX-style or QB-compatible users schema.

Metadata

--- @param source number
--- @param key string
--- @return any
RPS_Lib.GetMetaData(source, key)

--- @param source number
--- @param key string
--- @param value any
RPS_Lib.SetMetaData(source, key, value)
ESX: p.get(key) / p.set(key, value). QB/QBX: PlayerData.metadata[key] / p.Functions.SetMetaData(key, value).

Offline metadata

--- @param identifier string
--- @param key string
--- @return any
RPS_Lib.GetOfflineMetaData(identifier, key)

--- @param identifier string
--- @param key string
--- @param value any
RPS_Lib.SetOfflineMetaData(identifier, key, value)
Direct database reads/writes for players who aren't currently online. ESX queries users.metadata; QB/QBX queries players.metadata. Both columns are expected to hold JSON.


6. Permissions (server)

RPS_Lib.IsAdmin(source)

--- @param source number
--- @return boolean
Shorthand for IsPlayerAceAllowed(source, 'command'). Framework-agnostic.

RPS_Lib.HasGroupPermission(source, group)

--- @param source number
--- @param group string
--- @return boolean
ESX: checks p.getGroup() == group. QB/QBX: checks IsPlayerAceAllowed(source, 'group.' .. group) instead — note this means on QB/QBX you need the matching add_ace group.<group> command allow (or principal-based group) set up in your server config, not a job/gang check.

RPS_Lib.HasPermission(source, Admin, notify)

--- @param source number
--- @param Admin table  { WithAce, WithLicense, WithIdentifier, WithGroup }
--- @param notify boolean|nil  log which check passed via RPS.Info
--- @return boolean
The main permission entry point — tries up to four independent checks in this exact order, returning true on the first match:

  1. WithAce = trueIsPlayerAceAllowed(source, 'command')
  2. WithLicense = { ['license:abc...'] = true, ... } → checks GetLicense(source) against the table keys
  3. WithIdentifier = { ['steam:abc...'] = true, ... } → checks GetIdentifier(source) against the table keys
  4. WithGroup = { admin = true, police = true, ... } → checks HasGroupPermission(source, group) for each key
local allowed = RPS_Lib.HasPermission(source, {
    WithAce   = true,
    WithGroup = { admin = true, god = true },
}, true) -- true = print which check passed

if not allowed then return end

Any of the four keys can be omitted — only the ones you provide are checked.


7. Jobs, gangs & economy (server)

RPS_Lib.GetJob(source) / RPS_Lib.SetJob(source, jobName, grade)

--- @param source number
--- @return table { name, label, payment, isboss, grade = { name, level } }
RPS_Lib.GetJob(source)

--- @param source number
--- @param jobName string
--- @param grade number
RPS_Lib.SetJob(source, jobName, grade)
Same normalized shape as the client's GetJob(). Defaults to the unemployed table if the player isn't found.

RPS_Lib.GetGang(source) / RPS_Lib.SetGang(source, gangName, grade)

--- @param source number
--- @return table { name, label, isboss, grade = { name, level } }
RPS_Lib.GetGang(source)

--- @param source number
--- @param gangName string
--- @param grade number
RPS_Lib.SetGang(source, gangName, grade)
⚠️ ESX has no native gang system. GetGang always returns the neutral { name = 'none', label = 'None', isboss = false, grade = {...} } table on ESX, and SetGang silently does nothing on ESX (the function body checks if _Lib.Framework ~= 'esx' then ... end).

RPS_Lib.GetJobs()

--- @return table
Returns the full jobs/grades definition table. ESX: ESX.GetJobs(). QB: QBCore.Shared.Jobs. QBX: exports.qbx_core:GetJobs().

Money

--- @param source number
--- @param account string  'cash' | 'money' | 'bank' | 'black_money' | 'crypto'
---                        ('cash' and 'money' are interchangeable — both
---                        map to whichever name the active framework uses)
--- @return number
RPS_Lib.GetMoney(source, account)

--- @param source number
--- @param account string
--- @param amount number
RPS_Lib.AddMoney(source, account, amount)
RPS_Lib.RemoveMoney(source, account, amount)
ESX's cash account is natively named 'money'; QB/QBX's is natively named 'cash'. These functions normalize in the correct direction per framework — pass either 'cash' or 'money' and it resolves to whichever name the active framework actually expects (p.getAccount('money')/addAccountMoney/removeAccountMoney on ESX, p.Functions.GetMoney('cash')/AddMoney/RemoveMoney on QB/QBX). Any other account name ('bank', 'black_money', 'crypto') passes through unchanged.

⚠️ Earlier versions of rps_lib normalized 'money''cash' unconditionally before checking the framework, which silently broke cash handling on ESX (there is no 'cash' account in ESX — calling p.getAccount('cash') returns nil). Fixed — if you're on an older copy of this file, update framework/server/init.lua.

Offline money

--- @param identifier string
--- @param account string  'cash' | 'money' | 'bank'
--- @return number
RPS_Lib.GetOfflineMoney(identifier, account)

--- @param identifier string
--- @param account string
--- @param amount number
RPS_Lib.AddOfflineMoney(identifier, account, amount)
RPS_Lib.RemoveOfflineMoney(identifier, account, amount)  -- just calls AddOfflineMoney with -amount
Direct DB reads/writes for offline players. ESX queries individual columns on users — restricted to money and bank only (anything else logs an RPS.Error and returns 0/no-ops, rather than building a query from an unchecked column name). QB/QBX queries the JSON money column on players, so any key works there ('cash', 'bank', 'crypto', custom keys) since it's just a JSON object lookup, not a SQL column.

RPS_Lib.GetAllOfflinePlayers()

--- @return table
Returns every row from the players table. ESX: SELECT identifier, firstname, lastname, job, job_grade, money, bank FROM users. QB/QBX: SELECT citizenid, charinfo, job, money FROM players. No pagination — be mindful on large player tables.


8. Vehicle database & plates (server)

RPS_Lib.IsVehicleOwner(source, plate)

--- @param source number
--- @param plate string
--- @return boolean
Checks the owned_vehicles (ESX) or player_vehicles (QB/QBX) table for a row where the owner identifier matches GetIdentifier(source).

RPS_Lib.GetVehicleOwner(plate)

--- @param plate string
--- @return string|nil
Returns the owner's identifier (not server ID — use GetSourceFromIdentifier if you need the live source).

RPS_Lib.GeneratePlate(format, prefix)

--- @param format string|nil  Default 'AAAA 11A' — 'A' = random letter, '1' = random digit, anything else is literal
--- @param prefix string|nil
--- @return string  uppercased, truncated to 8 characters
RPS_Lib.GeneratePlate()                  -- e.g. "XQRT 42B"
RPS_Lib.GeneratePlate('11-AAA-11')       -- e.g. "84-QRT-91"
RPS_Lib.GeneratePlate('AAA1111', 'PD')   -- prefix is prepended before the pattern is applied
⚠️ No uniqueness check against the database — this just generates a random string in the given pattern. If you need a guaranteed-unique plate, call this in a loop checking GetVehicleOwner(plate) == nil (or your own existence query) until it returns one that isn't taken.

RPS_Lib.GiveVehicle(source, model)

--- @param source number
--- @param model string|number  model name or hash
Inserts a new row into owned_vehicles (ESX) or player_vehicles (QB/QBX) for the player, with a freshly generated plate. Does not spawn the vehicle — this only creates the database/garage entry.


9. Inventory (client)

Supports: ox_inventory, qs-inventory, qb-inventory, qb-inventory-old, ps-inventory, lj-inventory, codem-inventory, origen_inventory, tgiann-inventory, core_inventory, jaksam_inventory, cheeza_inventory, ak47_inventory, ak47_qb_inventory.

Core functions (work on every supported inventory)

--- Populates _Lib.Items / _Lib.ItemsByHash. Called automatically on
--- startup — you don't need to call this yourself.
--- @return nil
RPS_Lib.FetchItems()

--- @param hash number  result of GetHashKey(itemName)
--- @return string|nil
RPS_Lib.GetWeaponNameFromHash(hash)

--- @return string|nil  NUI base path for item images
RPS_Lib.GetInventoryImageLink()

--- @param name string
--- @param format string|nil  defaults to '.png'
--- @return string|nil
RPS_Lib.GetItemImageLink(name, format)

--- @param identifier string  unique stash ID
--- @param name string        display label
--- @param weight number      max weight (kg)
--- @param slots number
RPS_Lib.OpenStash(identifier, name, weight, slots)

--- @param targetServerId number  search/rob another player's inventory
RPS_Lib.OpenSearchInventory(targetServerId)

--- Closes whatever inventory UI is currently open
RPS_Lib.CloseInventory()

--- @param state boolean  lock/unlock the inventory during an action
RPS_Lib.SetInventoryBusy(state)

--- @param item string
--- @param amount number  durability reduction — ak47_inventory only, no-op elsewhere
RPS_Lib.RemoveItemQuality(item, amount)

--- Registers item-remove net events for the active inventory/framework.
--- Called automatically on startup.
RPS_Lib.RegisterInventoryEvents()

ox_inventory-exclusive functions

These all check _Lib.Inventory == 'ox_inventory' internally and safely return an empty/zero default on every other inventory — they won't error if called on a server running qb-inventory, they just won't do anything useful.

--- @param itemName string
--- @param metadata table|nil
--- @param strict boolean|nil
--- @return number
RPS_Lib.GetItemCount(itemName, metadata, strict)

--- @return table  full player inventory contents
RPS_Lib.GetPlayerItems()

--- @return number
RPS_Lib.GetPlayerWeight()

--- @return number
RPS_Lib.GetPlayerMaxWeight()

--- @param search string  'slots' | 'count'
--- @param item string|table
--- @param metadata table|string|nil
--- @return table|number
RPS_Lib.Search(search, item, metadata)

--- @param itemName string
--- @param metadata table|nil
--- @param strict boolean|nil
--- @return number|nil
RPS_Lib.GetSlotIdWithItem(itemName, metadata, strict)

--- @return table|nil  all matching slot ids
RPS_Lib.GetSlotIdsWithItem(itemName, metadata, strict)

--- @return table|nil  single matching slot data
RPS_Lib.GetSlotWithItem(itemName, metadata, strict)

--- @return table|nil  all matching slot data
RPS_Lib.GetSlotsWithItem(itemName, metadata, strict)

--- @return table|nil
RPS_Lib.GetCurrentWeapon()

--- @param data table  item data from an item-use callback
--- @param cb function|nil
RPS_Lib.UseItem(data, cb)

--- @param slot number
RPS_Lib.UseSlot(slot)

--- @param id string|number
--- @param owner string|number|nil
RPS_Lib.SetStashTarget(id, owner)

--- @param metadata string|table
--- @param value string|nil
RPS_Lib.DisplayMetadata(metadata, value)

--- @param serverId number  give an item to another player
--- @param slotId number
--- @param count number|nil
RPS_Lib.GiveItemToTarget(serverId, slotId, count)

--- @param state boolean  enable weapon wheel while disabling inventory weapons
RPS_Lib.WeaponWheel(state)

--- @param value boolean
RPS_Lib.SuppressItemNotifications(value)

--- @return boolean
RPS_Lib.IsInventoryBusy()

--- @return boolean
RPS_Lib.IsInventoryOpen()
-- Example: check before letting a player start an animation that needs both hands free
if RPS_Lib.GetCurrentWeapon() then
    RPS_Lib.Notify("Holster your weapon first.", "error")
    return
end

10. Inventory (server)

Items

--- Populates _Lib.Items / _Lib.ItemsByHash. Runs automatically on startup
--- via the inventory auto-detect thread.
RPS_Lib.FetchItems()

--- @return table  master item definitions for the active inventory
RPS_Lib.GetItems()

--- @param item string
--- @return string  label, or the item name itself if not found
RPS_Lib.GetItemLabel(item)

--- @param hash number
--- @return string|nil
RPS_Lib.GetWeaponNameFromHash(hash)

Add / remove / query items

These four work across every supported inventory (the function bodies branch per-system internally):

--- @param inventoryId number|string  player source OR stash identifier
--- @param item string
--- @param amount number
--- @param slot number|nil
--- @param meta table|nil
--- @return boolean
RPS_Lib.AddItem(inventoryId, item, amount, slot, meta)
RPS_Lib.RemoveItem(inventoryId, item, amount, slot, meta)

--- @param inventoryId number|string
--- @return table  every item currently in that inventory
RPS_Lib.GetInventoryItems(inventoryId)

--- @param inventoryId number|string
--- @param item string
--- @param metadata table|nil
--- @return number  count of that item
RPS_Lib.GetInventoryItem(inventoryId, item, metadata)

--- @param inventoryId number|string
--- @param item string
--- @param amount number
--- @return boolean
RPS_Lib.HasEnoughItem(inventoryId, item, amount)

--- @param inventoryId number|string
--- @param item string
--- @param amount number
--- @param metadata table|nil
--- @return boolean  whether the inventory has room for this item+amount
RPS_Lib.CanCarryItem(inventoryId, item, amount, metadata)
RegisterNetEvent('your_script:buyItem', function(item, amount)
    local src = source
    if not RPS_Lib.CanCarryItem(src, item, amount) then
        RPS_Lib.Notify(src, "You don't have enough space.", "error")
        return
    end
    RPS_Lib.AddItem(src, item, amount)
end)

ox_inventory-exclusive server functions

Same pattern as the client section — these check _Lib.Inventory == 'ox_inventory' and safely no-op (returning {} / 0 / false / doing nothing) on every other inventory.

--- @return number
RPS_Lib.CanCarryAmount(inventoryId, item)

--- @return boolean, number  can carry, free weight
RPS_Lib.CanCarryWeight(inventoryId, weight)

RPS_Lib.SetMaxWeight(inventoryId, maxWeight)
RPS_Lib.SetSlotCount(inventoryId, slots)

--- @return table|nil
RPS_Lib.GetSlot(inventoryId, slot)

--- @return number|nil
RPS_Lib.GetSlotForItem(inventoryId, itemName, metadata)
RPS_Lib.GetSlotIdWithItem(inventoryId, itemName, metadata, strict)

--- @return table|nil
RPS_Lib.GetSlotIdsWithItem(inventoryId, itemName, metadata, strict)
RPS_Lib.GetSlotWithItem(inventoryId, itemName, metadata, strict)
RPS_Lib.GetSlotsWithItem(inventoryId, itemName, metadata, strict)

--- @return number|nil
RPS_Lib.GetEmptySlot(inventoryId)

--- @return number, number, number  used slots, total count, remaining
RPS_Lib.GetItemSlots(inventoryId, item, metadata)

--- @return table|number
RPS_Lib.Search(inventoryId, search, item, metadata)

--- @return table|nil  full inventory object
RPS_Lib.GetInventory(inventoryId, owner)

--- @param id string|number
--- @param label string
--- @param slots number
--- @param maxWeight number
--- @param owner string|boolean|nil
--- @param groups table|nil
--- @param coords vector3|nil
RPS_Lib.RegisterStash(id, label, slots, maxWeight, owner, groups, coords)

--- @param properties table
--- @return string|nil  inventoryId of the new temp stash
RPS_Lib.CreateTemporaryStash(properties)

--- @param prefix string
--- @param items table
--- @param coords vector3
--- @param slots number|nil
--- @param maxWeight number|nil
--- @param instance string|number|nil
--- @param model number|nil
RPS_Lib.CustomDrop(prefix, items, coords, slots, maxWeight, instance, model)

--- @param playerId number
--- @return string|nil  dropId created from the player's inventory contents
RPS_Lib.CreateDropFromPlayer(playerId)

--- @param keep string|string[]|nil  items to preserve
RPS_Lib.ClearInventory(inventoryId, keep)
RPS_Lib.RemoveInventory(inventoryId)

--- @param source number  confiscate into a stash / return it later
RPS_Lib.ConfiscateInventory(source)
RPS_Lib.ReturnInventory(source)

--- @param target number
--- @param source number  let `source` inspect `target`'s inventory read-only
RPS_Lib.InspectInventory(target, source)

--- @param playerId number
--- @param invType string  'player'|'stash'|'container'|'drop'|'glovebox'|'trunk'|'dumpster'
--- @param data number|string|table
RPS_Lib.ForceOpenInventory(playerId, invType, data)

--- @return table|nil
RPS_Lib.GetCurrentWeapon(inventoryId)

RPS_Lib.SetDurability(inventoryId, slot, durability)
RPS_Lib.SetMetadata(inventoryId, slot, metadata)
RPS_Lib.UpdateVehicle(oldPlate, newPlate)

--- @return boolean
RPS_Lib.CanSwapItem(inventoryId, firstItem, firstItemCount, testItem, testItemCount)

--- @return table|nil
RPS_Lib.GetContainerFromSlot(inventoryId, slotId)

--- @param properties table  { slots, maxWeight, whitelist?, blacklist? }
RPS_Lib.SetContainerProperties(itemName, properties)

--- @param player table  { source, identifier, name, groups?, sex?, dateofbirth? }
--- @param data table|nil
RPS_Lib.SetPlayerInventory(player, data)

Usable items

--- @param item string
--- @param cb function(source, itemData)
RPS_Lib.CreateUseableItem(item, cb)
Registers a usable-item callback across whatever's active — ox_inventory:registerHook, qs-inventory:CreateUseableItem, or QBCore.Functions.CreateUseableItem (used for both 'qb' and 'qbx' framework, and for qb-inventory/qb-inventory-old).

RPS_Lib.CreateUseableItem('bandage', function(source, itemData)
    RPS_Lib.RemoveItem(source, 'bandage', 1)
    -- heal logic
end)

11. Vehicle keys (client)

Supports: ak47_vehiclekeys, ak47_qb_vehiclekeys, tgiann-hotwire, wasabi_carlock, qs-vehiclekeys, cd_garage, qb-vehiclekeys, qbx_vehiclekeys, vehiclekeys.

--- @param plate string
--- @param vehicle number  entity handle
--- @param virtual boolean|nil  temporary/virtual keys, where supported
RPS_Lib.GiveVehicleKey(plate, vehicle, virtual)
RPS_Lib.RemoveVehicleKey(plate, vehicle, virtual)
⚠️ On tgiann-hotwire, RemoveVehicleKey has no real equivalent by design (it's a physical-key simulation) — it falls back to pulling the key out of the ignition instead, if a vehicle handle is provided.

tgiann-hotwire-exclusive functions

All check _Lib.VehicleKey == 'tgiann-hotwire' and no-op (or return false) on every other vehicle-key system.

--- @param vehicleOrPlate number|string
--- @return boolean
RPS_Lib.HaveVehicleKey(vehicleOrPlate)

--- @param vehicle number
--- @return boolean
RPS_Lib.IsKeyInIgnition(vehicle)

--- @param vehicle number
--- @param state boolean  true = insert, false = remove
RPS_Lib.SetKeyInIgnition(vehicle, state)

--- @param vehicle number|nil
--- @param plate string|nil  use one or the other — restores ignition state on spawn
RPS_Lib.CheckKeyInIgnitionWhenSpawn(vehicle, plate)

--- @param vehicle number
--- @param state boolean  true = lock key in (can't be removed), false = allow removal
RPS_Lib.SetNonRemoveableIgnition(vehicle, state)

--- @param vehicle number
--- @param status number  eVehicleLockState enum (0-10) — falls back to
---                        SetVehicleDoorsLocked on non-tgiann-hotwire systems
RPS_Lib.SetVehicleLockStatus(vehicle, status)

--- @param state boolean|nil  nil = toggle based on current engine state
RPS_Lib.ChangeEngineStateButton(state)

12. Vehicle keys (server)

--- @param source number
--- @param plate string
--- @param vehNetId number
--- @param virtual boolean|nil
RPS_Lib.GiveVehicleKey(source, plate, vehNetId, virtual)
RPS_Lib.RemoveVehicleKey(source, plate, vehNetId, virtual)
On tgiann-hotwire, these call the real server export (exports['tgiann-hotwire']:GiveKeyPlate(source, plate, isNew)) directly — no client round-trip needed. On ak47_vehiclekeys, ak47_qb_vehiclekeys, cd_garage, qb-vehiclekeys, qbx_vehiclekeys, there's no server export, so these route through a TriggerClientEvent to the client functions in §11 instead.

tgiann-hotwire-exclusive server functions

--- @param vehNetId number|nil
--- @param entity number|nil  use one or the other
--- @return boolean
RPS_Lib.IsKeyInIgnition(vehNetId, entity)

--- @param state boolean
RPS_Lib.SetKeyInIgnition(vehNetId, entity, state)

RPS_Lib.CheckKeyInIgnitionWhenSpawn(vehNetId, entity)

--- @param state boolean
RPS_Lib.SetNonRemoveableIgnition(vehNetId, entity, state)

⚠️ tgiann-hotwire's own docs warn against calling GiveKey* on every pickup. It treats keys as persistent physical objects, not a togglable flag — repeated calls duplicate keys for the player. Only call GiveVehicleKey when a player should genuinely receive a new key (a sale, a garage handout, etc.), not every time they get in a vehicle they already have a key for.


13. Fuel (client)

Supports: LegacyFuel, ox_fuel, ps-fuel, lc_fuel, rcore_fuel, rcore_fuel_beta. Falls back to native GetVehicleFuelLevel / SetVehicleFuelLevel if nothing is detected.

--- @param vehicle number  entity handle
--- @return number  0-100
RPS_Lib.GetVehicleFuel(vehicle)

--- @param vehicle number
--- @param amount number  0.0 - 100.0
RPS_Lib.SetVehicleFuel(vehicle, amount)

--- @param vehicle number
--- @param fuelType string|nil  'regular' | 'plus' | 'premium' | 'diesel' | nil (reset)
RPS_Lib.SetVehicleFuelType(vehicle, fuelType)  -- lc_fuel only, no-op elsewhere

14. Garage (client)

Supports: qb-garages, cd_garage, jg-advancedgarages, rps_garage.

--- @param garageId string
--- @param vehicle number  entity handle
--- @param garageVehicleType string|nil  'car' | 'sea' | 'air' (jg-advancedgarages only)
RPS_Lib.StoreVehicleHousing(garageId, vehicle, garageVehicleType)

--- @param garageId string
--- @param vehicleType string|nil  'car' | 'air' | 'sea' (jg-advancedgarages only)
--- @param spawnCoords vector4|nil  jg-advancedgarages only
RPS_Lib.OpenGarageHousing(garageId, vehicleType, spawnCoords)

jg-advancedgarages-exclusive functions

RPS_Lib.ShowImpoundForm()                 -- opens the impound UI, if permitted
RPS_Lib.ShowPrivateGaragesDashboard()     -- create/edit/delete private garages
RPS_Lib.ShowChangePlateForm(vehicle)      -- @param vehicle number|nil

15. Garage (server)

--- @return table  every registered garage's config (jg-advancedgarages only, else {})
RPS_Lib.GetAllGarages()

--- @param plate string
--- @param netId number  vehicle network ID — prevents duplication when
---                       another script also spawns from the same garage.
---                       jg-advancedgarages v3.2.5+ only.
RPS_Lib.RegisterVehicleOutside(plate, netId)

--- @param plate string  deletes a vehicle registered as outside the garage
RPS_Lib.DeleteOutsideVehicle(plate)

16. Target (client)

Supports: ak47_target, ox_target, qb-target, qtarget. Every zone / entity / model / global registration is automatically cleaned up when the calling resource stops, via GetInvokingResource() tracking and an internal onResourceStop handler — you don't need to write your own cleanup for these.

--- @param name string
--- @param center vector3
--- @param length number
--- @param width number
--- @param options table     { minZ?, maxZ?, heading?, debugPoly? }
--- @param targetOptions table  { options = {...}, distance = number }
--- @return any  zone id
RPS_Lib.AddBoxZone(name, center, length, width, options, targetOptions)

--- @param points table  array of vector3
--- @param options table  { minZ, maxZ, debugPoly }
--- @return any
RPS_Lib.AddPolyZone(name, points, options, targetOptions)

--- @param center vector3
--- @param radius number
--- @return any
RPS_Lib.AddCircleZone(name, center, radius, options, targetOptions)

--- @param id any  zone id returned by one of the Add*Zone functions
RPS_Lib.RemoveZone(id)

--- @param bones string|table
RPS_Lib.AddTargetBone(bones, targetOptions)

--- @param entities table|number
RPS_Lib.AddTargetEntity(entities, targetOptions)
RPS_Lib.RemoveTargetEntity(entities, labels)

--- @param models string|number|table
RPS_Lib.AddTargetModel(models, targetOptions)
RPS_Lib.RemoveTargetModel(models, labels)

RPS_Lib.AddGlobalPed(targetOptions)
RPS_Lib.RemoveGlobalPed(labels)

RPS_Lib.AddGlobalVehicle(targetOptions)
RPS_Lib.RemoveGlobalVehicle(labels)

RPS_Lib.AddGlobalObject(targetOptions)
RPS_Lib.RemoveGlobalObject(labels)

RPS_Lib.AddGlobalPlayer(targetOptions)
RPS_Lib.RemoveGlobalPlayer(labels)

targetOptions.options[] entries accept qb-target-style fields (label, icon, action, job, gang, citizenid, item, event, type) — these are converted internally into ox_target-style (onSelect, groups, serverEvent/command, etc.) when an ak47_target/ox_target backend is active, so you can write one option table and it works on every supported target system.

RPS_Lib.AddBoxZone('shop_zone', vector3(100.0, 200.0, 30.0), 2.0, 2.0, {
    heading = 0.0,
}, {
    options = {
        {
            label  = 'Open Shop',
            icon   = 'fas fa-shopping-cart',
            job    = 'unemployed',  -- works as a single job string...
            action = function() TriggerEvent('your_script:openShop') end,
        },
    },
    distance = 2.5,
})

If you want to remove a zone before the resource stops, keep the return value and call RemoveZone yourself — auto-cleanup only fires on onResourceStop, not on demand.


17. Notify, progress, status (client)

RPS_Lib.Notify(msg, ntype, duration)

--- @param msg string
--- @param ntype string  'success' | 'error' | 'info'
--- @param duration number|nil  ms, defaults to 5000
Routes based on Config.Notify: oxTriggerEvent('ox_lib:notify', ...) (no lib global needed — ox_lib listens for this event on its own client script); esxESX.ShowNotification(msg); qbQBCore.Functions.Notify; qbxexports.qbx_core:Notify. Anything else fires a generic rps_lib:notify event you can hook in your own resource.

RPS_Lib.ShowProgress(data, successCb, cancelCb)

--- @param data table  { label, duration, useWhileDead?, canCancel?, disable?, anim?, prop? }
--- @param successCb function|nil  called if the bar completes
--- @param cancelCb function|nil   called if the bar is cancelled
--- @return boolean  true = completed, false = cancelled

⚠️ The function is ShowProgress, not Progressbar — there is no RPS_Lib.Progressbar. This exact typo has broken real integrations before; double-check this name if your progress bar isn't appearing.

data follows the ox_lib-style shape regardless of which backend is active — rps_lib translates internally for ESX's progressBars resource and QBCore's Functions.Progressbar.

local completed = RPS_Lib.ShowProgress({
    label     = 'Repairing engine...',
    duration  = 5000,
    canCancel = true,
    disable   = { move = true, car = true, combat = true },
}, function()
    RPS_Lib.Notify('Repair complete.', 'success')
end, function()
    RPS_Lib.Notify('Repair cancelled.', 'error')
end)

Status

--- @param target number|nil  server ID of another player, or nil for self
--- @return boolean
RPS_Lib.IsDead(target)
RPS_Lib.IsLastStand(target)

--- @return boolean  IsDead(target) or IsLastStand(target)
RPS_Lib.IsIncapacitated(target)

⚠️ The "downed" check is IsLastStand, not IsInLastStand. If you just want "is this player out of action for any reason," use IsIncapacitated — it already does IsDead(target) or IsLastStand(target) for you.

Checking a target other than yourself triggers a server round-trip via GetTargetMetaValue (see §4), blocking for up to 1 second.


18. Stress — two APIs, read this carefully

rps_lib has two separate, non-interoperable stress implementations. This is a real inconsistency in the codebase, not a doc simplification — know which one you're calling.

API A — framework/client/init.lua (older)

--- @param amount number
RPS_Lib.AddStress(amount)
RPS_Lib.RemoveStress(amount)
Reads Config.Stress directly. Fires generic events (rps_lib:addStress / ps-stress:server:addStress / hud:client:UpdateStress) — it does not call jg-stress-addon exports even if that's what's detected.

API B — integration/client/stress.lua + integration/server/stress.lua (newer, more complete)

-- Client
--- @return number  0-100
RPS_Lib.GetStress()

--- @param amount number
RPS_Lib.GainStress(amount)
RPS_Lib.RelieveStress(amount)
RPS_Lib.ResetStress()          -- jg-stress-addon: real export. Others: RelieveStress(100)
RPS_Lib.SetStressLevel(amount) -- jg-stress-addon: real export. Others: computed via Gain/Relieve

--- @return boolean  jg-stress-addon only, false elsewhere
RPS_Lib.IsPlayerJobWhitelisted()

-- Server
--- @param source number
--- @return number
RPS_Lib.GetStress(source)

--- @param source number
--- @param amount number
RPS_Lib.GainStress(source, amount)
RPS_Lib.RelieveStress(source, amount)
RPS_Lib.ResetStress(source)    -- always calls RelieveStress(source, 100); no native reset export server-side
Reads _Lib.Stress, and does call real jg-stress-addon exports (getStress, gainStress, relieveStress, resetStress, setStressLevel, isPlayerJobWhitelisted) when that's the detected system, with ps-stress/qb metadata fallbacks otherwise.

These are different function names, so both genuinely coexist on _Lib at the same timeAddStress/RemoveStress (API A) and GainStress/RelieveStress (API B) don't collide or overwrite each other. The risk isn't one clobbering the other; it's that calling the wrong one silently gives you the wrong behavior — API A never touches jg-stress-addon even when that's the detected system, since it only fires generic events and reads Config.Stress rather than dispatching to the addon's real exports.

Practical takeaway: always use API B (GainStress, RelieveStress, GetStress, ResetStress, SetStressLevel) for anything you write. Only reach for AddStress/RemoveStress if you specifically need the ps-stress/hud:client:UpdateStress event-firing behavior and don't care about jg-stress-addon integration.


19. 3D text & UI helpers (client)

From interface/3dtext.lua:

--- @param msg string
--- @param x number  @param y number  @param z number
--- @param scale number
--- @param r number  @param g number  @param b number  @param a number
RPS_Lib.Draw3DText(msg, x, y, z, scale, r, g, b, a)

--- @param msg string
--- @param coords vector3
--- @param drawDistance number|nil
RPS_Lib.Show3DTextUI(msg, coords, drawDistance)
RPS_Lib.Hide3DTextUI()

--- @param items table
--- @param title string|nil
RPS_Lib.OpenContextMenu(items, title)

--- @param title string
--- @param rows table
RPS_Lib.InputDialog(title, rows)

--- @param data table
RPS_Lib.AlertDialog(data)

--- @param npcName string
--- @param options table
RPS_Lib.OpenNpcDialogue(npcName, options)

--- @param id string
--- @param title string
--- @param tasks table
RPS_Lib.ShowChecklist(id, title, tasks)

--- @param taskIndex number
--- @param done boolean
RPS_Lib.UpdateChecklistTask(id, taskIndex, done)
RPS_Lib.HideChecklist(id)

--- @param text string
--- @param duration number|nil
RPS_Lib.ShowObjective(text, duration)
RPS_Lib.HideObjective()

--- @param entity number
--- @param cb function
RPS_Lib.UseGizmo(entity, cb)

These are higher-level UI helpers built on top of ox_lib's menu/dialog primitives where applicable — open interface/3dtext.lua directly if you need exact parameter shapes for rows, items, or data, since they mirror ox_lib's own table structures closely.


20. Banking (server)

Supports: Renewed-Banking, okokBanking, qb-banking. Falls back to esx_addonaccount on ESX if none of the three are detected.

--- @param job string
--- @return number
RPS_Lib.GetSocietyMoney(job)

--- @param job string
--- @param amount number
RPS_Lib.AddSocietyMoney(job, amount)
RPS_Lib.RemoveSocietyMoney(job, amount)
RPS_Lib.AddSocietyMoney('police', 500)
local balance = RPS_Lib.GetSocietyMoney('police')

If _Lib.Banking == 'none' and no esx_addonaccount fallback applies, AddSocietyMoney/RemoveSocietyMoney print an RPS.Error and do nothing; GetSocietyMoney returns 0.

The exact export names called per backend (verified against each resource's official docs): Renewed-BankinggetAccountMoney / addAccountMoney / removeAccountMoney (lowercase-first — this resource does not expose the qb-banking-style GetAccountBalance/ AddMoney/RemoveMoney names some other bridges assume). okokBankingGetAccount / AddMoney / RemoveMoney. qb-bankingGetAccountBalance / AddMoney / RemoveMoney.


21. Shared utilities (RPS table)

These live on the global RPS table (not _Lib) — available to any file inside rps_lib itself, and technically reachable from outside if you really need them, though they're meant as internal helpers rather than a public API surface.

--- @param name string
--- @return boolean
RPS.ResourceStarted(name)

RPS.Debug(...)   -- only prints if Config.Debug == true
RPS.Info(msg)    -- prints with [rps_lib] tag
RPS.Log = RPS.Info  -- alias
RPS.Warn(msg)    -- prints with WARN tag
RPS.Error(msg)   -- prints with ERROR tag

--- @param orig table
--- @return table
RPS.DeepCopy(orig)

--- @param tbl table  @param val any
--- @return boolean
RPS.TableContains(tbl, val)

--- @param target table  @param source table
--- @return table  target, mutated in place
RPS.MergeTable(target, source)

--- @param tbl table
--- @return number  works on non-sequential/mixed tables too
RPS.TableLength(tbl)

--- @param s string
--- @return string
RPS.Trim(s)

--- @param s string  @param sep string
--- @return table
RPS.Split(s, sep)

--- @param s string
--- @return string
RPS.Capitalise(s)

The Detect* functions (DetectFramework, DetectInventory, DetectTarget, DetectFuel, DetectVehicleKeys, DetectGarage, DetectBanking, DetectNotify, DetectProgressbar, DetectStress) are also on RPS, but you'll never need to call these yourself — they run once at startup and their results are already sitting in the _Lib fields from §3.


22. Config reference

rps_lib's own config.lua — you generally don't need to touch this from your resource, but it's useful to know what each value resolves to and which functions read Config.X directly instead of _Lib.X.

Config key Default Notes
Config.Framework 'auto' 'esx' | 'qb' | 'qbx'
Config.Notify 'ox' Not 'auto' by default. Read directly by _Lib.Notify (client) and the server _Lib.Notify(source, ...).
Config.Progressbar 'ox' Not 'auto' by default. Read directly by _Lib.ShowProgress.
Config.Target 'auto'
Config.Inventory 'auto'
Config.VehicleKey 'auto'
Config.FuelScript 'auto' Read directly by the fuel module (not _Lib.Fuel).
Config.Garage 'auto'
Config.Banking 'auto'
Config.Stress 'auto' Read by API A (AddStress/RemoveStress); API B reads _Lib.Stress instead — see §18.
Config.Debug false Gates RPS.Debug(...) output.

A validation pass runs at resource start and prints a WARN for any unset or unrecognized value — check your console if you've edited config.lua and something isn't routing the way you expect.


23. Full worked example

A vehicle impound system combining target, vehicle keys, banking, and stress in one realistic flow:

-- client.lua
local RPS_Lib = exports['rps_lib']:GetLibObject()

CreateThread(function()
    RPS_Lib.AddGlobalVehicle({
        options = {
            {
                label  = 'Impound Vehicle',
                icon   = 'fas fa-warehouse',
                groups = { 'police' },
                action = function(entity)
                    local plate = GetVehicleNumberPlateText(entity)
                    local netId = NetworkGetNetworkIdFromEntity(entity)
                    TriggerServerEvent('your_script:impoundVehicle', plate, netId)
                end,
            },
        },
        distance = 3.0,
    })
end)
-- server.lua
local RPS_Lib = exports['rps_lib']:GetLibObject()
local IMPOUND_FEE = 250

RegisterNetEvent('your_script:impoundVehicle', function(plate, netId)
    local source = source

    local allowed = RPS_Lib.HasPermission(source, {
        WithGroup = { police = true },
    })
    if not allowed then return end

    RPS_Lib.RemoveVehicleKey(source, plate, netId)
    RPS_Lib.AddSocietyMoney('police', IMPOUND_FEE)
    RPS_Lib.GainStress(source, 5)  -- API B, see §18 — not AddStress

    RPS_Lib.Notify(source, ('Vehicle %s impounded.'):format(plate), 'success')
    print(('^2[your_script]^7 %s impounded by %s, $%d added to police funds')
        :format(plate, source, IMPOUND_FEE))
end)

Notice that nothing here checks _Lib.Inventory, _Lib.VehicleKey, _Lib.Target, _Lib.Banking, or _Lib.Stress — the whole point of going through rps_lib is that this resource works unmodified across servers running completely different combinations of those backend systems.


24. Common mistakes

  • Guessing function names from memory or other libraries' conventions. It's ShowProgress, not Progressbar. IsLastStand, not IsInLastStand. GainStress/RelieveStress (API B), not AddStress/RemoveStress (API A) if you want jg-stress-addon support. Open the actual integration/*.lua file and read the doc-comment before calling — a stray top-level typo in your own file can abort your whole script's parse and present as a confusing rps_lib-side error, when the bug was never in rps_lib at all.
  • Caching a detected field and branching on it yourself. If you find yourself writing if RPS_Lib.Inventory == 'ox_inventory' then ... end in your own script, that's a sign the generic function you need either already exists (check §9/§10) or should be added to rps_lib itself rather than re-implemented per caller.
  • Calling GetLibObject() repeatedly. Store it once per file.
  • Assuming _Lib.Items is populated on tick 0. It's filled by an auto-detect thread that can take a moment. Call RPS_Lib.GetItems() (server) for an on-demand fetch instead of reading the cache early.
  • Writing your own onResourceStop cleanup for target zones. Already handled — see §16. A duplicate removal call on an already-cleaned-up zone can error depending on the underlying target resource.
  • Calling tgiann-hotwire's GiveVehicleKey on every vehicle entry. It duplicates keys — only call it when a player should receive a genuinely new key.
  • Forgetting ESX has no native gang system. GetGang/SetGang are effectively QB/QBX-only; on ESX they return a neutral stub and do nothing, respectively.