Economize 20% hoje mesmo Use o código WELCOME ao finalizar a compra. BEM-VINDO

Padrões de adaptador: ESX↔QBCore↔QBOX (Exportações, Eventos e…

Este é um adaptador de framework FiveM – para criadores de scripts. Envie um recurso que roda em ESX, QBCore, e Q-BOX (caixa de som) isolando chamadas específicas da estrutura por trás de uma adaptador fino. Solte o compartilhado/fw.lua e adaptadores por estrutura abaixo em qualquer recurso, chame o contrato de interface estável (FW.Player, FW.Job, FW.Dinheiro, FW.Inv, FW.Eventos) e manter a estrutura lógica de negócios agnóstica. Um pequeno matriz de teste com stubs detecta incompatibilidades antes de implantar.


Por que um adaptador?

As diferenças de estrutura se agrupam em torno das mesmas costuras:

  • Essencial acesso (ESX obterObjetoCompartilhado, QBCore ObterCoreObject, somente exportações QBOX)
  • Modelo de jogador (xPlayer vs Jogador/Dados do Jogador)
  • Identificadores (licença/steam vs citizenid)
  • Dinheiro e estoque APIs
  • Nomes de eventos no momento do carregamento/login/atualização do trabalho

UM interface unificada mantém essas costuras fora da lógica do seu jogo. Você troca o adaptador, não a base de código.

POR FALAR NISSO:Você pode usar nosso adaptador escrito aqui, gratuitamente:


Como usar (Drop-in)

Árvore (sugerido):

meu-recurso/ ├─ fxmanifest.lua ├─ compartilhado/ │ ├─ adaptadores/ │ │ ├─ esx.lua │ │ ├─ qb.lua │ │ └─ qbox.lua │ └─ fw.lua ├─ servidor/ │ └─ main.lua └─ cliente/ └─ main.lua

fxmanifest.lua (carregar adaptadores primeiro, depois fw.lua para que a detecção possa ser vinculada):

fx_version 'cerulean' game 'gta5' lua54 'yes' shared_scripts { 'shared/adapters/*.lua', 'shared/fw.lua' } client_scripts { 'client/*.lua' } server_scripts { '@oxmysql/lib/MySQL.lua', -- opcional: se você usar SQL 'server/*.lua' }

No seu código (servidor ou cliente):

-- use a interface estável em todos os lugares local src = source local p = FW.Player.getBySrc(src) local job = FW.Job.getName(p) FW.Money.add(p, 'cash', 250, 'delivery-bonus') FW.Inv.addItem(p, 'water', 1) FW.Events.notify(src, 'Bônus de trabalho pago.', 'success')

O único símbolo do qual você depende é FW. Todo o resto é interno ao adaptadores.


Contrato de interface (superfície estável)

Objetivo do projeto: Pequeno, explícito, documentado. Essas são as funções nas quais você pode confiar em todas as estruturas.

FW.meta

  • nome() -> 'esx'|'qbcore'|'qbox''
  • tem(nomeDoRecurso: string) -> booleano (recurso iniciado?)

FW.Player

  • getBySrc(src: número) -> qualquer (identificador do player do framework)
  • obterStateId(p) -> string (ESX: identificador; QB/QBOX: citizenid)
  • obterIdDoServidor(p) -> número (id numérico)
  • obterNome(p) -> string

FW.Job

  • obterNome(p) -> string
  • obterNota(p) -> número|string
  • onChange(manipulador(origem, oldJob, newJob)) (dispara quando o trabalho muda, se detectável)

FW.Dinheiro

  • obter(p, conta: 'dinheiro'|'banco'|'dinheiro_sujo'?) -> número
  • add(p, conta, valor: número, motivo?: string)
  • remove(p, conta, valor: número, motivo?: string)

FW.Inv (melhor esforço; ver Notas)

  • adicionarItem(p, nome: string, contagem: número, metadados?: tabela) -> booleano
  • removeItem(p, nome: string, contagem: número, metadados?: tabela) -> booleano

Nota de inventário: os servidores variam (qb-inventory, ox_inventory, qs‑inventory, etc.). A implementação padrão usa o inventário do framework quando disponível e retorna para inventário de bois se detectado.

FW.Eventos

  • notificar(alvo: número, msg: string, tipo?: 'info'|'sucesso'|'erro')
  • onPlayerLoaded(manipulador(origem)) (melhor esforço, com fallback via jogadorJoining)

Adaptadores Drop-in (copiar/colar)

Estes são padrões pragmáticos. Se o seu fork for diferente (especialmente para QBOX), ajuste os poucos comentários marcados.

compartilhado/fw.lua

-- framework bridge bootstrap
FW = FW or {}

local function started(name)
  local st = GetResourceState(name)
  return st == 'started' or st == 'starting'
end

local which
if started('qbx_core') then which = 'qbox'
elseif started('qb-core') then which = 'qbcore'
elseif started('es_extended') then which = 'esx' end

if which == 'qbcore' then
  FW = Adapters.qb()
elseif which == 'qbox' then
  FW = Adapters.qbox()
elseif which == 'esx' then
  FW = Adapters.esx()
else
  error('[FW] No supported framework found (es_extended / qb-core / qbx_core).')
end

-- tiny helpers common to all adapters
function FW.meta.has(res)
  return started(res)
end

compartilhado/adaptadores/esx.lua

Adapters = Adapters or {}

Adapters.esx = function()
  local ESX = exports['es_extended']:getSharedObject()

  local M = {
    meta = { name = function() return 'esx' end },
    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}
  }

  -- Player
  function M.Player.getBySrc(src) return ESX.GetPlayerFromId(src) end
  function M.Player.getStateId(p) return p.identifier end
  function M.Player.getServerId(p) return p.source end
  function M.Player.getName(p) return p.getName and p.getName() or GetPlayerName(p.source) end

  -- Job
  function M.Job.getName(p) return (p.getJob and p.getJob().name) or (p.job and p.job.name) end
  function M.Job.getGrade(p)
    local j = p.getJob and p.getJob() or p.job
    return j and (j.grade or (j.grade and j.grade.grade))
  end
  function M.Job.onChange(handler)
    -- ESX fires when job changes (commonly 'esx:setJob')
    RegisterNetEvent('esx:setJob', function(job)
      local src = source
      handler(src, nil, job and job.name)
    end)
  end

  -- Money
  local function norm(account) return account == 'cash' and 'money' or account end
  function M.Money.get(p, account)
    account = norm(account)
    if account == 'money' then return p.getMoney() end
    local acc = p.getAccount and p.getAccount(account)
    return acc and acc.money or 0
  end
  function M.Money.add(p, account, amount)
    account = norm(account)
    if account == 'money' then p.addMoney(amount) else p.addAccountMoney(account, amount) end
  end
  function M.Money.remove(p, account, amount)
    account = norm(account)
    if account == 'money' then p.removeMoney(amount) else p.removeAccountMoney(account, amount) end
  end

  -- Inventory (ESX native, with ox fallback)
  local hasOX = GetResourceState('ox_inventory') == 'started'
  if hasOX then
    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.source, name, count, meta) end
    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.source, name, count, meta) end
  else
    function M.Inv.addItem(p, name, count) p.addInventoryItem(name, count); return true end
    function M.Inv.removeItem(p, name, count) p.removeInventoryItem(name, count); return true end
  end

  -- Events
  function M.Events.notify(target, msg, kind)
    kind = kind or 'info'
    -- Implement your UI notify event here. Example placeholder:
    TriggerClientEvent('fw:notify', target, msg, kind)
  end
  function M.Events.onPlayerLoaded(handler)
    RegisterNetEvent('esx:playerLoaded', function(_)
      handler(source)
    end)
  end

  return M
