Pular para o conteúdo

Arquitetura

O bs_core é dono do estado. Dinheiro, XP, necessidades, posição, dados do personagem: só o bs_core os escreve, e só o bs_core guarda a linha. Todos os outros recursos pedem ao bs_core através de exports. É isso que mantém os saves honestos: há um único sítio que marca um personagem como alterado e um único sítio que o escreve.

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

O cliente pede, o servidor decide, o servidor devolve o resultado. Tudo o que um cliente envia é verificado outra vez no servidor. As necessidades, por exemplo, só podem descer a partir de um reporte do cliente; repor a fome é uma ação do servidor, vinda de um consumível.

O que cada um faz está na lista de resources.

GrupoRecursos
Corebs_core, bs_loading, bs_multichar, bs_esx_bridge
UIbs_ui (HUD, notify, partes NUI partilhadas), bs_radial, bs_menu (hub), bs_chat, bs_scoreboard
Jogadorbs_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
Zombiesbs_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 e uma desconexão passam ambos por BS.UnloadPlayer, que força o save primeiro. Do lado do cliente, exports.bs_core:GetSessionState() diz-te onde estás, e exports.bs_core:IsLoaded() é a verificação simples de “existe um personagem”.

O objeto Player vive no estado Lua do bs_core. As funções não sobrevivem à fronteira dos exports, por isso não recebes o objeto: chamas exports que fazem o trabalho:

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

Do lado do cliente: exports.bs_core:GetData(), GetNeeds(), IsLoaded().

Os metadados do personagem (GetPlayerMeta/SetPlayerMeta) são o sítio fácil para pequenos dados por personagem. O bs_drugs guarda lá o vício, sem tabela própria.

O bs_core tem um pub/sub em processo (BS.On / BS.Emit) e volta a difundir cada hook como um evento normal chamado bs:hook:<name>. No teu 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

Pedido/resposta sobre net events. Inclui o ficheiro no teu manifest e passas a ter BSB no teu próprio 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

No sentido inverso, o servidor pergunta a um cliente:

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

Como funciona: um pedido é difundido para todos os recursos e aquele que registou o nome responde, os outros ficam calados. Portanto:

  • um nome desconhecido ou de um recurso parado dá timeout, não dá erro
  • os nomes são globais, põe-lhes o prefixo do teu sistema (squad:invite, factions:roster, vendor:buy)
  • o servidor limita cada cliente a 40 pedidos por segundo por recurso
  • um handler que lança erro imprime callback "x" errored e o cliente recebe nil

A convenção para os retornos é uma tabela com ok, e error quando falha: { ok = false, error = 'Not in a squad.' }.

Existe também BSB.RegisterCommand(name, ace, help, args, handler) (verificação ACE, pcall e sugestão no chat de uma só vez) e BSB.Radial(spec) no cliente para a roda radial. Usa-os em vez de fazeres o teu.

@bs_core/shared/sh_services.lua dá BS.Provide / BS.Use. É uma forma verificada de chamar exports: o fornecedor diz o que oferece, o consumidor recebe um handle ou uma linha de log bem visível, nunca um nil silencioso.

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

É novo, a maior parte do código ainda chama os exports diretamente. Ambos funcionam.

BagEmDefinido porSignificado
bsDownedjogadorbs_medicalo jogador está caído
bsDead, bsRevivablejogadorbs_medical
bsDrugModsjogador (local)bs_drugsdrain, regen, cap, infection, sway
bsSteadyjogador (local)bs_boostersmultiplicador de recuo/oscilação
bsDronejogadorbs_gadgetsa pilotar um drone
bsSquadjogadorbs_factionsinformação do esquadrão
bsFactionStyle, bsFactionTag, bsFactionCanInvite, bsFactionSafeZonejogadorbs_factionsestilo da tag, permissões, safe zone
bsVipTagjogadorbs_vip{ tier, colour, style, hex, blip, hud }, nil sem VIP
bsInRaid, bsRoguejogadorbs_extractionestado da zona PvP
bsSittingjogadorbs_emotes
bsZombieentidadebs_zombiesid do registo do walker. Também zType, zGait, zState… em modo em rede
bsZombieDeadentidadebs_zombiescadáver em rede
bsCorpseentidadebs_medicalcorpo de jogador
bsProtected, bsKeepentidadeváriosa limpeza do bs_world ignora-a
bsVehicle, bsLocked, bsOwned, bsWindowsDownveículobs_vehicles
bsBuiltentidadebs_building

Se criares uma entidade que não deve ser limpa pelo bs_world, define bsProtected nela.

bs:<system>:<thing>, com lower camel case no fim:

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

O bs_core guarda os seus próprios nomes em BS.Events (bs_core/shared/sh_events.lua) para que nada os tenha escritos à mão. Os hooks são bs:hook:*. Os nomes de callbacks usam system:action sem o prefixo bs:.

Todos os eventos e callbacks por recurso estão na referência da API.