Skip to content

📚 Libs

Documentation relating to the spooni_libs.

1. Installation

spooni_libs works Standalone.

To install spooni_libs:

  • Download the resource
  • 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.

lua
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

lua
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)

lua
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)

lua
exports["spooni_libs"]:AddNetEntity({
    netId = netId,
    options = { ... }
})

Every ped in the world (global ped options)

lua
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

lua
exports["spooni_libs"]:AddGlobalPlayerInteraction({
    options = { { label = "Give item", serverEvent = "myres:giveItem" } }
})

exports["spooni_libs"]:AddGlobalVehicleInteraction({
    options = { { label = "Inspect", onSelect = function(_, veh) end } }
})

Option fields

FieldDescription
label / textText shown in the menu row.
distance / interactDstPer-option distance limit (optional).
groups / group / job / jobsJob/grade gate. { police = 2 } = police grade ≥ 2, or "police" / { "police", "sheriff" }.
items / itemItem 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.
serverEventServer event triggered on confirm (args, serverId).
eventClient event triggered on confirm.

Managing interactions

lua
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 interaction

On-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(...).

lua
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
end

The 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 coords and hides if you look away from that spot (like the ALT pins). If you omit coords it falls back to a screen-centred menu and the optional title is 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:

lua
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 SetInteractionItems yourself, or use a canInteract callback that checks the item server-side. Options without items always 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, ...).

lua
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)
end

serverId is false when nothing was picked; reason then tells you why:

ReasonMeaning
"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_los note: 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. Pass ignore_los = true (and usually ignore_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.

lua
-- 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
end

Placement 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):

lua
-- 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:

lua
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 everyone

Stream distances are in config.luaConfig.PropPlacer: SpawnDistance (spawn a prop when a player gets this close, default 80) and DespawnDistance (despawn it when they get this far, must be > SpawnDistance to avoid flicker).

Persistence is optional in the same block: SaveToDatabase (default false). Left false, props are in-memory only and gone on a server restart. Set it true to save placed props to a DB table (DatabaseTable, default spooni_placed_props) so they survive restarts and other resources can read them; the table is created automatically and props are reloaded on start.

PlaceProp returns true once the prop is registered, or false + 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:

lua
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 = false to 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 / Announce also take an optional last argument loadingBar (true/false) to force the sweep on or off for that one call. Omit it (or pass nil) to use Config.NotifyLoadingBar:

lua
exports["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 = false to drop the weathered-paper background — notifications and announcements then render as text only (with a shadow so it stays readable). true keeps the paper background.

Notify / Announce take an optional final argument background (true/false) after loadingBar, to force the background on or off for that one call. Omit it (or pass nil) to use Config.NotifyBackground:

lua
exports["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:

  1. UI/img/<name>.png (inside spooni_libs)
  2. the framework inventory image path (e.g. nui://vorp_inventory/html/img/items/<name>.png)
  3. 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:

lua
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):

lua
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.

lua
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")
end

Progress Bar

The default NUI progress bar, with an optional animation and attached prop.

lua
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 to 0 to 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.

lua
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 with exports["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:

lua
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.

lua
-- 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
end

How 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 Padlock call while one is already running returns false immediately.

The two safecrack animations are configurable in config.lua:

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.

lua
-- 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")
end

How 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:

PresetFeelPinsPin healthPin damageSweet spot
loweasy43206wide (13)
mediuma bit harder3220108
hardhard2150155
veryhardvery hard111022narrow (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:

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:

  1. If a door system is configured (Config.Lockpick.Doorlock.Resource exports GetDoor()), it tries the nearest door first — "BLACKLIST" → notify, a door → open the minigame and fire Doorlock.OpenEvent on success.
  2. 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 calls Lockpick() under the hood).
lua
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 same spooni_libs minigame. The handcuff path needs Config.Lockpick = true in 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 at UI/img/padlock.png and 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 world coords, its doors = { [doorHash] = {x, y, z} }, and islock (locked by default?).
  • Config.DoorsOptions — per-door rules: { jobs = {...}, items = {...} | false, lockpick = true | false }. jobs / items decide who can toggle it with ALT; lockpick = false blacklists 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)

OptionDefaultMeaning
EnabledtrueMaster switch. false = the door system doesn't run at all.
Distance2.0How close you must be to a door to toggle it.
ShowIcontrueDraw the lock/unlock icon on doors (slight perf cost).
DevfalseEnable the /doorhash and /finddoor setup commands.
HousingfalseHouse-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):

lua
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 fieldDescription
itemItem or WEAPON_... name.
priceA number (default currency), { cash =, gold = } (all required), or { items = { { name=, amount= } } } (barter).
stockOptional purchase limit. Omit / -1 = unlimited.
componentsOptional weapon components.
metadataOptional item metadata applied on purchase.
label, imageOptional 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 sell entries.

Opening a shop from a script

Server side — open a registered shop by id, or an inline (server-trusted) definition:

lua
-- 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:

lua
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):

