Skip to content

ns-lib โ€‹

Cross-framework abstraction layer for FiveM and RedM resources.

  • Frameworks: VORP, RSG-Core, RedEM:RP, ESX, QBCore (auto-detected)
  • Inventory: ox_inventory, vorp_inventory, rsg-inventory, qb-inventory, redemrp_inventory
  • SQL: oxmysql, mysql-async
  • Notify: ox_lib โ†’ framework native โ†’ game native (auto fallback)
  • Discord: shared bot for every dependent script โ€” no per-script HTTP duplication

Support / community: discord.gg/UyyngemnF8


1. Setup โ€‹

Follow these steps in order โ€” every dependent script assumes ns-lib is already running.

1.1 Install โ€‹

  1. Drop this resource into your resources/ folder as ns-lib.
  2. Open your server.cfg and add ensure ns-lib before any script that depends on it.
  3. (Optional) Configure Discord โ€” see ยง7.
  4. Restart the server. On a healthy boot you should see:
    [ns-lib] v1.3.0 initializing...
    [ns-lib] framework=rsg | inventory=ox | sql=oxmysql
    [ns-lib] adapters loaded โœ“
    [ns-lib] discord helpers enabled (guild=...)   -- or "idle" if not configured

If detection fails, the resource stops itself with a loud red error โ€” fix the missing framework / SQL driver and restart.

1.2 Wire it into a dependent script โ€‹

In your script's fxmanifest.lua:

lua
dependency 'ns-lib'

shared_scripts {
    '@ns-lib/lib/init.lua',   -- MUST be first; loads NSLib namespace
    'config.lua',
    'shared/*.lua',
}

client_scripts { 'client/*.lua' }
server_scripts { 'server/*.lua' }

After this, NSLib.* is available everywhere (with the client/server split below). No framework-specific code needed.


2. Boot sequence โ€‹

What actually happens during startup, in order:

1. ns-lib starts (server)
   โ”œโ”€ shared/version.lua, shared/types.lua, shared/api.lua loaded
   โ”‚  โ†’ NSLib.* defined as "not implemented" stubs
   โ”œโ”€ server-config.lua reads convars (Discord)
   โ”œโ”€ server/detect.lua scans GetResourceState(...)
   โ”‚  โ†’ picks framework, inventory, sql
   โ”‚  โ†’ loads adapters/<kind>/<name>.lua into NSLib._fw / _inv / _db
   โ”œโ”€ adapter Init() hooks run (if defined)
   โ”œโ”€ NSLib.ready = true
   โ”œโ”€ TriggerEvent('ns-lib:ready', { framework, inventory, sql, version })
   โ””โ”€ Already-connected players receive 'ns-lib:client:info'

2. Dependent script starts
   โ”œโ”€ '@ns-lib/lib/init.lua' wires NSLib.* via exports['ns-lib']
   โ”‚  โ”œโ”€ server side โ†’ full API
   โ”‚  โ””โ”€ client side โ†’ events + framework info; mutating calls become "server-only" stubs
   โ””โ”€ Your script can now call NSLib.* freely

3. A player connects / spawns
   โ”œโ”€ Framework's own loaded event fires
   โ”œโ”€ Adapter fans out into NSLib._events.playerLoaded
   โ”œโ”€ NSLib.OnPlayerLoaded(...) callbacks run
   โ”œโ”€ TriggerEvent('ns-lib:playerLoaded', source, player)   -- proxy event
   โ””โ”€ TriggerClientEvent('ns-lib:client:info', source, ...) -- pushes detection info to client

Use AddEventHandler('ns-lib:ready', ...) if you need to defer setup until the bridge is fully online (rare โ€” the @-import already blocks until the resource started).


3. Client/server split โ€‹

CapabilityServerClient
Player / money / inventory / jobโœ…โŒ (calls error โ€” use TriggerServerEvent)
Database (Query, Execute, โ€ฆ)โœ…โŒ
Permissions (IsAdmin, HasGroup, HasAce)โœ…โŒ
Discord (GetDiscordId, GetDiscordRoles)โœ…โŒ
Notify(source, msg, type)โœ… (targets a player)โœ… (renders for self, ignores source arg)
Teleport(coords, opts)โ€”โœ…
TeleportPlayer(src, coords, opts)โœ…โŒ
Blip.*, Ped.* (RedM)โ€”โœ…
Events (OnPlayerLoaded, OnPlayerLogout, OnJobChange)โœ…โœ… (listen-only)
NSLib.framework / .inventory / .sqlโœ…โœ… (auto-pushed)