end

compartilhado/adaptadores/qb.lua (QBCore)

Adapters = Adapters or {}

Adapters.qb = function()
  local QBCore = exports['qb-core']:GetCoreObject()

  local M = {
    meta = { name = function() return 'qbcore' end },
    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}
  }

  -- Player
  function M.Player.getBySrc(src) return QBCore.Functions.GetPlayer(src) end
  function M.Player.getStateId(p) return p.PlayerData.citizenid end
  function M.Player.getServerId(p) return p.PlayerData.source end
  function M.Player.getName(p)
    local pd = p.PlayerData
    return (pd.charinfo and (pd.charinfo.firstname .. ' ' .. pd.charinfo.lastname)) or GetPlayerName(pd.source)
  end

  -- Job
  function M.Job.getName(p) return p.PlayerData.job.name end
  function M.Job.getGrade(p)
    local g = p.PlayerData.job.grade
    return type(g) == 'table' and (g.level or g.grade) or g
  end
  function M.Job.onChange(handler)
    -- QBCore client event relays job update; mirror serverside via simple relay if needed.
    RegisterNetEvent('QBCore:Server:OnJobUpdate', function(job)
      handler(source, nil, job and job.name)
    end)
  end

  -- Money
  function M.Money.get(p, account) return p.PlayerData.money[account] or 0 end
  function M.Money.add(p, account, amount, reason) p.Functions.AddMoney(account, amount, reason or 'fw') end
  function M.Money.remove(p, account, amount, reason) p.Functions.RemoveMoney(account, amount, reason or 'fw') end

  -- Inventory (qb-inventory or ox)
  local hasOX = GetResourceState('ox_inventory') == 'started'
  if hasOX then
    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.PlayerData.source, name, count, meta) end
    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.PlayerData.source, name, count, meta) end
  else
    function M.Inv.addItem(p, name, count, meta) return p.Functions.AddItem(name, count, false, meta) end
    function M.Inv.removeItem(p, name, count) return p.Functions.RemoveItem(name, count) end
  end

  -- Events
  function M.Events.notify(target, msg, kind)
    TriggerClientEvent('fw:notify', target, msg, kind or 'info')
  end
  function M.Events.onPlayerLoaded(handler)
    RegisterNetEvent('QBCore:Server:PlayerLoaded', function()
      handler(source)
    end)
  end

  return M
