Skip to content

Architecture

bs_core owns state. Money, XP, needs, position, character data: only bs_core writes them, and only bs_core saves the row. Every other resource asks bs_core through exports. That’s what keeps saves honest, there is one place that marks a character dirty and one place that writes it.

NUI (html in each resource, shared shell in bs_ui)
| SendNUIMessage / fetch (NUI callbacks)
Client Lua mirrors state, never authoritative
| net events + BSB callbacks
Server Lua Player objects, validation, hooks
| oxmysql (behind bs_core/server/sv_database.lua)
MariaDB migrations in bs_core/sql

The client asks, the server decides, the server sends the result back. Everything a client sends is checked again on the server. Needs for example can only go down from a client report, restoring hunger is a server action from a consumable.

GroupResources
Corebs_core, bs_loading, bs_multichar, bs_esx_bridge
UIbs_ui (HUD, notify, shared NUI bits), bs_radial, bs_menu (hub), bs_chat, bs_scoreboard
Playerbs_inventory, bs_skills, bs_medical, bs_bodily, bs_emotes, bs_boosters, bs_gadgets, bs_drugs
Worldbs_world, bs_interactions (ALT target dot, containers), bs_economy (loot), bs_traders, bs_vehicles, bs_building, bs_stash, bs_recycler, bs_mining, bs_scenes, bs_events, bs_extraction
Zombiesbs_zombies, bs_zombiepeds, bs_zombiesfx, bs_atlas, nodes
Socialbs_factions, bs_faction_bases, bs_vip
Staffbs_admin, bs_config, bs_guard, bs_cinecam
Assetsbs_assets
connecting -> playerConnecting: identity resolved, ban checked
loading -> client handshake (bs:session:clientReady)
select -> limbo, frozen and invisible, character list/create/delete/select
playing -> Player object exists server side, ped spawned

/logout and a disconnect both go through BS.UnloadPlayer, which force-saves first. Client side, exports.bs_core:GetSessionState() tells you where you are, and exports.bs_core:IsLoaded() is the simple “is there a character” check.

The Player object lives in bs_core’s Lua state. Functions don’t survive the export boundary, so you don’t get the object, you call exports that do the thing:

-- server
local p = exports.bs_core:GetPlayerSummary(src) -- plain table: charId, name, level, xp, accounts, needs, role...
exports.bs_core:AddMoney(src, 'cash', 50, 'my_reason')
exports.bs_core:RemoveMoney(src, 'bank', 200, 'my_reason')
exports.bs_core:AddXP(src, 25, 'my_reason')
local v = exports.bs_core:GetPlayerMeta(src, 'my_key', nil)
exports.bs_core:SetPlayerMeta(src, 'my_key', { any = 'table' })
exports.bs_core:AwardAction(src, 'zombie_kill') -- uses Config.Rewards

Client side: exports.bs_core:GetData(), GetNeeds(), IsLoaded().

Character metadata (GetPlayerMeta/SetPlayerMeta) is the easy place for small per-character data. bs_drugs keeps addiction there with no table of its own.

bs_core has an in-process pub/sub (BS.On / BS.Emit) and re-broadcasts every hook as a normal event called bs:hook:<name>. From your resource:

AddEventHandler('bs:hook:player:loaded', function(data) ... end)
AddEventHandler('bs:hook:player:unloaded', function(...) ... end)
HookSide
core:readyboth
player:loaded, player:spawned, player:unloadedboth
player:money, player:levelUpboth
player:xp, player:actionserver
character:created, character:selectedserver
session:state, needs:changedclient

Request/response over net events. Include the file in your manifest and you get BSB in your own Lua state:

shared_scripts { '@bs_core/shared/sh_bridge.lua' }
-- server
BSB.RegisterCallback('mything:get', function(src, a, b)
return { ok = true, value = a + b }
end)
-- client
local res = BSB.Await('mything:get', 5000, 1, 2) -- blocks this thread, nil on timeout

The other way, server asks one client:

-- client
BSB.RegisterClientCallback('mything:where', function() return GetEntityCoords(PlayerPedId()) end)
-- server
local pos = BSB.AwaitClient(src, 'mything:where', 3000)

How it works: a request is broadcast to every resource and the one that registered the name answers, the rest stay quiet. So:

  • an unknown or stopped name times out, it doesn’t error
  • names are global, prefix them with your system (squad:invite, factions:roster, vendor:buy)
  • the server rate-limits each client to 40 requests per second per resource
  • a handler that throws prints callback "x" errored and the client gets nil

The convention for returns is a table with ok, and error when it fails: { ok = false, error = 'Not in a squad.' }.

There’s also BSB.RegisterCommand(name, ace, help, args, handler) (ACE check, pcall, chat suggestion in one) and BSB.Radial(spec) on the client for the radial wheel. Use those instead of rolling your own.

@bs_core/shared/sh_services.lua gives BS.Provide / BS.Use. It’s a checked way to call exports: the provider says what it offers, the consumer gets a handle or a loud log line, never a silent nil.

local inv = BS.Use('inventory')
if inv then inv.GiveItem(src, 'bandage', 1) end

It’s new, most code still calls exports directly. Both work.

BagOnSet byMeaning
bsDownedplayerbs_medicalplayer is down
bsDead, bsRevivableplayerbs_medical
bsDrugModsplayer (local)bs_drugsdrain, regen, cap, infection, sway
bsSteadyplayer (local)bs_boostersrecoil/sway multiplier
bsDroneplayerbs_gadgetspiloting a drone
bsSquadplayerbs_factionssquad info
bsFactionStyle, bsFactionTag, bsFactionCanInvite, bsFactionSafeZoneplayerbs_factionstag style, permissions, safe zone
bsVipTagplayerbs_vip{ tier, colour, style, hex, blip, hud }, nil without VIP
bsInRaid, bsRogueplayerbs_extractionPvP zone state
bsSittingplayerbs_emotes
bsZombieentitybs_zombieswalker record id. Also zType, zGait, zState… in networked mode
bsZombieDeadentitybs_zombiesnetworked corpse
bsCorpseentitybs_medicalplayer body
bsProtected, bsKeepentitymanybs_world’s cleanup sweep skips it
bsVehicle, bsLocked, bsOwned, bsWindowsDownvehiclebs_vehicles
bsBuiltentitybs_building

If you spawn an entity that must not get cleaned up by bs_world, set bsProtected on it.

bs:<system>:<thing>, lower camel case at the end:

bs:session:clientReady bs:characters:select bs:player:money
bs:inventory:state bs:squad:sync bs:z:add
bs:ui:notify bs:ui:opened bs:medical:died

bs_core keeps its own names in BS.Events (bs_core/shared/sh_events.lua) so nothing hardcodes them. Hooks are bs:hook:*. Callback names use system:action without the bs: prefix.

Every event and callback per resource is in the API reference.