lua
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.

lua
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 = true in config.lua. The registerNpc / registerNpcs / unregisterNpcs / getStats exports only exist when it is enabled — calling them while it's false throws No such export ... in resource spooni_libs.

So a script should guard its calls with the always-available check:

lua
if 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

lua
exports["spooni_libs"]:registerNpc(resourceName, npcData, options)

Parameters

ParameterTypeDescription
resourceNamestringUsually GetCurrentResourceName(). Used to automatically unregister all NPCs when the resource stops.
npcDatatableNPC configuration table.
optionstableOptional default settings.

Example

lua
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

lua
exports["spooni_libs"]:registerNpcs(resourceName, npcList, options)

Example

lua
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

PropertyTypeRequiredDescription
idstringNoUnique NPC identifier. If omitted, one is generated automatically.
modelstringYesNPC model name.
xnumberYesX coordinate.
ynumberYesY coordinate.
znumberYesZ coordinate.
headingnumberNoNPC heading.
spawnDistancenumberNoDistance required to spawn the NPC.
despawnDistancenumberNoDistance required to despawn the NPC.
outfitnumberNoOutfit preset ID.
scenariostringNoScenario played after spawning.
animDictstringNoAnimation dictionary.
animNamestringNoAnimation name.
interactiontableNoMurphy Interaction configuration.
bliptableNoBlip configuration.
onSpawnfunctionNoExecuted after the NPC has spawned.
onDespawnfunctionNoExecuted before the NPC is removed.
onNearbyfunctionNoExecuted while the player is inside the spawn distance.

Default Options

The options table lets you define default values for every NPC.

lua
{
    spawnDistance = 80.0,
    despawnDistance = 120.0,
    defaultOutfit = 0
}

NPC Blip

lua
blip = {

    name = "Doctor",

    sprite = "blip_shop_doctor",

    blipHash = 1664425300,

    color = "BLIP_MODIFIER_MP_COLOR_2"

}
PropertyDescription
nameBlip name shown on the map.
spriteBlip sprite.
blipHashBlip type hash.
colorOptional blip modifier.

NPC Animation

lua
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

lua
scenario = "WORLD_HUMAN_SMOKE"

Scenarios are easier to use than animations and are recommended whenever possible.

Note: Use either scenario or animDict/animName, not both.


NPC Interaction

lua
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.

lua
onSpawn = function(ped, npc)

    print("NPC Spawned")

end

onDespawn

Executed before the NPC is deleted.

lua
onDespawn = function(npc)

    print("NPC Removed")

end

onNearby

Executed continuously while the player remains within the spawn distance.

lua
onNearby = function(npc)

    -- Your code here

end

Unregister NPCs

Removes every NPC registered by the specified resource.

lua
exports["spooni_libs"]:unregisterNpcs(GetCurrentResourceName())

Statistics

Returns runtime information about the Ped Manager.

lua
local stats = exports["spooni_libs"]:getStats()

print(stats.registeredResources)
print(stats.totalNPCs)
print(stats.activeNPCs)

Returns:

FieldDescription
registeredResourcesTotal registered resources.
totalNPCsTotal registered NPCs.
activeNPCsNPCs currently spawned in the world.

Framework Bridge (SPOONILIBS.Function(...))

Important Notes

Some functions behave differently depending on the selected framework.

For example:

  • On VORP, GiveGold uses currency type 1.
  • On RSG, GiveGold uses bank.
  • On TPZ, GiveGold uses account type 1.

Always test your function on the framework you use before releasing a script.

lua
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)
end

Uses 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 Revive is missing its description, syntax and parameters — only Returns/Example survived from the original notes. Flagging until the missing part can be supplied.

Revive

Returns

nil

Example

lua
SPOONILIBS.Revive(source)

IsOnline

Checks if a player is online / loaded when supported by the framework.

Syntax

lua
SPOONILIBS.IsOnline(source, identifier)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
identifierstringPlayer identifier.

Returns

boolean | nil

Example

lua
local online = SPOONILIBS.IsOnline(source, identifier)

Jobs & Permissions

GetJob

Returns the player's current job.

Syntax