end

compartilhado/adaptadores/qbox.lua (QBOX / qbx_core)

Adapters = Adapters or {}

Adapters.qbox = function()
  -- QBOX typically exposes functions via exports only.
  -- If your fork also ships a GetCoreObject, swap accordingly.
  local QBX = exports['qbx_core']

  local M = {
    meta = { name = function() return 'qbox' end },
    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}
  }

  -- Player (QBOX uses Player with PlayerData similar to QBCore)
  function M.Player.getBySrc(src) return QBX:GetPlayer(src) end -- adjust if your API differs
  function M.Player.getStateId(p) return p.PlayerData.citizenid end
  function M.Player.getServerId(p) return p.PlayerData.source end
  function M.Player.getName(p)
    local pd = p.PlayerData
    return (pd.charinfo and (pd.charinfo.firstname .. ' ' .. pd.charinfo.lastname)) or GetPlayerName(pd.source)
  end

  -- Job
  function M.Job.getName(p) return p.PlayerData.job.name end
  function M.Job.getGrade(p)
    local g = p.PlayerData.job.grade
    return type(g) == 'table' and (g.level or g.grade) or g
  end
  function M.Job.onChange(handler)
    -- Some QBOX builds forward QBCore job events; if not, wire your own when setting jobs.
    RegisterNetEvent('QBCore:Server:OnJobUpdate', function(job)
      handler(source, nil, job and job.name)
    end)
  end

  -- Money
  function M.Money.get(p, account) return p.PlayerData.money[account] or 0 end
  function M.Money.add(p, account, amount, reason)
    if p.Functions and p.Functions.AddMoney then p.Functions.AddMoney(account, amount, reason or 'fw')
    else QBX:AddMoney(p.PlayerData.source, account, amount, reason or 'fw') end
  end
  function M.Money.remove(p, account, amount, reason)
    if p.Functions and p.Functions.RemoveMoney then p.Functions.RemoveMoney(account, amount, reason or 'fw')
    else QBX:RemoveMoney(p.PlayerData.source, account, amount, reason or 'fw') end
  end

  -- Inventory (ox preferred on many QBOX servers)
  local hasOX = GetResourceState('ox_inventory') == 'started'
  if hasOX then
    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.PlayerData.source, name, count, meta) end
    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.PlayerData.source, name, count, meta) end
  else
    -- fall back to qb-style if present
    if p and p.Functions and p.Functions.AddItem then
      function M.Inv.addItem(p, name, count, meta) return p.Functions.AddItem(name, count, false, meta) end
      function M.Inv.removeItem(p, name, count) return p.Functions.RemoveItem(name, count) end
    else
      function M.Inv.addItem() return false end
      function M.Inv.removeItem() return false end
    end
  end

  -- Events
  function M.Events.notify(target, msg, kind)
    TriggerClientEvent('fw:notify', target, msg, kind or 'info')
  end
  function M.Events.onPlayerLoaded(handler)
    -- Some QBOX builds reuse QBCore load events; if yours differs, relay from your login logic.
    RegisterNetEvent('QBCore:Server:PlayerLoaded', function()
      handler(source)
    end)
  end

  return M
