Architecture
The one rule
Section titled “The one rule”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.
Layers
Section titled “Layers”NUI (html in each resource, shared shell in bs_ui) | SendNUIMessage / fetch (NUI callbacks)Client Lua mirrors state, never authoritative | net events + BSB callbacksServer Lua Player objects, validation, hooks | oxmysql (behind bs_core/server/sv_database.lua)MariaDB migrations in bs_core/sqlThe 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.
Resource map
Section titled “Resource map”| Group | Resources |
|---|---|
| Core | bs_core, bs_loading, bs_multichar, bs_esx_bridge |
| UI | bs_ui (HUD, notify, shared NUI bits), bs_radial, bs_menu (hub), bs_chat, bs_scoreboard |
| Player | bs_inventory, bs_skills, bs_medical, bs_bodily, bs_emotes, bs_boosters, bs_gadgets, bs_drugs |
| World | bs_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 |
| Zombies | bs_zombies, bs_zombiepeds, bs_zombiesfx, bs_atlas, nodes |
| Social | bs_factions, bs_faction_bases, bs_vip |
| Staff | bs_admin, bs_config, bs_guard, bs_cinecam |
| Assets | bs_assets |
Session states
Section titled “Session states”connecting -> playerConnecting: identity resolved, ban checkedloading -> client handshake (bs:session:clientReady)select -> limbo, frozen and invisible, character list/create/delete/selectplaying -> 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.
Player data from another resource
Section titled “Player data from another resource”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:
-- serverlocal 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.RewardsClient 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)| Hook | Side |
|---|---|
core:ready | both |
player:loaded, player:spawned, player:unloaded | both |
player:money, player:levelUp | both |
player:xp, player:action | server |
character:created, character:selected | server |
session:state, needs:changed | client |
The callback bridge (BSB)
Section titled “The callback bridge (BSB)”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' }-- serverBSB.RegisterCallback('mything:get', function(src, a, b) return { ok = true, value = a + b }end)
-- clientlocal res = BSB.Await('mything:get', 5000, 1, 2) -- blocks this thread, nil on timeoutThe other way, server asks one client:
-- clientBSB.RegisterClientCallback('mything:where', function() return GetEntityCoords(PlayerPedId()) end)-- serverlocal 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" erroredand the client getsnil
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.
Services
Section titled “Services”@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) endIt’s new, most code still calls exports directly. Both work.
Statebags used
Section titled “Statebags used”| Bag | On | Set by | Meaning |
|---|---|---|---|
bsDowned | player | bs_medical | player is down |
bsDead, bsRevivable | player | bs_medical | |
bsDrugMods | player (local) | bs_drugs | drain, regen, cap, infection, sway |
bsSteady | player (local) | bs_boosters | recoil/sway multiplier |
bsDrone | player | bs_gadgets | piloting a drone |
bsSquad | player | bs_factions | squad info |
bsFactionStyle, bsFactionTag, bsFactionCanInvite, bsFactionSafeZone | player | bs_factions | tag style, permissions, safe zone |
bsVipTag | player | bs_vip | { tier, colour, style, hex, blip, hud }, nil without VIP |
bsInRaid, bsRogue | player | bs_extraction | PvP zone state |
bsSitting | player | bs_emotes | |
bsZombie | entity | bs_zombies | walker record id. Also zType, zGait, zState… in networked mode |
bsZombieDead | entity | bs_zombies | networked corpse |
bsCorpse | entity | bs_medical | player body |
bsProtected, bsKeep | entity | many | bs_world’s cleanup sweep skips it |
bsVehicle, bsLocked, bsOwned, bsWindowsDown | vehicle | bs_vehicles | |
bsBuilt | entity | bs_building |
If you spawn an entity that must not get cleaned up by bs_world, set bsProtected on it.
Event naming
Section titled “Event naming”bs:<system>:<thing>, lower camel case at the end:
bs:session:clientReady bs:characters:select bs:player:moneybs:inventory:state bs:squad:sync bs:z:addbs:ui:notify bs:ui:opened bs:medical:diedbs_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.
bs19.org · Privacy · Terms · Cookie settings