Zum Inhalt springen

Architektur

bs_core besitzt den Zustand. Geld, XP, Bedürfnisse, Position, Charakterdaten: nur bs_core schreibt sie, und nur bs_core speichert die Zeile. Jede andere Ressource fragt bs_core über Exports. So bleiben Speicherstände sauber: Es gibt genau eine Stelle, die einen Charakter als geändert markiert, und genau eine, die ihn schreibt.

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

Der Client fragt, der Server entscheidet, der Server schickt das Ergebnis zurück. Alles, was ein Client sendet, wird auf dem Server erneut geprüft. Bedürfnisse zum Beispiel können aus einem Client-Report nur sinken; Hunger zu stillen ist eine Server-Aktion durch ein Verbrauchsgut.

Was jede davon tut, steht in der Resource-Liste.

GruppeRessourcen
Corebs_core, bs_loading, bs_multichar, bs_esx_bridge
UIbs_ui (HUD, Notify, gemeinsame NUI-Teile), bs_radial, bs_menu (Hub), bs_chat, bs_scoreboard
Spielerbs_inventory, bs_skills, bs_medical, bs_bodily, bs_emotes, bs_boosters, bs_gadgets, bs_drugs
Weltbs_world, bs_interactions (ALT target dot, containers), bs_economy (loot), bs_traders, bs_vehicles, bs_building, bs_crafting, bs_gates, bs_stash, bs_recycler, bs_mining, bs_scenes, bs_events, bs_extraction
Zombiesbs_zombies, bs_zombiepeds, bs_zombiesfx, bs_atlas, nodes
Sozialbs_factions, bs_faction_bases, bs_jobs, bs_vip
Staffbs_admin, bs_tester, 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 und ein Disconnect laufen beide über BS.UnloadPlayer, das zuerst erzwungen speichert. Clientseitig sagt dir exports.bs_core:GetSessionState(), wo du stehst, und exports.bs_core:IsLoaded() ist die einfache Prüfung “gibt es einen Charakter”.

Das Player-Objekt lebt im Lua-State von bs_core. Funktionen überstehen die Export-Grenze nicht, du bekommst also nicht das Objekt, sondern rufst Exports auf, die die Arbeit erledigen:

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

Clientseitig: exports.bs_core:GetData(), GetNeeds(), IsLoaded().

Charakter-Metadaten (GetPlayerMeta/SetPlayerMeta) sind der einfache Ort für kleine Daten pro Charakter. bs_drugs speichert dort die Sucht, ohne eigene Tabelle.

bs_core hat ein In-Process-Pub/Sub (BS.On / BS.Emit) und sendet jeden Hook erneut als normales Event namens bs:hook:<name>. Aus deiner Ressource:

AddEventHandler('bs:hook:player:loaded', function(data) ... end)
AddEventHandler('bs:hook:player:unloaded', function(...) ... end)
HookSeite
core:readybeide
player:loaded, player:spawned, player:unloadedbeide
player:money, player:levelUpbeide
player:xp, player:actionServer
character:created, character:selectedServer
session:state, needs:changedClient

Request/Response über Net-Events. Binde die Datei in dein Manifest ein, und du hast BSB in deinem eigenen 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

In die andere Richtung, der Server fragt einen Client:

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

So funktioniert es: Eine Anfrage wird an jede Ressource gesendet, und die, die den Namen registriert hat, antwortet, die übrigen schweigen. Daher:

  • ein unbekannter oder gestoppter Name läuft in ein Timeout, es gibt keinen Fehler
  • Namen sind global, setze das Präfix deines Systems davor (squad:invite, factions:roster, vendor:buy)
  • der Server begrenzt jeden Client auf 40 Anfragen pro Sekunde pro Ressource
  • ein Handler, der einen Fehler wirft, gibt callback "x" errored aus, und der Client erhält nil

Die Konvention für Rückgaben ist eine Tabelle mit ok und bei Fehler zusätzlich error: { ok = false, error = 'Not in a squad.' }.

Es gibt außerdem BSB.RegisterCommand(name, ace, help, args, handler) (ACE-Prüfung, pcall und Chat-Vorschlag in einem) und BSB.Radial(spec) auf dem Client für das Radialrad. Nutze diese, statt eigene zu bauen.

@bs_core/shared/sh_services.lua liefert BS.Provide / BS.Use. Es ist ein geprüfter Weg, Exports aufzurufen: Der Anbieter sagt, was er bietet, der Nutzer bekommt ein Handle oder eine laute Logzeile, nie ein stilles nil.

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

Es ist neu, der meiste Code ruft Exports noch direkt auf. Beides funktioniert.

BagAufGesetzt vonBedeutung
bsDownedSpielerbs_medicalSpieler ist am Boden
bsDead, bsRevivableSpielerbs_medical
bsDrugModsSpieler (lokal)bs_drugsdrain, regen, cap, infection, sway
bsSteadySpieler (lokal)bs_boostersRückstoß-/Schwank-Multiplikator
bsDroneSpielerbs_gadgetssteuert eine Drohne
bsSquadSpielerbs_factionsSquad-Infos
bsFactionStyle, bsFactionTag, bsFactionCanInvite, bsFactionSafeZoneSpielerbs_factionsTag-Stil, Berechtigungen, Safe Zone
bsVipTagSpielerbs_vip{ tier, colour, style, hex, blip, hud }, nil ohne VIP
bsInRaid, bsRogueSpielerbs_extractionPvP-Zonen-Status
bsSittingSpielerbs_emotes
bsZombieEntitybs_zombiesWalker-Registry-ID. Im vernetzten Modus auch zType, zGait, zState…
bsZombieDeadEntitybs_zombiesvernetzte Leiche
bsCorpseEntitybs_medicalSpielerkörper
bsProtected, bsKeepEntityvieleder Aufräumlauf von bs_world überspringt sie
bsVehicle, bsLocked, bsOwned, bsWindowsDownFahrzeugbs_vehicles
bsBuiltEntitybs_building

Wenn du eine Entity erzeugst, die bs_world nicht aufräumen soll, setze bsProtected darauf.

bs:<system>:<thing>, am Ende in lowerCamelCase:

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 hält seine eigenen Namen in BS.Events (bs_core/shared/sh_events.lua), damit nichts sie hartkodiert. Hooks heißen bs:hook:*. Callback-Namen nutzen system:action ohne das Präfix bs:.

Jedes Event und jeder Callback pro Ressource steht in der API-Referenz.