end

Exemplos de uso

1) Pagar um bônus de trabalho

RegisterNetEvent('myres:payBonus', function()
  local src = source
  local p = FW.Player.getBySrc(src)
  if not p then return end

  if FW.Job.getName(p) == 'delivery' then
    FW.Money.add(p, 'cash', 250, 'delivery-bonus')
    FW.Events.notify(src, 'Bonus paid (+$250).', 'success')
  else
    FW.Events.notify(src, 'You are not on duty as Delivery.', 'error')
  end
end)

2) Concessão de estoque com fallback de boi já tratado

função local giveStarter(fonte) local p = FW.Player.getBySrc(fonte) se p então FW.Inv.addItem(p, 'água', 2) fim fim FW.Events.onPlayerLoaded(giveStarter)

Catálogo Antipadrão (e Correções)

AntipadrãoPor que mordeConsertar com adaptador
Objeto central de codificação rígida (ESX = exports['es_extended']:getSharedObject() espalhados por toda parte)Bloqueia você no ESX, é tedioso migrarApenas ligue FW.*. A resolução do núcleo reside no adaptador.
Armazenando o identificador do player de estrutura a longo prazo (por exemplo, manter xPlayer em uma mesa para sempre)Os identificadores podem ficar obsoletos; as referências variam de acordo com a estruturaRe-buscar via FW.Player.getBySrc(origem) quando você age, ou armazena em cache por obterStateId chave e re-resolver.
Assumindo identificadores são os mesmos (ESX identificador vs QB/QBOX identidade de cidadão)Quebra relações/migrações de BDUsar FW.Player.getStateId(p) e uma mesa de faixa de pedestres durante as migrações.
Nomes de eventos diretos na lógica de negócios (esx:playerLoaded, QBCore:Servidor:PlayerLoaded)Frágil em garfosInscreva-se via FW.Events.onPlayerLoaded.
Suposições de estoque mistoOs servidores trocam inventários com frequênciaUsar FW.Inv.* que detecta inventário de bois primeiro, depois a estrutura.
Esquemas SQL congelados em uma estruturacontas, identificador, etc. divergeUse colunas neutras (id_do_estado, dinheiro_dinheiro, banco_dinheiro) e ajudantes de migração abaixo.