lua
SPOONILIBS.GetJob(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

string | false

Example

lua
local job = SPOONILIBS.GetJob(source)

GetJobGrade

Returns the player's current job grade.

Syntax

lua
SPOONILIBS.GetJobGrade(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

number | string | false

Example

lua
local grade = SPOONILIBS.GetJobGrade(source)

SetJob

Sets the player's job and job grade.

Syntax

lua
SPOONILIBS.SetJob(source, job, grade)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
jobstringJob name.
gradenumberJob grade.

Returns

boolean

Example

lua
SPOONILIBS.SetJob(source, "police", 3)

GetGroup

Returns the player's group or permission status.

Syntax

lua
SPOONILIBS.GetGroup(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

string | boolean | false

Example

lua
local group = SPOONILIBS.GetGroup(source, "admin")

GetClan

Returns the player's clan data when supported by the framework.

Syntax

lua
SPOONILIBS.GetClan(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

table | false | number

Example

lua
local clan = SPOONILIBS.GetClan(source)

Metadata

GetMetadata

Returns character metadata.

Syntax

lua
SPOONILIBS.GetMetadata(source, charid)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
charidanyCharacter identifier.

Returns

table | false

Example

lua
local metadata = SPOONILIBS.GetMetadata(source, charid)

SetMetadata

Updates character metadata.

Syntax

lua
SPOONILIBS.SetMetadata(source, charid, metadata)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
charidanyCharacter identifier.
metadatatableItem or character metadata table.

Returns

nil

Example

lua
SPOONILIBS.SetMetadata(source, charid, metadata)

Items & Inventory

RegisterUsable

Registers an item as usable and triggers a server event when used.

Syntax

lua
SPOONILIBS.RegisterUsable(item, trigger, metadata)

Parameters

ParameterTypeDescription
itemstringItem name.
triggerstringServer event triggered when the item is used.
metadatatableItem or character metadata table.

Returns

nil

Example

lua
SPOONILIBS.RegisterUsable("bread", "myScript:useBread")

AddItem

Adds an item to the player's inventory.

Syntax

lua
SPOONILIBS.AddItem(source, item, amount, metadata)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
amountnumberAmount / quantity.
metadatatableItem or character metadata table.

Returns

nil

Example

lua
SPOONILIBS.AddItem(source, "bread", 2, {})

RemoveItem

Removes an item from the player's inventory.

Syntax

lua
SPOONILIBS.RemoveItem(source, item, amount, metadata)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
amountnumberAmount / quantity.
metadatatableItem or character metadata table.

Returns

nil

Example

lua
SPOONILIBS.RemoveItem(source, "bread", 1, {})

CheckItem

Checks how many items the player has.

Syntax

lua
SPOONILIBS.CheckItem(source, item)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.

Returns

number | false

Example

lua
local count = SPOONILIBS.CheckItem(source, "bread")

CanCarryItem

Checks if the player can carry a specific item amount.

Syntax

lua
SPOONILIBS.CanCarryItem(source, item, amount)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
amountnumberAmount / quantity.

Returns

boolean

Example

lua
local canCarry = SPOONILIBS.CanCarryItem(source, "bread", 2)

CanCarryItems

Checks if the player can carry items based on weight / framework limits.

Syntax

lua
SPOONILIBS.CanCarryItems(source, item, amount)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
amountnumberAmount / quantity.

Returns

boolean

Example

lua
local canCarry = SPOONILIBS.CanCarryItems(source, "bread", 2)

GetItem

Returns data for a specific item from the player's inventory.

Syntax

lua
SPOONILIBS.GetItem(source, item)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.

Returns

table | false

Example

lua
local itemData = SPOONILIBS.GetItem(source, "bread")

GetItemByMetadata

Returns an item that contains specific metadata.

Syntax

lua
SPOONILIBS.GetItemByMetadata(source, item, metadata)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
metadatatableItem or character metadata table.

Returns

table | false

Example

lua
local item = SPOONILIBS.GetItemByMetadata(source, "bread", {quality = 100})

GetItemMainId

Returns an item by its unique main ID / item ID.

Syntax

lua
SPOONILIBS.GetItemMainId(_source, mainid)

Parameters

ParameterTypeDescription
_sourcenumberPlayer server ID.
mainidnumberUnique item ID.

Returns

table | false

Example

lua
local item = SPOONILIBS.GetItemMainId(source, mainid)

SetItemMetadata

Updates metadata for an inventory item.

Syntax

lua
SPOONILIBS.SetItemMetadata(source, itemid, metadata, item)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemidnumberUnique item ID.
metadatatableItem or character metadata table.
itemstringItem name.

Returns

boolean | nil

Example

lua
SPOONILIBS.SetItemMetadata(source, itemid, metadata, "bread")

RemoveItemId

Removes a specific item by its unique ID.

Syntax

lua
SPOONILIBS.RemoveItemId(_source, mainid)

Parameters

ParameterTypeDescription
_sourcenumberPlayer server ID.
mainidnumberUnique item ID.

Returns

boolean | nil

Example

lua
SPOONILIBS.RemoveItemId(source, mainid)

GetInventory

Returns the full player inventory.

Syntax

lua
SPOONILIBS.GetInventory(source, data)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
datatableInventory registration data table.

Returns

table | false

Example

lua
local inventory = SPOONILIBS.GetInventory(source)

GetItemInventory

Returns item data / count from a custom inventory.

Syntax

lua
SPOONILIBS.GetItemInventory(source, invid, itemName)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
invidstringCustom inventory ID.
itemNamestringItem name.

Returns

table | number | false | nil

Example

lua
local item = SPOONILIBS.GetItemInventory(source, "my_stash", "bread")

GetItems

Returns the shared item list.

Syntax

lua
SPOONILIBS.GetItems()

Parameters

This function does not require parameters.

Returns

table

Example

lua
local items = SPOONILIBS.GetItems()

GetImagesInventory

Returns the NUI path used for inventory item images.

Syntax

lua
SPOONILIBS.GetImagesInventory(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

string | nil

Example

lua
local imagePath = SPOONILIBS.GetImagesInventory(source)

Weapons & Custom Inventories
Weapons

AddWeapon

Adds a weapon to the player's inventory.

Syntax

lua
SPOONILIBS.AddWeapon(source, weapon, ammo, serial, label, desc, components, comps)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
weaponstringWeapon name.
ammonumberWeapon ammo amount.
serialstringWeapon serial number.
labelstringCustom weapon label.
descstringCustom weapon description.
componentstableWeapon components table.
compstableWeapon components / cosmetics table.

Returns

nil

Example

lua
SPOONILIBS.AddWeapon(source, "WEAPON_REVOLVER_CATTLEMAN", 50)

GiveWeapon

Transfers a weapon from one player to another when supported.

Syntax

lua
SPOONILIBS.GiveWeapon(source, weaponId, target)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
weaponIdnumberWeapon unique ID.
targetnumberTarget player server ID.

Returns

boolean | nil

Example

lua
SPOONILIBS.GiveWeapon(source, weaponId, target)

RemoveWeapon

Removes a weapon from the player.

Syntax

lua
SPOONILIBS.RemoveWeapon(source, weaponId)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
weaponIdnumberWeapon unique ID.

Returns

boolean | nil

Example

lua
SPOONILIBS.RemoveWeapon(source, weaponId)

GetWeapons

Returns the player's weapons.

Syntax

lua
SPOONILIBS.GetWeapons(source)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.

Returns

table

Example

lua
local weapons = SPOONILIBS.GetWeapons(source)

CanCarryWeapons

Checks if the player can carry weapons based on weight / framework limits.

Syntax

lua
SPOONILIBS.CanCarryWeapons(source, item, amount)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
itemstringItem name.
amountnumberAmount / quantity.

Returns

boolean

Example

lua
local canCarry = SPOONILIBS.CanCarryWeapons(source, "WEAPON_REVOLVER_CATTLEMAN", 1)

Custom Inventories / Stashes

RegisterInventory

Registers a custom inventory / container.

Syntax

lua
SPOONILIBS.RegisterInventory(source, data)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
datatableInventory registration data table.

Returns

nil

Example

lua
SPOONILIBS.RegisterInventory(source, data)

OpenInventory

Opens a custom inventory / stash.

Syntax

lua
SPOONILIBS.OpenInventory(source, stash, slots)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
stashstringCustom inventory / stash ID.
slotsnumberInventory slot count.

Returns

nil

Example

lua
SPOONILIBS.OpenInventory(source, "my_stash", 100)

OpenPlayerInventory

Opens another player's inventory when supported.

Syntax

lua
SPOONILIBS.OpenPlayerInventory(source, tsource)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
tsourcenumberTarget player server ID.

Returns

nil

Example

lua
SPOONILIBS.OpenPlayerInventory(source, target)

CloseInventory

Closes the player's inventory.

Syntax

lua
SPOONILIBS.CloseInventory(source, stash)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
stashstringCustom inventory / stash ID.

Returns

nil

Example

lua
SPOONILIBS.CloseInventory(source, "my_stash")

UpdateInventory

Updates custom inventory slots when supported.

Syntax

lua
SPOONILIBS.UpdateInventory(source, stash, slots)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
stashstringCustom inventory / stash ID.
slotsnumberInventory slot count.

Returns

nil

Example

lua
SPOONILIBS.UpdateInventory(source, "my_stash", 100)

BlackListCustomInv

Blacklists an item from a custom inventory when supported.

Syntax

lua
SPOONILIBS.BlackListCustomInv(source, invId, item)

Parameters

ParameterTypeDescription
sourcenumberPlayer server ID.
invIdstringCustom inventory ID.
itemstringItem name.

Returns

nil

Example

lua
SPOONILIBS.BlackListCustomInv(source, "my_stash", "bread")