Saltearse al contenido

Arquitectura

bs_core es dueño del estado. Dinero, XP, necesidades, posición, datos del personaje: solo bs_core los escribe, y solo bs_core guarda la fila. Todos los demás recursos se lo piden a bs_core mediante exports. Eso mantiene los guardados honestos: hay un único sitio que marca un personaje como modificado y un único sitio que lo escribe.

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

El cliente pide, el servidor decide, el servidor devuelve el resultado. Todo lo que envía un cliente se vuelve a comprobar en el servidor. Las necesidades, por ejemplo, solo pueden bajar por un reporte del cliente; restaurar el hambre es una acción del servidor a partir de un consumible.

Qué hace cada uno está en la lista de resources.

GrupoRecursos
Corebs_core, bs_loading, bs_multichar, bs_esx_bridge
UIbs_ui (HUD, notify, partes NUI compartidas), bs_radial, bs_menu (hub), bs_chat, bs_scoreboard
Jugadorbs_inventory, bs_skills, bs_medical, bs_bodily, bs_emotes, bs_boosters, bs_gadgets, bs_drugs
Mundobs_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
Zombisbs_zombies, bs_zombiepeds, bs_zombiesfx, bs_atlas, nodes
Socialbs_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 y una desconexión pasan ambos por BS.UnloadPlayer, que fuerza el guardado primero. Del lado del cliente, exports.bs_core:GetSessionState() te dice dónde estás, y exports.bs_core:IsLoaded() es la comprobación simple de “hay un personaje”.

El objeto Player vive en el estado Lua de bs_core. Las funciones no sobreviven a la frontera de los exports, así que no recibes el objeto: llamas a exports que hacen el trabajo:

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

Del lado del cliente: exports.bs_core:GetData(), GetNeeds(), IsLoaded().

Los metadatos del personaje (GetPlayerMeta/SetPlayerMeta) son el sitio fácil para datos pequeños por personaje. bs_drugs guarda ahí la adicción, sin tabla propia.

bs_core tiene un pub/sub en proceso (BS.On / BS.Emit) y vuelve a difundir cada hook como un evento normal llamado bs:hook:<name>. Desde tu recurso:

AddEventHandler('bs:hook:player:loaded', function(data) ... end)
AddEventHandler('bs:hook:player:unloaded', function(...) ... end)
HookLado
core:readyambos
player:loaded, player:spawned, player:unloadedambos
player:money, player:levelUpambos
player:xp, player:actionservidor
character:created, character:selectedservidor
session:state, needs:changedcliente

Petición/respuesta sobre net events. Incluye el archivo en tu manifest y tendrás BSB en tu propio estado Lua:

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

En el otro sentido, el servidor pregunta a un cliente:

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

Cómo funciona: una petición se difunde a todos los recursos y el que registró el nombre responde, los demás callan. Por tanto:

  • un nombre desconocido o de un recurso detenido da timeout, no da error
  • los nombres son globales, ponles el prefijo de tu sistema (squad:invite, factions:roster, vendor:buy)
  • el servidor limita cada cliente a 40 peticiones por segundo por recurso
  • un handler que lanza un error imprime callback "x" errored y el cliente recibe nil

La convención para los retornos es una tabla con ok, y error cuando falla: { ok = false, error = 'Not in a squad.' }.

También existe BSB.RegisterCommand(name, ace, help, args, handler) (comprobación ACE, pcall y sugerencia en el chat en uno) y BSB.Radial(spec) en el cliente para la rueda radial. Úsalos en vez de montar los tuyos.

@bs_core/shared/sh_services.lua da BS.Provide / BS.Use. Es una forma comprobada de llamar a exports: el proveedor dice lo que ofrece, el consumidor recibe un handle o una línea de log bien visible, nunca un nil silencioso.

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

Es nuevo, la mayor parte del código aún llama a los exports directamente. Ambos funcionan.

BagEnLo defineSignificado
bsDownedjugadorbs_medicalel jugador está caído
bsDead, bsRevivablejugadorbs_medical
bsDrugModsjugador (local)bs_drugsdrain, regen, cap, infection, sway
bsSteadyjugador (local)bs_boostersmultiplicador de retroceso/vaivén
bsDronejugadorbs_gadgetspilotando un dron
bsSquadjugadorbs_factionsinformación de escuadra
bsFactionStyle, bsFactionTag, bsFactionCanInvite, bsFactionSafeZonejugadorbs_factionsestilo de tag, permisos, zona segura
bsVipTagjugadorbs_vip{ tier, colour, style, hex, blip, hud }, nil sin VIP
bsInRaid, bsRoguejugadorbs_extractionestado de la zona PvP
bsSittingjugadorbs_emotes
bsZombieentidadbs_zombiesid del registro del walker. También zType, zGait, zState… en modo en red
bsZombieDeadentidadbs_zombiescadáver en red
bsCorpseentidadbs_medicalcuerpo de jugador
bsProtected, bsKeepentidadvariosel barrido de limpieza de bs_world la omite
bsVehicle, bsLocked, bsOwned, bsWindowsDownvehículobs_vehicles
bsBuiltentidadbs_building

Si creas una entidad que bs_world no debe limpiar, define bsProtected en ella.

bs:<system>:<thing>, en lowerCamelCase al final:

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 guarda sus propios nombres en BS.Events (bs_core/shared/sh_events.lua) para que nada los escriba a mano. Los hooks son bs:hook:*. Los nombres de callbacks usan system:action sin el prefijo bs:.

Cada evento y callback por recurso está en la referencia de la API.