Notas de Migração de SQL e Identificador (Referência Rápida)

  • Chave da pessoa primária:
    • ESX → identificador (licença/steam)
    • QB/QBOX → identidade de cidadão
  • Chave neutra em suas tabelas: id_do_estado (string). Loja FW.Player.getStateId(p).
  • Dinheiro:
    • ESX: dinheiro (dinheiro), contas.bancárias, contas.dinheiro_preto
    • QB/QBOX: PlayerData.dinheiro.dinheiro|banco
  • Faixa de pedestres mínima (aterro único):
-- Exemplo: preencha sua chave neutra de ESX users UPDATE my_table t JOIN users u ON u.identifier = t.identifier SET t.state_id = u.identifier WHERE t.state_id IS NULL; -- Exemplo: migre para QB/QBOX onde você tem uma tabela de mapeamento esx_identifier→citizenid UPDATE my_table t JOIN id_map m ON m.esx_identifier = t.state_id SET t.state_id = m.citizenid WHERE m.citizenid IS NOT NULL;

Mantenha a faixa de pedestres (id_mapa) somente durante a transição; gravações futuras devem sempre usar id_do_estado.


Matriz de testes e CI: validar um script entre frameworks

Você não precisa inicializar um servidor CFX completo no CI para detectar a maioria dos problemas do adaptador. Exportações de stubs e executar testes unitários para a superfície do contrato.

1) Teste mínimo (Busted)

testes/fw_spec.lua

local function makeStub(framework)
  _G.Adapters = {}
  if framework == 'esx' then
    _G.exports = { ['es_extended'] = { getSharedObject = function()
      return {
        GetPlayerFromId = function(src)
          return {source = src, identifier = 'license:abc', getMoney = function() return 100 end,
                  addMoney = function() end, removeMoney = function() end,
                  getJob = function() return {name='mechanic', grade=2} end,
                  addAccountMoney=function() end, removeAccountMoney=function() end,
                  addInventoryItem=function() end, removeInventoryItem=function() end,
                  getName=function() return 'Alex ESX' end }
        end
      }
    end } }
    _G.GetResourceState = function(n) return n=='es_extended' and 'started' or 'missing' end
    dofile('shared/adapters/esx.lua')
  elseif framework == 'qbcore' then
    _G.exports = { ['qb-core'] = { GetCoreObject = function()
      return { Functions = { GetPlayer=function(src)
        return { PlayerData={source=src,citizenid='CITZ123',job={name='mechanic',grade=2},
                              money={cash=100,bank=500},charinfo={firstname='Alex',lastname='QB'}},
                 Functions={AddMoney=function() end, RemoveMoney=function() end, AddItem=function() return true end, RemoveItem=function() return true end} }
      end } }
    end } }
    _G.GetResourceState = function(n) return n=='qb-core' and 'started' or 'missing' end
    dofile('shared/adapters/qb.lua')
  elseif framework == 'qbox' then
    _G.exports = { ['qbx_core'] = setmetatable({}, { __index = function()
      return function(name) end
    end }) }
    _G.GetResourceState = function(n) return n=='qbx_core' and 'started' or 'missing' end
    dofile('shared/adapters/qbox.lua')
  end
  dofile('shared/fw.lua')
end

describe('FW contract', function()
  it('resolves player and money (esx)', function()
    makeStub('esx')
    assert.are.equal('esx', FW.meta.name())
    local p = FW.Player.getBySrc(1)
    assert.are.equal('license:abc', FW.Player.getStateId(p))
    assert.are.equal(100, FW.Money.get(p, 'cash'))
  end)

  it('resolves player and money (qbcore)', function()
    makeStub('qbcore')
    assert.are.equal('qbcore', FW.meta.name())
    local p = FW.Player.getBySrc(2)
    assert.are.equal('CITZ123', FW.Player.getStateId(p))
    assert.are.equal(100, FW.Money.get(p, 'cash'))
  end)
end)

2) Ações GitHub (luacheck + busted)

