📚 Libs
Documentation relating to the spooni_libs.
1. Installation
spooni_libs works Standalone.
To install spooni_libs:
- Download the resource
- On Github
- Drag and drop the resource into your resources folder
spooni_libs
- Add this ensure in your server.cfg (before any script that depends on it)
ensure spooni_libs - Now you can configure the script as you like
config.lua
- At the end, restart the server
If you have any problems, you can always open a ticket in the Spooni Discord.
2. Usage
spooni_libs is a framework bridge library used to call the same functions across multiple RedM frameworks (VORP, RSG, TPZ and Custom). Most other spooni_ scripts depend on it for progress bars, notifications, the interactions/target system, shops, minigames and more — install it before any script that requires it.
3. For developers
Supported framework modes:
- VORP
- RSG
- TPZ
- Custom
All functions should be used server-side unless your script explicitly exposes them to client-side usage.
Export API (exports["spooni_libs"]:Function())
Core Exports Index
Every export is server-side unless noted -- client in a comment.
exports["spooni_libs"]:Keyboard(data)
exports["spooni_libs"]:StartProgressBar(data, callback)
exports["spooni_libs"]:StartProgressBarCircle(data, callback) -- native rpg_meter bar (bottom-center)
exports["spooni_libs"]:IsProgressBarCircleActive() -- true while a circle bar is running
exports["spooni_libs"]:Notify(title, text, icon, duration, placement, color, loadingBar, background)
exports["spooni_libs"]:Announce(title, text, duration, placement, color, loadingBar, background)
exports["spooni_libs"]:IsPedManagerEnabled() -- always available; true when Config.PedManager is on
exports["spooni_libs"]:registerNpc(resourceName, npcData, options)
exports["spooni_libs"]:unregisterNpcs(GetCurrentResourceName())
exports["spooni_libs"]:getStats()
exports["spooni_libs"]:GetLanguage()
-- PADLOCK MINIGAME (safecrack)
exports["spooni_libs"]:Padlock({ code1 = , code2 = , code3 = , code4 = }) -- returns true/false (client)
-- LOCKPICK MINIGAME (pick a lock)
exports["spooni_libs"]:Lockpick(difficulty) -- difficulty: "low"|"medium"|"hard"|"veryhard", returns true/false (client)
-- INTERACTIONS / TARGET
exports["spooni_libs"]:AddInteraction(data)
exports["spooni_libs"]:AddLocalEntity(data)
exports["spooni_libs"]:AddNetEntity(data)
exports["spooni_libs"]:AddGlobalPlayerInteraction(data)
exports["spooni_libs"]:AddGlobalVehicleInteraction(data)
exports["spooni_libs"]:RegisterPedOptions(resourceName, options)
exports["spooni_libs"]:AddOption(id, option)
exports["spooni_libs"]:RemoveInteraction(id)
exports["spooni_libs"]:SetInteractionGroups(groups)
exports["spooni_libs"]:OpenInteractions(data) -- on-demand menu (no ALT), returns the choice
exports["spooni_libs"]:CloseInteractions()
exports["spooni_libs"]:IsInteractionsOpen()
-- PLAYER SELECTOR (click a player)
exports["spooni_libs"]:SelectPlayer(options) -- pin over every nearby player, click one, returns his server id (client)
-- SHOP (built-in buy/sell shop)
-- server:
exports["spooni_libs"]:RegisterShop(def) -- register a persistent shop (returns id | false, err)
exports["spooni_libs"]:OpenShop(src, idOrDef) -- open for a player: registered id OR an inline def
exports["spooni_libs"]:CloseShop(src)
exports["spooni_libs"]:IsOpen(src)
exports["spooni_libs"]:UnregisterShop(id)
exports["spooni_libs"]:IsShopRegistered(id)
-- client:
exports["spooni_libs"]:OpenShop(shopId) -- open a registered shop by id
exports["spooni_libs"]:CloseShop()
exports["spooni_libs"]:IsShopOpen()
-- PROP PLACER (place a prop, streamed per-client by distance, pick it back up)
-- client:
exports["spooni_libs"]:PlaceProp(options) -- placement UI + gizmo; registers the prop, returns true | false, reason
-- server:
exports["spooni_libs"]:GetPlacedProps() -- table of every placed prop { [id] = data }
exports["spooni_libs"]:RemovePlacedProp(id) -- remove a placed prop by id (despawns it for everyone)Interactions, Target & Player Selector
A built-in target system that draws world pins while the player holds LEFT ALT. Scroll up/down to pick an option and press E to confirm. It is part of spooni_libs, so no extra resource (ox_target / interact) is required.
There are two ways to use it: the world target above (hold ALT), and an on-demand menu opened from code with OpenInteractions — no ALT (see On-demand menu below). Both work at the same time.
All settings live in config.lua under the INTERACTIONS / TARGET section. The sprite dict (stream/spooni_interact.ytd) and the draw sizes are defined at the top of c/interactions.lua.
Static point
exports["spooni_libs"]:AddInteraction({
id = "shop_butcher", -- optional, auto-generated if omitted
coords = vector3(-275.0, 803.0, 119.0),
distance = 6.0, -- scan distance
renderDistance = 3.0, -- when the pin appears
interactDst = 1.5, -- when the menu opens
options = {
{
label = "Open shop",
onSelect = function() print("opened") end
},
{
label = "Police only",
groups = { police = 0 }, -- job/grade gate
event = "myresource:doThing"
}
}
})Local entity / NPC (pin anchored to a bone)
exports["spooni_libs"]:AddLocalEntity({
entity = ped,
bone = 47576, -- SKEL_Head
options = {
{ label = "Talk", onSelect = function(_, entity) print(entity) end }
}
})Networked entity (synced cleanup across clients)
exports["spooni_libs"]:AddNetEntity({
netId = netId,
options = { ... }
})Every ped in the world (global ped options)
exports["spooni_libs"]:RegisterPedOptions("myresource", {
{
label = "Rob",
canInteract = function(entity) return IsEntityDead(entity) end,
onSelect = function(_, entity) end
}
})
-- exports["spooni_libs"]:UnregisterPedOptions("myresource")Global player / vehicle interactions
exports["spooni_libs"]:AddGlobalPlayerInteraction({
options = { { label = "Give item", serverEvent = "myres:giveItem" } }
})
exports["spooni_libs"]:AddGlobalVehicleInteraction({
options = { { label = "Inspect", onSelect = function(_, veh) end } }
})Option fields
| Field | Description |
|---|---|
label / text | Text shown in the menu row. |
distance / interactDst | Per-option distance limit (optional). |
groups / group / job / jobs | Job/grade gate. { police = 2 } = police grade ≥ 2, or "police" / { "police", "sheriff" }. |
items / item | Item gate (see note below). |
canInteract(entity, coords, args, interaction) | Return true to show the option. |
canUse(option, entity, interaction) | Alternative gate callback. |
onSelect(option, entity, interaction) | Run on confirm. |
action(entity, coords, args, serverId) | Alternative confirm callback. |
serverEvent | Server event triggered on confirm (args, serverId). |
event | Client event triggered on confirm. |
Managing interactions
local id = exports["spooni_libs"]:AddInteraction({ ... })
exports["spooni_libs"]:AddOption(id, { label = "Extra", onSelect = function() end })
exports["spooni_libs"]:ChangeOptionText(id, 1, "New label")
exports["spooni_libs"]:RemoveInteraction(id) -- removes the whole interactionOn-demand menu (no ALT)
OpenInteractions works like the Keyboard (input) export: you call it with a list of options, it shows at the world coords you pass, and it blocks until the player picks one — then it returns the chosen option and its index, so the calling script decides what to do (just like Keyboard returns the typed text). No ALT, no registered world point. Scroll to pick, E to confirm, Backspace to cancel.
coords is passed the same way it looks in a script config — an array { x, y, z } (an optional 4th heading value is ignored) or { x = .., y = .., z = .. }. You do not need vector3(...).
local selected, index = exports["spooni_libs"]:OpenInteractions({
coords = { -275.0, 803.0, 119.0 }, -- array, like in your config (or { x =, y =, z = })
options = {
{ label = "Open stash" },
{ label = "Change duty" },
{ label = "Close", close = true }, -- close = true -> a plain cancel button
},
})
if selected then
-- selected is the option table you passed; index is its position (1-based)
if index == 1 then
print("open stash")
elseif selected.label == "Change duty" then
print("change duty")
end
else
print("cancelled") -- Backspace, or the close button
endThe returned value is nil when cancelled. Options may still carry onSelect / serverEvent / event / group-item gating / canInteract — those run on confirm as well, and keepOpen = true keeps the menu open after running them (so the call does not return yet). You can also close it from code with CloseInteractions(), and IsInteractionsOpen() returns true while it is open.
The menu is anchored to
coordsand hides if you look away from that spot (like the ALT pins). If you omitcoordsit falls back to a screen-centred menu and the optionaltitleis shown.
Permission gating
Job/group gating is pulled automatically from the configured framework (VORP / RSG / TPZ) on character select and on job change. You can also set it manually:
exports["spooni_libs"]:SetInteractionGroups({ police = 2, sheriff = 0 })
exports["spooni_libs"]:SetInteractionItems({ water = 3, bread = 1 })Item gating is not auto-populated from the inventory. Either call
SetInteractionItemsyourself, or use acanInteractcallback that checks the item server-side. Options withoutitemsalways show.
Player Selector
A client export that shows the interaction pin above every nearby player and blocks until one is clicked. The mouse cursor is freed, hovering a pin highlights it, left-click picks that player and ESC cancels. It returns the clicked player's server id, so the calling script decides what to do with it (give an item, hire, treat, ...).
local serverId, reason = exports["spooni_libs"]:SelectPlayer({
distance = 8.0, -- search radius in meters (default 8.0)
amount_of_players = 8, -- max candidates shown (default 6)
allow_self = false, -- true = your own pin appears too, so you can pick yourself
allow_in_vehicle = false, -- true = usable while in a vehicle
allow_on_horse = false, -- true = usable while mounted
ignore_los = false, -- true = skip the line-of-sight test (see note below)
ignore_interior = false, -- true = skip the same-interior test
})
if serverId then
print("picked player", serverId)
else
print("no pick:", reason)
endserverId is false when nothing was picked; reason then tells you why:
| Reason | Meaning |
|---|---|
"no_players" | Nobody within distance (after the filters). |
"cancelled" | ESC was pressed (or the caller died mid-selection). |
"busy" | Another selection is already running. |
"vehicle" / "horse" | Caller is in a vehicle / mounted and the matching allow flag is off. |
"dict" | The pin texture failed to load. |
ignore_losnote: the line-of-sight test only sees what is in front of your character, and props (desks, counters) block it. Indoors — behind a desk, right after closing a menu — it can filter out a player standing next to you. Passignore_los = true(and usuallyignore_interior = true) there; at small radii the distance check alone is enough.
The pins are the same sprites the ALT interactions use, so the look stays consistent. Only one selection can run at a time, and the export never consumes or triggers anything by itself — it only returns who was clicked.
Prop Placer
Places a prop in the world with a live semi-transparent ghost you position, with an optional gizmo (three.js handles) for precise adjustment. On confirm the prop is registered on the server (in memory — no database) and every client streams its own local, non-networked copy by distance: it spawns when a player gets near, despawns when they walk away, and respawns when they come back — until someone picks it up. Only nearby clients hold the object, so it stays light on RedM's entity limit (same idea as a furniture/clan streamer, without the DB). Props are ephemeral — gone on a server restart, which is exactly what you want for temporary cover / barricades.
-- CLIENT — e.g. when a "barricade" item is used
local ok = exports["spooni_libs"]:PlaceProp({
model = "p_barricade01x", -- REQUIRED — prop model (string or hash)
distance = 2.5, -- how far in front of you the ghost floats
rotateSpeed = 2.0, -- degrees per frame while holding a rotate key
ground = true, -- keep the prop sitting on the ground
alpha = 170, -- ghost transparency while placing (0-255)
pickup = true, -- let anyone nearby pick it up
pickupDist = 2.0, -- how close you must be to pick up
pickupEvent = "myscript:propPickedUp", -- SERVER event fired on pickup (see below)
labels = { place = "Place", cancel = "Cancel", rotate = "Rotate", adjust = "Adjust", pickup = "Pick Up" },
})
if ok then
-- placed — keep the item consumed
else
-- cancelled / bad model — refund the item if you want
endPlacement controls: walk / turn to aim, ←/→ rotate, SPACE opens the gizmo (drag the arrows to move, the rings to rotate, or the ← / → keys), E places, Back cancels. Standing next to a placed prop, E picks it up.
Pickup refund is server-side. Because any nearby player can pick a prop up, the refund is handled on the server: pass a pickupEvent name and register that event on the server — it fires as (pickerSource, model):
-- SERVER side of your own resource
RegisterNetEvent("myscript:propPickedUp", function(pickerSource, model)
-- give the item back to whoever picked it up, however your framework does it
end)Server helpers:
local all = exports["spooni_libs"]:GetPlacedProps() -- { [id] = { model, x, y, z, rx, ry, rz, owner, ... } }
exports["spooni_libs"]:RemovePlacedProp(id) -- despawn a specific prop for everyoneStream distances are in
config.lua→Config.PropPlacer:SpawnDistance(spawn a prop when a player gets this close, default80) andDespawnDistance(despawn it when they get this far, must be> SpawnDistanceto avoid flicker).Persistence is optional in the same block:
SaveToDatabase(defaultfalse). Leftfalse, props are in-memory only and gone on a server restart. Set ittrueto save placed props to a DB table (DatabaseTable, defaultspooni_placed_props) so they survive restarts and other resources can read them; the table is created automatically and props are reloaded on start.
PlacePropreturnstrueonce the prop is registered, orfalse+ a reason ("busy"/"model"/"cancelled"). It does not consume or give any item — gating the item (and refunding on pickup) is the calling script's job.
Notifications, Inputs & Progress Bars
A full notification + announcement system is built directly into spooni_libs; no separate notification resource is required.
Client example:
exports["spooni_libs"]:Notify(
"Success!",
"You received ~img:notify_tick~ in your inventory.",
"notify_tick",
7000,
"middle-left",
"COLOR_GREEN"
)Supported colors: COLOR_WHITE, COLOR_GREEN, COLOR_BLUE, COLOR_RED. The selected color is applied only to the complete title and description. The whole notification behaves like the progress bar: a white fill loads from 0% to 100% across it over its duration, sweeping over the icon, title and text. While unfilled, the text shows in the chosen color; where the white bar has already passed, the text turns black. Images (icon and inline ~img:~) keep their natural colors throughout so they always stay recognizable.
Set
Config.NotifyLoadingBar = falseto turn the white loading sweep off entirely — notifications and announcements then stay static in their chosen color, but still slide in/out and auto-dismiss after their duration.
Notify/Announcealso take an optional last argumentloadingBar(true/false) to force the sweep on or off for that one call. Omit it (or passnil) to useConfig.NotifyLoadingBar:luaexports["spooni_libs"]:Notify("Success!", "Saved.", "notify_tick", 5000, "middle-left", "COLOR_GREEN", false) -- no sweep exports["spooni_libs"]:Notify("Success!", "Saved.", "notify_tick", 5000, "middle-left", "COLOR_GREEN") -- uses the config
Set
Config.NotifyBackground = falseto drop the weathered-paper background — notifications and announcements then render as text only (with a shadow so it stays readable).truekeeps the paper background.
Notify/Announcetake an optional final argumentbackground(true/false) afterloadingBar, to force the background on or off for that one call. Omit it (or passnil) to useConfig.NotifyBackground:luaexports["spooni_libs"]:Notify("Success!", "Saved.", "notify_tick", 5000, "middle-left", "COLOR_GREEN", nil, false) -- text only exports["spooni_libs"]:Notify("Success!", "Saved.", "notify_tick", 5000, "middle-left", "COLOR_GREEN") -- uses the config
Supported placements: top-right, top-center, top-left, middle-right, middle-center, middle-left, bottom-right, bottom-center, bottom-left.
Notifications slide in and out from the side they are anchored to (left, right, top or bottom; the middle-center placement scales in) and use the weathered_paper background. Drop a weathered_paper.png file into UI/img to customize the look (UI/img/*.png is already exposed by the manifest).
Icon / image resolution
Every image name you pass — the icon argument or any inline ~img:name~ — is resolved in this priority order:
UI/img/<name>.png(insidespooni_libs)- the framework inventory image path (e.g.
nui://vorp_inventory/html/img/items/<name>.png) UI/img/notify_tick.png(default fallback when the image is found nowhere)
So any image you drop into UI/img can be referenced directly by name. Full URLs (https://...) and absolute paths are used as-is.
While Config.Dev is enabled you can test with the /testnotify and /testannounce console commands.
Announcement example:
exports["spooni_libs"]:Announce(
"SERVER ANNOUNCEMENT",
"Server restart in 10 minutes!",
10000,
"top-center",
"COLOR_RED"
)Server example (the first argument is the player source, or -1 for everyone):
exports["spooni_libs"]:Notify(
source,
"Information",
"Your message",
nil,
5000,
"middle-left",
"COLOR_BLUE"
)Direct server events are also available as spooni_libs:notify and spooni_libs:announce client events.
Keyboard Input
The keyboard input popup — call it, get back what the player typed.
local success, firstname, lastname = exports["spooni_libs"]:Keyboard({
header = "CHARACTER CREATION",
rows = {
"First Name",
"Last Name"
}
})
if success then
print(firstname)
print(lastname)
else
print("Input cancelled")
endProgress Bar
The default NUI progress bar, with an optional animation and attached prop.
exports["spooni_libs"]:StartProgressBar({
-- Duration in milliseconds.
time = 5000,
-- Text displayed inside the progress bar.
text = "Searching...",
-- Item image name (without .png).
-- Images are loaded automatically from your inventory image path.
image = "bread",
-- Allow cancelling with ESC.
canStop = true,
-- Disable player controls while the progress bar is active.
disableControls = true,
-- Optional animation.
animation = {
animDict = "amb_work@world_human_hammer@table@male_a@base",
anim = "base",
flags = 1
},
-- Optional prop attached to the player.
prop = {
model = `p_hammer01x`,
bone = 57005,
coords = {
x = 0.12,
y = 0.0,
z = 0.0
},
rotation = {
x = 0.0,
y = 0.0,
z = 0.0
}
}
}, function(cancelled)
if cancelled then
print("Progress cancelled")
else
print("Progress finished")
end
end)A new progress bar cannot start while one is already active. After it finishes or is cancelled, a cooldown (
Config.ProgressBarCooldown, in milliseconds) must pass before a new one can start. Set it to0to disable the cooldown.
Progress Bar (Circle)
A second, lightweight progress bar drawn natively at the bottom-center of the screen (the in-game rpg_meter). It is completely separate from the NUI StartProgressBar above — both exist, use whichever fits. It has no ESC-cancel and no animation / prop options; it simply fills 0 → 100% and then fires the callback.
exports["spooni_libs"]:StartProgressBarCircle({
-- Duration in milliseconds.
time = 5000,
-- Text displayed under the bar.
text = "Searching...",
}, function()
print("Progress finished")
end)Only one circle bar can run at a time; a second call while one is active returns
false. Check it withexports["spooni_libs"]:IsProgressBarCircleActive().
While Config.Dev is enabled you can test it with the /testprogress2 console command (the NUI bar stays on /testprogress).
Minigames & World Systems
Shore Teleport
When a player dies in deep water they are faded out and moved back to the nearest dry shore, then faded back in — so a drowned body never sinks and gets stuck underwater. It is optional and lives in config.lua under AMBIENT WORLD SETTINGS:
ShoreTeleport = true, -- master on/off
ShoreTeleportSettings = {
CheckInterval = 3000, -- how often (ms) it checks for a drowned player
FadeOutMs = 800, -- fade-out before the teleport
BlackHoldMs = 600, -- how long the screen stays black
FadeInMs = 1200, -- fade-in after the teleport
ProbeRadius = 200.0, -- how far around the body to search for shore
ProbeStep = 8.0, -- distance between each search ring
LandMargin = 0.5, -- how far above the water the ground must be to count as shore
ShorePoints = { -- fallback points, used only when no nearby shore is found
vector3(3058.9019, 808.8454, 41.7765),
},
}The system first probes the world outward from the body for real dry ground; ShorePoints is only a fallback for when no shore is found nearby. Add your own vector3 points for the deep-water areas your map uses.
Padlock Minigame (Safecrack)
A safecrack (combination-dial) minigame is built directly into spooni_libs; no separate resource is required. It is a client export that opens a combination-dial lock and blocks until the player solves it, fails, or cancels, then returns true/false.
-- the 4-number combination (each 0-39)
local code = { code1 = 5, code2 = 10, code3 = 15, code4 = 20 }
local opened = exports["spooni_libs"]:Padlock(code)
if opened then
print("SUCCESS!") -- lock cracked
else
print("FAIL!") -- 3 mistakes, ESC, or the player died
endHow it plays:
- The dial is dragged with the mouse; stop on each number of the combination in order. Each correct number plays an unlock tick; the final number opens the shackle.
- 3 mistakes (passing a number without stopping on the right one) fails the lock.
- ESC cancels. If the player dies mid-crack the lock closes and returns
false. - A second
Padlockcall while one is already running returnsfalseimmediately.
The two safecrack animations are configurable in config.lua:
Padlock = {
OpenAnim = { "mini_games@safecrack@base", "dial_turn_right_stage_03" }, -- looped while open
FinishAnim = { "mini_games@safecrack@base", "open_lt" }, -- played on success
}While Config.Dev is enabled you can test it in-game with the /testlock console command — it generates a random combination, prints it to the console, and opens the minigame (alongside /testnotify, /testannounce, /testprogress, /testinp).
Lockpick Minigame
A "pick a lock" (pin-tumbler) minigame is built directly into spooni_libs; no separate resource is required. It is a client export that opens the pin-tumbler minigame and blocks until the player picks it, breaks all the pins, or cancels, then returns true/false.
-- difficulty is optional; defaults to Config.Lockpick.Default
local opened = exports["spooni_libs"]:Lockpick("hard") -- "low" | "medium" | "hard" | "veryhard"
if opened then
print("picked!")
else
print("failed / no lockpick / cancelled")
endHow it plays:
- Move the mouse left/right to rotate the pick; hold W / A / S / D (or the arrow keys) to apply torque to the cylinder.
- Near the hidden "sweet spot" the cylinder turns all the way → the lock opens.
- Off the sweet spot the pick takes damage and eventually breaks; run out of pins → fail.
- ESC cancels. If the player dies mid-pick the lock closes and returns
false.
Difficulty
The difficulty thresholds live in config.lua under Config.Lockpick.Difficulties. Each preset tunes the minigame; higher numPins / pinHealth and lower pinDamage with a wider solvePadding = easier:
| Preset | Feel | Pins | Pin health | Pin damage | Sweet spot |
|---|---|---|---|---|---|
low | easy | 4 | 320 | 6 | wide (13) |
medium | a bit harder | 3 | 220 | 10 | 8 |
hard | hard | 2 | 150 | 15 | 5 |
veryhard | very hard | 1 | 110 | 22 | narrow (3) |
Lockpick() with no argument uses Config.Lockpick.Default.
Item gate
By default the export requires the player to carry a lockpick item and removes one when the minigame is failed — configurable in config.lua:
Lockpick = {
Item = "lockpick", -- "" disables the item gate entirely
RequireItem = true, -- must own the item to start
RemoveOnFail = true, -- consume 1 on failure
NoItemText = "You don't have a lockpick on you!",
OpenAnim = { "mech_ransack@chest@front_lid@h70cm@swing_open@a", "enter_lf" },
Default = "medium",
Difficulties = { ... },
}The item check/removal is server-authoritative (the client cannot pick or consume without the server confirming the configured item). The only optional extra is the difficulty argument.
Using the item from the inventory (RegisterUsable)
The lockpick item is also usable directly from the inventory. When the player "uses" it:
- If a door system is configured (
Config.Lockpick.Doorlock.ResourceexportsGetDoor()), it tries the nearest door first —"BLACKLIST"→ notify, a door → open the minigame and fireDoorlock.OpenEventon success. - Otherwise it fires
Config.Lockpick.UseEvent— by default"Spooni-Police:Commands:LockpickHandcuffs", i.e. lockpick a nearby handcuffed player (handled by spooni_police, which then callsLockpick()under the hood).
Lockpick = {
...
RegisterUsable = true, -- make the item usable from the inventory
UseEvent = "Spooni-Police:Commands:LockpickHandcuffs", -- fired on use when no door is nearby ("" to disable)
Doorlock = {
Resource = "spooni_libs", -- the built-in door system exports GetDoor() ("" to disable door-lockpicking)
OpenEvent = "spooni_libs:doorlock:fromLockpick", -- fired on success to unlock the picked door
BlacklistText = "It is impossible to break this lock!",
},
}So in spooni_police the lockpick is reached two ways: using the lockpick item near a handcuffed player (→
LockpickHandcuffs), and the prison-wagon "get out" prompt / ALT interaction. Both run the samespooni_libsminigame. The handcuff path needsConfig.Lockpick = truein spooni_police.
While Config.Dev is enabled, test the minigame directly with /testlockpick [low|medium|hard|veryhard] (needs the lockpick item, or set Config.Lockpick.RequireItem = false).
Self-contained: the dial uses GSAP (
UI/js/TweenMax.min.js+UI/js/Draggable.min.js, bundled locally) and the game's built-in jQuery, so it needs no internet at runtime. The lock art lives atUI/img/padlock.pngand the UI is scoped under#padlock-root, so it never interferes with the inputs / progress bar / notifications on the same page.
Doorlock
A door-locking system is built into spooni_libs. Doors you define stay locked or unlocked server-wide; authorised players lock/unlock them by holding ALT near the door, and the lockpick item can force lockpickable doors open.
Defining doors
Doors live in doors.lua:
Config.Doors— each entry is a named door (or double door) with its worldcoords, itsdoors = { [doorHash] = {x, y, z} }, andislock(locked by default?).Config.DoorsOptions— per-door rules:{ jobs = {...}, items = {...} | false, lockpick = true | false }.jobs/itemsdecide who can toggle it with ALT;lockpick = falseblacklists it from the lockpick. A door with no entry here is freely toggleable and lockpickable.
To capture a new door's hash, set Config.Doorlock.Dev = true, stand in the doorway and run /doorhash (prints the hash to paste into doors.lua) or /finddoor (lists nearby door hashes).
Settings (Config.Doorlock, in doors.lua)
| Option | Default | Meaning |
|---|---|---|
Enabled | true | Master switch. false = the door system doesn't run at all. |
Distance | 2.0 | How close you must be to a door to toggle it. |
ShowIcon | true | Draw the lock/unlock icon on doors (slight perf cost). |
Dev | false | Enable the /doorhash and /finddoor setup commands. |
Housing | false | House-door keys via an external housing resource (leave off until you have one). |
Lockpicking a door
Using the lockpick item near a door runs the lockpick minigame; on success the door unlocks for everyone. Doors flagged lockpick = false show the BlacklistText instead. This is wired by default (Config.Lockpick.Doorlock.Resource = "spooni_libs").
Shop System
A full buy/sell shop with its own NUI is built directly into spooni_libs; no separate shop resource is required. Money, items and weapons all run through the framework bridge, so it works on every supported framework. Any script can open a shop for a player with one export, or you can place ready-made shops in the world from configstores.lua.
Defining a shop
One format is used everywhere (Config.Stores and the exports):
local def = {
id = 'blacksmith', -- required for RegisterShop, optional for an inline OpenShop
label = 'Blacksmith', -- title shown in the UI
currency = 'cash', -- default currency: 'cash' | 'gold'
buy = { -- what the player BUYS (the shop sells)
{ item = 'ammo_revolver', price = 12 },
{ 'ammo_rifle', 15 }, -- short form: { item, price }
{ item = 'WEAPON_REVOLVER_CATTLEMAN', price = 45, stock = 5, components = {} },
{ item = 'lockpick', price = { cash = 20, gold = 1 } }, -- multi-currency (all required)
{ item = 'fancy_hat', price = { items = { { name = 'pelt', amount = 3 } } } }, -- barter
},
sell = { -- what the player SELLS (the shop buys; pays out instantly)
{ item = 'pelt', price = 5 },
{ item = '*', price = 1 }, -- wildcard: anything else at a flat price
},
-- Optional location/trigger (omit it = the shop opens only through the exports):
ped = { model = 'cs_mp_gunsmith', coords = vec4(x, y, z, h), scenario = 'WORLD_HUMAN_...' },
blip = { sprite = 'blip_ambient_gunsmith', label = 'Blacksmith', scale = 0.2 },
trigger = 'prompt', -- 'prompt' | 'interaction' | 'none'
promptLabel = 'Open shop',
distance = 2.5,
meta = { ... }, -- free-form; echoed back in the OnPurchase hook
}| Buy field | Description |
|---|---|
item | Item or WEAPON_... name. |
price | A number (default currency), { cash =, gold = } (all required), or { items = { { name=, amount= } } } (barter). |
stock | Optional purchase limit. Omit / -1 = unlimited. |
components | Optional weapon components. |
metadata | Optional item metadata applied on purchase. |
label, image | Optional display overrides. |
Weapons can be bought in quantity — selecting e.g. ×5 delivers 5 separate weapons, each with its own generated serial (frameworks can't stack weapons). The SELL tab is hidden automatically when a shop has no
sellentries.
Opening a shop from a script
Server side — open a registered shop by id, or an inline (server-trusted) definition:
-- registered id:
exports["spooni_libs"]:RegisterShop(def) -- once, on start
exports["spooni_libs"]:OpenShop(src, 'blacksmith')
-- or an inline def built on the fly (e.g. filtered per player/grade):
exports["spooni_libs"]:OpenShop(src, def)Client side — open a shop that was registered with an id:
exports["spooni_libs"]:OpenShop('blacksmith')Purchase hook
Right after a successful buy/sell the server fires an event, so your script can log or react (spooni_libs never touches a database itself):
AddEventHandler("Spooni-Libs:Server:OnPurchase", function(src, data)
-- data = { shopId, item, label,
-- type = 'item' | 'weapon' | 'sell' | 'sell-weapon',
-- amount, price, currency, meta }
print(("%s bought %dx %s"):format(src, data.amount, data.label))
end)data.meta is whatever you passed as def.meta when opening the shop — handy for tagging which shop/location a purchase came from.
World shops (configstores.lua)
Config.EnableStores is the master switch for the shops listed in Config.Stores (the ones with a ped/blip/prompt). false = none spawn in the world, but the export API still works. Add your own entries (using the format above) to spawn standalone shops.
Config.EnableStores = true
Config.Stores = {
{ id = 'spooni_generalstore', label = 'General Store', trigger = 'prompt',
ped = { ... }, blip = { ... }, buy = { ... }, sell = { ... } },
}Language & money
The UI strings are translated in language.lua (EN/RO/DE/IT/FR/ES/PT) and follow Config.Language. Item icons come straight from the framework (SPOONILIBS.GetImagesInventory, which respects Config.ExternImagesInventory). Buy/sell feedback appears as a spooni_libs notification on the left while the shop is open. Money is cash or gold depending on the price you set.
NPC / Ped Manager
The Ped Manager allows you to easily register one or multiple NPCs. NPCs are automatically spawned when a player gets close and automatically removed when the player leaves the configured area.
Enable it first: set
Config.PedManager = trueinconfig.lua. TheregisterNpc/registerNpcs/unregisterNpcs/getStatsexports only exist when it is enabled — calling them while it'sfalsethrowsNo such export ... in resource spooni_libs.So a script should guard its calls with the always-available check:
luaif exports["spooni_libs"]:IsPedManagerEnabled() then exports["spooni_libs"]:registerNpcs(GetCurrentResourceName(), npcList, { ... }) end
IsPedManagerEnabled()exists whether the Ped Manager is on or off, so this never errors.
Register a Single NPC
exports["spooni_libs"]:registerNpc(resourceName, npcData, options)Parameters
| Parameter | Type | Description |
|---|---|---|
resourceName | string | Usually GetCurrentResourceName(). Used to automatically unregister all NPCs when the resource stops. |
npcData | table | NPC configuration table. |
options | table | Optional default settings. |
Example
exports["spooni_libs"]:registerNpc(GetCurrentResourceName(), {
id = "doctor",
model = "U_M_M_NBXDOCTOR_01",
x = -287.51,
y = 804.84,
z = 119.38,
heading = 90.0,
scenario = "WORLD_HUMAN_STAND_WAITING"
})Register Multiple NPCs
exports["spooni_libs"]:registerNpcs(resourceName, npcList, options)Example
exports["spooni_libs"]:registerNpcs(GetCurrentResourceName(), {
{
id = "doctor",
model = "U_M_M_NBXDOCTOR_01",
x = -287.51,
y = 804.84,
z = 119.38,
heading = 90.0,
scenario = "WORLD_HUMAN_STAND_WAITING"
},
{
id = "bartender",
model = "U_M_M_NBXBARTENDER_01",
x = -313.42,
y = 806.17,
z = 118.98,
heading = 180.0,
scenario = "WORLD_HUMAN_BARTENDER"
}
}, {
spawnDistance = 80.0,
despawnDistance = 120.0,
defaultOutfit = 0
})NPC Properties
| Property | Type | Required | Description |
|---|---|---|---|
id | string | No | Unique NPC identifier. If omitted, one is generated automatically. |
model | string | Yes | NPC model name. |
x | number | Yes | X coordinate. |
y | number | Yes | Y coordinate. |
z | number | Yes | Z coordinate. |
heading | number | No | NPC heading. |
spawnDistance | number | No | Distance required to spawn the NPC. |
despawnDistance | number | No | Distance required to despawn the NPC. |
outfit | number | No | Outfit preset ID. |
scenario | string | No | Scenario played after spawning. |
animDict | string | No | Animation dictionary. |
animName | string | No | Animation name. |
interaction | table | No | Murphy Interaction configuration. |
blip | table | No | Blip configuration. |
onSpawn | function | No | Executed after the NPC has spawned. |
onDespawn | function | No | Executed before the NPC is removed. |
onNearby | function | No | Executed while the player is inside the spawn distance. |
Default Options
The options table lets you define default values for every NPC.
{
spawnDistance = 80.0,
despawnDistance = 120.0,
defaultOutfit = 0
}NPC Blip
blip = {
name = "Doctor",
sprite = "blip_shop_doctor",
blipHash = 1664425300,
color = "BLIP_MODIFIER_MP_COLOR_2"
}| Property | Description |
|---|---|
name | Blip name shown on the map. |
sprite | Blip sprite. |
blipHash | Blip type hash. |
color | Optional blip modifier. |
NPC Animation
animDict = "amb_rest@world_human_smoke@male_a@idle_a"
animName = "idle_a"Use animDict and animName together to play a custom animation.
NPC Scenario
scenario = "WORLD_HUMAN_SMOKE"Scenarios are easier to use than animations and are recommended whenever possible.
Note: Use either
scenariooranimDict/animName, not both.
NPC Interaction
interaction = {
title = "Doctor",
distance = 5.0,
interactDst = 2.0,
options = {
{
label = "Open Doctor",
action = function()
print("Doctor menu opened")
end
}
}
}Callbacks
onSpawn
Executed immediately after the NPC has been created.
onSpawn = function(ped, npc)
print("NPC Spawned")
endonDespawn
Executed before the NPC is deleted.
onDespawn = function(npc)
print("NPC Removed")
endonNearby
Executed continuously while the player remains within the spawn distance.
onNearby = function(npc)
-- Your code here
endUnregister NPCs
Removes every NPC registered by the specified resource.
exports["spooni_libs"]:unregisterNpcs(GetCurrentResourceName())Statistics
Returns runtime information about the Ped Manager.
local stats = exports["spooni_libs"]:getStats()
print(stats.registeredResources)
print(stats.totalNPCs)
print(stats.activeNPCs)Returns:
| Field | Description |
|---|---|
registeredResources | Total registered resources. |
totalNPCs | Total registered NPCs. |
activeNPCs | NPCs currently spawned in the world. |
Framework Bridge (SPOONILIBS.Function(...))
Important Notes
Some functions behave differently depending on the selected framework.
For example:
- On VORP,
GiveGolduses currency type1. - On RSG,
GiveGoldusesbank. - On TPZ,
GiveGolduses account type1.
Always test your function on the framework you use before releasing a script.
Recommended Safe Usage
local amount = 100
if not source then
return
end
if amount <= 0 then
return
end
local newBalance = SPOONILIBS.GiveMoney(source, amount)
if not newBalance then
print("Failed to give money.")
else
print("New balance:", newBalance)
endUses
SPOONILIBS.GiveMoney(source, amount)as an example — this function isn't documented elsewhere in the source notes either; treat the signature as indicative until confirmed.
Players, Jobs, Permissions & Metadata
The source documentation for
Reviveis missing its description, syntax and parameters — onlyReturns/Examplesurvived from the original notes. Flagging until the missing part can be supplied.
Revive
Returns
nil
Example
SPOONILIBS.Revive(source)IsOnline
Checks if a player is online / loaded when supported by the framework.
Syntax
SPOONILIBS.IsOnline(source, identifier)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
identifier | string | Player identifier. |
Returns
boolean | nil
Example
local online = SPOONILIBS.IsOnline(source, identifier)Jobs & Permissions
GetJob
Returns the player's current job.
Syntax
SPOONILIBS.GetJob(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
string | false
Example
local job = SPOONILIBS.GetJob(source)GetJobGrade
Returns the player's current job grade.
Syntax
SPOONILIBS.GetJobGrade(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
number | string | false
Example
local grade = SPOONILIBS.GetJobGrade(source)SetJob
Sets the player's job and job grade.
Syntax
SPOONILIBS.SetJob(source, job, grade)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
job | string | Job name. |
grade | number | Job grade. |
Returns
boolean
Example
SPOONILIBS.SetJob(source, "police", 3)GetGroup
Returns the player's group or permission status.
Syntax
SPOONILIBS.GetGroup(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
string | boolean | false
Example
local group = SPOONILIBS.GetGroup(source, "admin")GetClan
Returns the player's clan data when supported by the framework.
Syntax
SPOONILIBS.GetClan(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
table | false | number
Example
local clan = SPOONILIBS.GetClan(source)Metadata
GetMetadata
Returns character metadata.
Syntax
SPOONILIBS.GetMetadata(source, charid)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
charid | any | Character identifier. |
Returns
table | false
Example
local metadata = SPOONILIBS.GetMetadata(source, charid)SetMetadata
Updates character metadata.
Syntax
SPOONILIBS.SetMetadata(source, charid, metadata)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
charid | any | Character identifier. |
metadata | table | Item or character metadata table. |
Returns
nil
Example
SPOONILIBS.SetMetadata(source, charid, metadata)Items & Inventory
RegisterUsable
Registers an item as usable and triggers a server event when used.
Syntax
SPOONILIBS.RegisterUsable(item, trigger, metadata)Parameters
| Parameter | Type | Description |
|---|---|---|
item | string | Item name. |
trigger | string | Server event triggered when the item is used. |
metadata | table | Item or character metadata table. |
Returns
nil
Example
SPOONILIBS.RegisterUsable("bread", "myScript:useBread")AddItem
Adds an item to the player's inventory.
Syntax
SPOONILIBS.AddItem(source, item, amount, metadata)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
amount | number | Amount / quantity. |
metadata | table | Item or character metadata table. |
Returns
nil
Example
SPOONILIBS.AddItem(source, "bread", 2, {})RemoveItem
Removes an item from the player's inventory.
Syntax
SPOONILIBS.RemoveItem(source, item, amount, metadata)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
amount | number | Amount / quantity. |
metadata | table | Item or character metadata table. |
Returns
nil
Example
SPOONILIBS.RemoveItem(source, "bread", 1, {})CheckItem
Checks how many items the player has.
Syntax
SPOONILIBS.CheckItem(source, item)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
Returns
number | false
Example
local count = SPOONILIBS.CheckItem(source, "bread")CanCarryItem
Checks if the player can carry a specific item amount.
Syntax
SPOONILIBS.CanCarryItem(source, item, amount)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
amount | number | Amount / quantity. |
Returns
boolean
Example
local canCarry = SPOONILIBS.CanCarryItem(source, "bread", 2)CanCarryItems
Checks if the player can carry items based on weight / framework limits.
Syntax
SPOONILIBS.CanCarryItems(source, item, amount)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
amount | number | Amount / quantity. |
Returns
boolean
Example
local canCarry = SPOONILIBS.CanCarryItems(source, "bread", 2)GetItem
Returns data for a specific item from the player's inventory.
Syntax
SPOONILIBS.GetItem(source, item)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
Returns
table | false
Example
local itemData = SPOONILIBS.GetItem(source, "bread")GetItemByMetadata
Returns an item that contains specific metadata.
Syntax
SPOONILIBS.GetItemByMetadata(source, item, metadata)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
metadata | table | Item or character metadata table. |
Returns
table | false
Example
local item = SPOONILIBS.GetItemByMetadata(source, "bread", {quality = 100})GetItemMainId
Returns an item by its unique main ID / item ID.
Syntax
SPOONILIBS.GetItemMainId(_source, mainid)Parameters
| Parameter | Type | Description |
|---|---|---|
_source | number | Player server ID. |
mainid | number | Unique item ID. |
Returns
table | false
Example
local item = SPOONILIBS.GetItemMainId(source, mainid)SetItemMetadata
Updates metadata for an inventory item.
Syntax
SPOONILIBS.SetItemMetadata(source, itemid, metadata, item)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
itemid | number | Unique item ID. |
metadata | table | Item or character metadata table. |
item | string | Item name. |
Returns
boolean | nil
Example
SPOONILIBS.SetItemMetadata(source, itemid, metadata, "bread")RemoveItemId
Removes a specific item by its unique ID.
Syntax
SPOONILIBS.RemoveItemId(_source, mainid)Parameters
| Parameter | Type | Description |
|---|---|---|
_source | number | Player server ID. |
mainid | number | Unique item ID. |
Returns
boolean | nil
Example
SPOONILIBS.RemoveItemId(source, mainid)GetInventory
Returns the full player inventory.
Syntax
SPOONILIBS.GetInventory(source, data)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
data | table | Inventory registration data table. |
Returns
table | false
Example
local inventory = SPOONILIBS.GetInventory(source)GetItemInventory
Returns item data / count from a custom inventory.
Syntax
SPOONILIBS.GetItemInventory(source, invid, itemName)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
invid | string | Custom inventory ID. |
itemName | string | Item name. |
Returns
table | number | false | nil
Example
local item = SPOONILIBS.GetItemInventory(source, "my_stash", "bread")GetItems
Returns the shared item list.
Syntax
SPOONILIBS.GetItems()Parameters
This function does not require parameters.
Returns
table
Example
local items = SPOONILIBS.GetItems()GetImagesInventory
Returns the NUI path used for inventory item images.
Syntax
SPOONILIBS.GetImagesInventory(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
string | nil
Example
local imagePath = SPOONILIBS.GetImagesInventory(source)Weapons & Custom Inventories
Weapons
AddWeapon
Adds a weapon to the player's inventory.
Syntax
SPOONILIBS.AddWeapon(source, weapon, ammo, serial, label, desc, components, comps)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
weapon | string | Weapon name. |
ammo | number | Weapon ammo amount. |
serial | string | Weapon serial number. |
label | string | Custom weapon label. |
desc | string | Custom weapon description. |
components | table | Weapon components table. |
comps | table | Weapon components / cosmetics table. |
Returns
nil
Example
SPOONILIBS.AddWeapon(source, "WEAPON_REVOLVER_CATTLEMAN", 50)GiveWeapon
Transfers a weapon from one player to another when supported.
Syntax
SPOONILIBS.GiveWeapon(source, weaponId, target)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
weaponId | number | Weapon unique ID. |
target | number | Target player server ID. |
Returns
boolean | nil
Example
SPOONILIBS.GiveWeapon(source, weaponId, target)RemoveWeapon
Removes a weapon from the player.
Syntax
SPOONILIBS.RemoveWeapon(source, weaponId)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
weaponId | number | Weapon unique ID. |
Returns
boolean | nil
Example
SPOONILIBS.RemoveWeapon(source, weaponId)GetWeapons
Returns the player's weapons.
Syntax
SPOONILIBS.GetWeapons(source)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
Returns
table
Example
local weapons = SPOONILIBS.GetWeapons(source)CanCarryWeapons
Checks if the player can carry weapons based on weight / framework limits.
Syntax
SPOONILIBS.CanCarryWeapons(source, item, amount)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
item | string | Item name. |
amount | number | Amount / quantity. |
Returns
boolean
Example
local canCarry = SPOONILIBS.CanCarryWeapons(source, "WEAPON_REVOLVER_CATTLEMAN", 1)Custom Inventories / Stashes
RegisterInventory
Registers a custom inventory / container.
Syntax
SPOONILIBS.RegisterInventory(source, data)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
data | table | Inventory registration data table. |
Returns
nil
Example
SPOONILIBS.RegisterInventory(source, data)OpenInventory
Opens a custom inventory / stash.
Syntax
SPOONILIBS.OpenInventory(source, stash, slots)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
stash | string | Custom inventory / stash ID. |
slots | number | Inventory slot count. |
Returns
nil
Example
SPOONILIBS.OpenInventory(source, "my_stash", 100)OpenPlayerInventory
Opens another player's inventory when supported.
Syntax
SPOONILIBS.OpenPlayerInventory(source, tsource)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
tsource | number | Target player server ID. |
Returns
nil
Example
SPOONILIBS.OpenPlayerInventory(source, target)CloseInventory
Closes the player's inventory.
Syntax
SPOONILIBS.CloseInventory(source, stash)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
stash | string | Custom inventory / stash ID. |
Returns
nil
Example
SPOONILIBS.CloseInventory(source, "my_stash")UpdateInventory
Updates custom inventory slots when supported.
Syntax
SPOONILIBS.UpdateInventory(source, stash, slots)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
stash | string | Custom inventory / stash ID. |
slots | number | Inventory slot count. |
Returns
nil
Example
SPOONILIBS.UpdateInventory(source, "my_stash", 100)BlackListCustomInv
Blacklists an item from a custom inventory when supported.
Syntax
SPOONILIBS.BlackListCustomInv(source, invId, item)Parameters
| Parameter | Type | Description |
|---|---|---|
source | number | Player server ID. |
invId | string | Custom inventory ID. |
item | string | Item name. |
Returns
nil
Example
SPOONILIBS.BlackListCustomInv(source, "my_stash", "bread")