Calling a server-only function from the client raises a clear error: [ns-lib] NSLib.X is server-only. Trigger a server event from the client instead.


4. Events โ€‹

lua
-- Player joined and the framework reports them as fully loaded.
NSLib.OnPlayerLoaded(function(source, player)
    print(('Player loaded: %s (%s)'):format(player.name, player.identifier))
end)

-- Player disconnected (only the source is given โ€” Player record is gone).
NSLib.OnPlayerLogout(function(source) end)

-- Job changed (fired on SetJob and on framework's own job-change event).
NSLib.OnJobChange(function(source, newJob) end)

Equivalent standard events (for resources that don't @-import the lib):

lua
AddEventHandler('ns-lib:ready',         function(info) end)
AddEventHandler('ns-lib:playerLoaded',  function(source, player) end)
AddEventHandler('ns-lib:playerLogout',  function(source) end)
AddEventHandler('ns-lib:jobChange',     function(source, newJob) end)

5. API reference โ€‹

5.1 Player โ€‹

lua
NSLib.GetPlayer(source)       -- โ†’ Player | nil
NSLib.GetIdentifier(source)   -- โ†’ "steam:..." | "license:..." | nil  (raw account id)
NSLib.GetAllPlayers()         -- โ†’ Player[]
NSLib.IsLoaded(source)        -- โ†’ bool

Player.identifier is always the raw steam:xxx / license:xxx โ€” not VORP's charIdentifier (1, 2, 3โ€ฆ) which is a slot index. For the character primary key (cross-framework), use Player.charId:

FrameworkcharId source
VORPchar.charIdentifier
QBCore / RSGPlayerData.citizenid
ESXxPlayer.identifier
RedEM:RPuser:getIdentifier()

5.2 Money โ€‹

lua
NSLib.GetMoney(source, type)          -- type: 'cash' | 'bank' | 'gold' | 'rol'
NSLib.AddMoney(source, type, amount)
NSLib.RemoveMoney(source, type, amount)

5.3 Inventory โ€‹

lua
NSLib.AddItem(source, name, count, metadata?)
NSLib.RemoveItem(source, name, count, metadata?)
NSLib.GetItemCount(source, name)
NSLib.HasItem(source, name, count?)        -- count defaults to 1
NSLib.GetInventory(source)                 -- โ†’ Item[]
NSLib.RegisterUsableItem(name, callback)
NSLib.CanCarry(source, name, count)

ns-lib does not auto-register items. Each dependent script's README lists items it needs โ€” add them manually to your framework's item DB (ox_inventory/data/items.lua, vorp_inventory SQL, qb-core/shared/items.lua, โ€ฆ).

5.4 Job โ€‹

lua
NSLib.GetJob(source)                  -- โ†’ { name, grade, label }
NSLib.SetJob(source, name, grade)
NSLib.HasJob(source, name, minGrade?) -- โ†’ bool

5.5 Database (hybrid sync/async) โ€‹

Pass a callback for async, omit it for sync (waits via Citizen.Await).

lua
local rows = NSLib.Query('SELECT * FROM players WHERE id = ?', { id })

NSLib.Query('SELECT * FROM players', {}, function(rows)
    print(#rows)
end)

NSLib.QuerySingle(sql, params, cb?)  -- single row
NSLib.Scalar(sql, params, cb?)       -- first column of first row
NSLib.Execute(sql, params, cb?)      -- โ†’ affected rows
NSLib.Insert(sql, params, cb?)       -- โ†’ insertId

5.6 Notify โ€‹

lua
NSLib.Notify(source, message, type, duration?)
-- type: 'success' | 'error' | 'info' | 'warning'
-- duration in ms (optional)

Resolution chain: ox_lib โ†’ framework's native notification โ†’ game-native fallback.

5.7 Permissions (server-only) โ€‹

Three-layer resolution: console (source = 0) โ†’ CFX ace (group.X) โ†’ framework Player.group.

lua
NSLib.IsAdmin(source)            -- group.admin OR Player.group โˆˆ AdminGroups
NSLib.HasGroup(source, 'mod')    -- group.mod OR Player.group == 'mod'
NSLib.HasAce(source, 'command.ban')  -- shortcut around IsPlayerAceAllowed (console = true)

Extend the admin group set at runtime:

lua
NSLib.AdminGroups['supporter'] = true

Where Player.group comes from per framework:

  • VORP โ†’ char.group
  • QB / RSG โ†’ PlayerData.permission
  • ESX โ†’ xPlayer:getGroup()
  • RedEM โ†’ user:getGroup()

5.8 Teleport โ€‹

One-shot teleport with fade-out โ†’ freeze โ†’ collision stream-in โ†’ fade-in. Coordinates accept vector3, vector4, or { x, y, z, h?/w? }.

lua
-- CLIENT (teleport self)
NSLib.Teleport(vector4(-178.5, 631.5, 113.5, 90.0))

NSLib.Teleport(vector3(2641.5, -1037.4, 47.5), {
    heading   = 180.0,
    fade      = true,    -- screen fade (default true)
    fadeMs    = 600,     -- fade duration ms
    freeze    = true,    -- freeze until collision loaded (default true)
    timeoutMs = 5000,    -- max wait for collision
})

-- SERVER (teleport another player โ€” fires 'ns-lib:client:teleport' to target)
NSLib.TeleportPlayer(targetSrc, vector4(2641.5, -1037.4, 47.5, 180.0))
NSLib.TeleportPlayer(targetSrc, { x = 2641.5, y = -1037.4, z = 47.5 }, { heading = 180.0, fade = false })

5.9 Blip (RedM, client-only) โ€‹

Declarative wrapper over BlipAddForCoords / BlipAddForEntity / BlipAddForRadius โ€” no scattered SetBlipSprite/SetBlipName/SetBlipScale boilerplate. String hash names (e.g. 'blip_ambient_sheriff') are auto-joaat'd.

lua
-- Static map marker
local b = NSLib.Blip.Create({
    coords        = vector3(-178.5, 631.5, 113.5),
    sprite        = `blip_ambient_sheriff`,
    name          = 'Valentine Sheriff',
    scale         = 0.9,
    extraModifier = `BLIP_MODIFIER_LAW_DEFAULT`,
})

-- Attached to an entity (moves with the ped/horse/vehicle)
local headBlip = NSLib.Blip.CreateForEntity({
    entity = somePed,
    sprite = `blip_ambient_bounty_target`,
    name   = 'Wanted Outlaw',
})

-- Radius circle on the map (RDR Online "search area" style)
local areaBlip = NSLib.Blip.CreateRadius({
    coords = vector3(2641.5, -1037.4, 47.5),
    radius = 180.0,
    sprite = `blip_mission_area_bounty`,
})

-- Update / extend / remove
NSLib.Blip.Update(b, { name = 'New Name', scale = 1.2, flashes = true })
NSLib.Blip.AddModifier(b, `BLIP_MODIFIER_USE_HEADING_INDICATOR`)
NSLib.Blip.Remove(b)                          -- nil-safe
myBlipList = NSLib.Blip.RemoveAll(myBlipList)

All Create* opts: coords, entity, radius, sprite, name, scale, modifier, extraModifier, extraModifiers, colour, flashes, shortRange, priority.

5.10 Ped (RedM, client-only) โ€‹

lua
-- Static NPC (vendor, quest giver, โ€ฆ)
local vendor = NSLib.Ped.Spawn({
    model         = `cs_mp_jackmarston`,
    coords        = vector4(-178.5, 631.5, 113.5, 90.0),
    freeze        = true,
    invincible    = true,
    blockEvents   = true,
    noFlee        = true,
    noTarget      = true,
    placeOnGround = true,
})

-- Hostile / target ped (armed, AI free)
local enemy = NSLib.Ped.Spawn({
    model   = `g_m_m_unidustergang_01`,
    coords  = pos,
    heading = math.random(0, 359) + 0.0,
    weapon  = `WEAPON_REVOLVER_CATTLEMAN`,
    ammo    = 100,
})

NSLib.Ped.Update(vendor, { freeze = false, invincible = false })
NSLib.Ped.Delete(vendor)                       -- safe DeletePed with mission flag
myEnemies = NSLib.Ped.DeleteAll(myEnemies)

-- Just load a model (useful outside Spawn for CreateObject etc.)
if NSLib.Ped.LoadModel(`p_campfire01x`, 5000) then
    -- ...
end

Spawn opts: model, coords (vector3/vector4/table), heading, network, mission, freeze, invincible, health, blockEvents, noFlee, noTarget, noRagdoll, relationGroup, weapon, ammo, scenario, placeOnGround, releaseModel, loadTimeoutMs. The same opts apply to Update().

Client-only. These natives don't exist on the server โ€” calling them from server code throws "attempt to call nil". Use TriggerClientEvent to reach the player.


6. Usage example โ€‹

lua
-- server/main.lua
NSLib.OnPlayerLoaded(function(source, player)
    print(('Player loaded: %s (%s)'):format(player.name, player.identifier))
end)

function GiveWine(source, quality)
    if not NSLib.HasItem(source, 'empty_bottle', 1) then
        return NSLib.Notify(source, 'You need an empty bottle', 'error')
    end
    NSLib.RemoveItem(source, 'empty_bottle', 1)
    NSLib.AddItem(source, 'wine', 1, { quality = quality })
    NSLib.Notify(source, 'Wine bottled', 'success')
end

7. Discord (server-only) โ€‹

Bot token + guild ID are read from server convars (ns_lib_discord_token, ns_lib_discord_guild) โ€” set them once in server.cfg and every dependent script gets Discord access for free. The token stays out of git history and never reaches the client.

7.1 API โ€‹

lua
NSLib.GetDiscordId(source)
-- โ†’ "315214743864344586" or nil if the player hasn't linked Discord to FiveM.

NSLib.GetDiscordRoles(source, function(roleIds, err)
    -- roleIds : array of role-ID strings (snowflakes), [] when not in guild or err set
    -- err     : nil | 'no_discord_id' | 'auth' | 'parse' | 'network' | 'http_<status>' | 'disabled'
end)

7.2 Setup checklist โ€‹

  1. Create a bot: https://discord.com/developers/applications โ†’ New Application โ†’ Bot โ†’ Reset Token

  2. Enable Server Members Intent under the Bot tab โ†’ Privileged Gateway Intents (REQUIRED โ€” without it body.roles is missing).

  3. Invite the bot: OAuth2 โ†’ URL Generator โ†’ scope bot, perm Read Messages โ†’ open the URL โ†’ add to your guild.

  4. Discord Settings โ†’ Advanced โ†’ enable Developer Mode.

  5. Right-click your server โ†’ Copy Server ID.

  6. Add the convars to your server.cfg (use set, NOT setr โ€” set keeps them server-only and never replicates to clients):

    cfg
    set ns_lib_discord_enabled "true"
    set ns_lib_discord_token   "YOUR_BOT_TOKEN"
    set ns_lib_discord_guild   "YOUR_GUILD_ID"

After restart, the console stays silent on success. If Enabled=true but either secret is missing, ns-lib prints a loud red warning at boot:

[ns-lib] Discord enabled but ns_lib_discord_token / ns_lib_discord_guild not set in server.cfg โ€” Discord helpers will fail.

To disable Discord entirely without removing convars, set ns_lib_discord_enabled "false".

7.3 Mapping role IDs to keys โ€‹

The library returns raw role IDs. Each script decides how to map them to its own role-key vocabulary (member / vip / staff / โ€ฆ).

lua
-- your_script/config.lua
Config.Roles = {
    member = '1089999246914232381',
    vip    = '1324526243689009236',
}

-- your_script/server/foo.lua
local function MapRoles(rawIds)
    local found = {}
    for _, id in ipairs(rawIds) do
        for key, configured in pairs(Config.Roles) do
            if id == configured then found[key] = true end
        end
    end
    return found
end

local function OnSomeEvent(source)
    NSLib.GetDiscordRoles(source, function(roles, err)
        if err == 'no_discord_id' then
            return NSLib.Notify(source, 'Link your Discord first', 'error')
        elseif err then
            return NSLib.Notify(source, 'Discord check failed: ' .. err, 'error')
        end
        local has = MapRoles(roles)
        if has.vip then
            -- grant VIP perks
        end
    end)
end

7.4 Notes โ€‹

  • No caching โ€” every call hits Discord directly. Role changes show up live. Bot's global rate limit is ~50 req/s, comfortable for any normal server population.
  • A 404 (player isn't in the guild) returns err = nil, roles = {} โ€” not an error. Treat empty roles as "no privileges".
  • The HTTP request is async; NSLib.GetDiscordRoles always uses a callback. There is no sync wrapper.

8. Admin & versioning โ€‹

/lib-status     -- prints detected framework / inventory / sql, lists mounted adapters

Pin a minimum major version in your script's init code:

lua
NSLib.RequireMinVersion(1)   -- errors if NSLib.VERSION < 1.x

9. Caveats โ€‹

  • No standalone fallback. ns-lib requires one of the supported frameworks. If none is detected at startup, the resource errors out and stops.
  • Hot reload. Restarting ns-lib invalidates NSLib references in dependent scripts because @-import runs once on resource start. Restart all dependent scripts after restarting ns-lib.
  • Items are not auto-registered. See ยง5.3.

License โ€‹

MIT

Released under the MIT License.