.github/fluxos de trabalho/lua.yml

nome: Lua CI em: [push, pull_request] trabalhos: teste: execuções em: ubuntu-latest estratégia: matriz: lua: [ '5.4' ] etapas: - usos: actions/checkout@v4 - nome: Instalar Lua e LuaRocks usos: leafo/gh-actions-lua@v10 com: { luaVersion: ${{ matrix.lua }} } - nome: Instalar rocks usos: leafo/gh-actions-luarocks@v4 - execução: luarocks instalar luacheck - execução: luarocks instalar busted - nome: Lint executar: luacheck . --no-color --codes - nome: Execução de teste: busted -v

.luacheckrc (linha de base)

std = 'lua54' unused_args = false max_line_length = 140 ignore = { '211', '212' } -- ajuste ao seu estilo

Para testes de integração completos, inicie seu servidor de desenvolvimento uma vez e faça um teste de fumaça com um pequeno conjunto de comandos. Stubs de CI são suficientes para detectar quebras de superfície.


Lista de verificação de implementação

  • Derrubar compartilhado/adaptadores/*.lua e compartilhado/fw.lua em seu recurso
  • Substitua todas as chamadas diretas ESX/QBCore/QBOX em seu código por FW.*
  • Mantenha apenas um chave de persistência: id_do_estado em suas mesas
  • Configurar preferência de inventário (oxir primeiro por padrão)
  • Adicione CI (luacheck + busted) e um teste mínimo para cada chamada que você usar
  • Documente quaisquer desvios locais (eventos específicos do fork) no topo do seu arquivo adaptador

Superfície frequentemente estendida (complementos opcionais)

  • FW.Duty.set(p, verdadeiro|falso) – enrole seus botões de serviço
  • FW.Permissions.has(origem, aceOrGroup) – centralizar verificações de administração/grupo
  • FW.Vehicle.spawn(modelo, coordenadas) – ocultar ajudantes de geração de framework

Mantenha o essencial contrato minúsculo; coloque ajudantes opcionais em um módulo separado.


Tabela rápida de mapeamento tridirecional

PreocupaçãoESXQBCoreQBOX (típico)
Acesso principalexportações['es_extended']:getSharedObject()exportações['qb-core']:GetCoreObject()Não global; exportações em qbx_núcleo
Jogador por srcESX.GetPlayerFromId(origem)QBCore.Functions.GetPlayer(origem)exportações.qbx_core:GetPlayer(origem) (ajuste se for bifurcado)
IdentificadorxPlayer.identificadorDados do jogador.id do cidadãoDados do jogador.id do cidadão
Adicionar dinheiroadicionar dinheiro / adicionarContaDinheiroFunções.AdicionarDinheiroFunções.AdicionarDinheiro ou exportações.qbx_core:AddMoney
Nome do trabalhoxPlayer.job.nameDadosDoJogador.trabalho.nomeDadosDoJogador.trabalho.nome
Evento carregado pelo jogadoresx:playerLoadedQBCore:Servidor:PlayerLoadedFrequentemente reutiliza eventos QBCore; específico do fork

Em caso de dúvida sobre o QBOX, inspecione seu garfo qbx_núcleo exportações e conecte-o adequadamente.


Notas Finais

  • Mantenha os adaptadores tedioso: sem efeitos colaterais, sem chamadas de banco de dados.
  • Trate os identificadores da estrutura como opaco; extraia o que você precisa através do contrato.
  • Quando você precisa divergir para um cliente, copie o adaptador, não sua lógica de negócios.

Leia a seguir: Convertendo scripts FiveM entre ESX, QBCore e QBOX (Página Pilar)

Lucas
Lucas

Eu sou Luke, sou um gamer e adoro escrever sobre FiveM, GTA e roleplay. Eu administro uma comunidade de roleplay e tenho cerca de 10 anos de experiência em administração de servidores.

Artigos: 570