Ahorra hoy mismo en 20%. Usa el código WELCOME al finalizar la compra. WELCOME

Adapter Patterns: ESX↔QBCore↔QBOX (Exports, Events &a…

Este es un adaptador de FiveM Framework para programadores. Incluye un recurso que se ejecuta en ESX, QBCore, y QBOX aislando llamadas específicas del marco detrás de un adaptador delgadoSuelta el compartido/fw.lua y los adaptadores por marco a continuación en cualquier recurso, llame al contrato de interfaz estable (FW.Player, Trabajo FW, FW.Money, FW.Inv, Eventos FW) y mantener la lógica de negocios independiente del marco. Una pequeña matriz de prueba con stubs atrapa desajustes antes de implementar.


¿Por qué un adaptador?

Las diferencias en el marco se agrupan en torno a las mismas costuras:

  • Centro acceso (ESX obtenerObjetoCompartido, QBCore Obtener objeto principal, Solo exportaciones de QBOX)
  • Modelo de jugador (xPlayer vs Jugador/Datos del Jugador)
  • Identificadores (licencia/steam vs. citizenid)
  • Dinero e inventario API
  • Nombres de eventos en el momento de carga/inicio de sesión/actualización del trabajo

A interfaz unificada Mantiene estas uniones fuera de la lógica del juego. Se cambia el adaptador, no el código base.

POR CIERTO:Puedes utilizar nuestro adaptador escrito aquí, de forma gratuita:


Cómo usar (instalación directa)

Árbol (sugerido):

mi-recurso/ ├─ fxmanifest.lua ├─ compartido/ │ ├─ adaptadores/ │ │ ├─ esx.lua │ │ ├─ qb.lua │ │ └─ qbox.lua │ └─ fw.lua ├─ servidor/ │ └─ principal.lua └─ cliente/ └─ principal.lua

fxmanifest.lua (primero cargue los adaptadores y luego fw.lua para que la detección pueda unirse):

fx_version 'cerulean' juego 'gta5' lua54 'sí' shared_scripts { 'shared/adapters/*.lua', 'shared/fw.lua' } client_scripts { 'client/*.lua' } server_scripts { '@oxmysql/lib/MySQL.lua', -- opcional: si usa SQL 'server/*.lua' }

En tu código (servidor o cliente):

-- usa la interfaz estable en todas partes 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, 'Bono de trabajo pagado.', 'success')

El único símbolo del que dependes es Frente. Todo lo demás es interno al adaptadores.


Contrato de interfaz (superficie estable)

Objetivo del diseño: Pequeño, explícito, documentado. Estas son las funciones en las que puede confiar en todos los marcos.

FW.meta

  • name() -> 'esx'|'qbcore'|'qbox'
  • has(resourceName: string) -> boolean (¿recurso iniciado?)

FW.Player

  • getBySrc(src: number) -> any (identificador del reproductor de marco)
  • getStateId(p) -> string (ESX: identificador; QB/QBOX: id. ciudadano)
  • getServerId(p) -> number (identificación numérica)
  • getName(p) -> string

Trabajo FW

  • getName(p) -> string
  • getGrade(p) -> number|string
  • onChange(handler(src, trabajoantiguo, trabajonuevo)) (se activa cuando cambia el trabajo, si es detectable)

FW.Money

  • get(p, account: 'cash'|'bank'|'black_money'?) -> number
  • add(p, cuenta, importe: número, motivo?: cadena)
  • remove(p, cuenta, importe: número, motivo?: cadena)

FW.Inv (máximo esfuerzo; ver Notas)

  • addItem(p, name: string, count: number, metadata?: table) -> boolean
  • removeItem(p, name: string, count: number, metadata?: table) -> boolean

Nota de inventario: Los servidores varían (qb-inventory, ox_inventory, qs-inventory, etc.). La implementación predeterminada utiliza el inventario del marco cuando está disponible y recurre a inventario_de_bueyes Si se detecta.

Eventos FW

  • notificar(objetivo: número, mensaje: cadena, tipo: 'info'|'éxito'|'error')
  • onPlayerLoaded(controlador(src)) (máximo esfuerzo, con respaldo mediante jugadorUniéndose)

Adaptadores directos (copiar/pegar)

Estos son valores predeterminados pragmáticos. Si tu bifurcación es diferente (especialmente para QBOX), ajusta los comentarios marcados.

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

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

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

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

Ejemplos de uso

1) Pagar un bono laboral

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) Concesión de inventario con reserva de buey ya gestionada

función local giveStarter(src) local p = FW.Player.getBySrc(src) si p entonces FW.Inv.addItem(p, 'agua', 2) fin fin FW.Events.onPlayerLoaded(giveStarter)

Catálogo de antipatrones (y soluciones)

Antipatrón¿Por qué muerde?Arreglar con adaptador
Objeto principal codificado de forma rígida (ESX = exportaciones['es_extended']:getSharedObject() dispersos por todas partes)Te encierra en ESX y es tedioso migrarSolo llamar Frente.*La resolución del núcleo reside en el adaptador.
Almacenamiento de identificadores de reproductores de marco a largo plazo (por ejemplo, mantener xPlayer en una tabla para siempre)Los identificadores pueden volverse obsoletos; las referencias difieren según el marcoRecuperar mediante FW.Player.getBySrc(fuente) cuando actúas, o escondes por obtenerIdDeEstado clave y volver a resolver.
Suponiendo identificadores son los mismos (ESX identificador contra QB/QBOX ID de ciudadano)Rompe las relaciones/migraciones de bases de datosUsar FW.Player.getStateId(p) y una mesa de cruce de peatones durante las migraciones.
Nombres de eventos directos en la lógica empresarial (esx:playerLoaded, QBCore:Servidor:Jugador cargado)Frágil entre las bifurcacionesSuscríbete vía FW.Eventos.onPlayerLoaded.
Supuestos de inventario mixtoLos servidores intercambian inventarios con frecuenciaUsar Inv.FW.* que detecta inventario_de_bueyes Primero, luego el marco.
Esquemas SQL congelados en un marcocuentas, identificador, etc. divergenUtilice columnas neutrales (id_de_estado, dinero_efectivo, banco de dinero) y ayudantes de migración a continuación.

Notas sobre la migración de SQL e identificadores (referencia rápida)

  • Clave de persona principal:
    • ESX → identificador (licencia/steam)
    • QB/QBOX → ID de ciudadano
  • Clave neutral en tus tablas: id_de_estado (cadena). Almacenar FW.Player.getStateId(p).
  • Dinero:
    • ESX: dinero (dinero en efectivo), cuentas.bancarias, cuentas.dinero_negro
    • QB/QBOX: Datos del jugador.dinero.efectivo|banco
  • Paso de peatones mínimo (relleno único):
-- Ejemplo: rellene su clave neutral desde 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; -- Ejemplo: migre a QB/QBOX donde tiene una tabla de mapeo 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;

Mantener el paso de peatones (id_mapa) solo durante la transición; las escrituras futuras siempre deben usar id_de_estado.


Matriz de prueba y CI: Validar un script en diferentes marcos

No es necesario iniciar un servidor CFX completo en CI para detectar la mayoría de los problemas del adaptador. Exportaciones de stubs y ejecutar pruebas unitarias para la superficie del contrato.

1) Prueba mínima (fallida)

pruebas/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) Acciones GitHub (luacheck + busted)

.github/workflows/lua.yml

nombre: Lua CI en: [push, pull_request] trabajos: prueba: se ejecuta en: ubuntu-latest estrategia: matriz: lua: [ '5.4' ] pasos: - usos: acciones/checkout@v4 - nombre: Instalar Lua y LuaRocks usos: leafo/gh-actions-lua@v10 con: { luaVersion: ${{ matrix.lua }} } - nombre: Instalar rocks usos: leafo/gh-actions-luarocks@v4 - ejecutar: luarocks install luacheck - ejecutar: luarocks install busted - nombre: Lint run: luacheck . --no-color --codes - nombre: Ejecución de prueba: busted -v

.luacheckrc (base)

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

Para realizar pruebas de integración completas, inicie su servidor de desarrollo una vez y realice una prueba de humo con un pequeño conjunto de comandos. Los stubs de CI son suficientes para detectar fallos superficiales.


Lista de verificación de implementación

  • Gota compartido/adaptadores/*.lua y compartido/fw.lua en su recurso
  • Reemplace todas las llamadas directas ESX/QBCore/QBOX en su código con Frente.*
  • Conservar únicamente uno clave de persistencia: id_de_estado en tus tablas
  • Configurar la preferencia de inventario (ox primero por defecto)
  • Agregue CI (luacheck + busted) y una prueba mínima para cada llamada que use
  • Documente cualquier desviación local (eventos específicos de la bifurcación) en la parte superior de su archivo de adaptador

Superficie frecuentemente extendida (complementos opcionales)

  • FW.Duty.set(p, verdadero|falso) – envuelve tus conmutadores de servicio
  • FW.Permissions.has(src, aceOrGroup) – centralizar los controles de administración/grupo
  • FW.Vehicle.spawn(modelo, coordenadas) – ocultar ayudantes de generación de framework

Mantener el centro Contraer minúsculo; colocar ayudantes opcionales en un módulo separado.


Tabla rápida de mapeo de tres vías

InquietudESXQBCoreQBOX (típico)
Acceso al núcleoexportaciones['es_extended']:getSharedObject()exportaciones['qb-core']:GetCoreObject()No global; exportaciones en núcleo qbx
Jugador por srcESX.GetPlayerFromId(origen)QBCore.Functions.GetPlayer(src)exportaciones.qbx_core:GetPlayer(src) (ajustar si está bifurcado)
IdentificadorxPlayer.identificadorDatos del jugador.id del ciudadanoDatos del jugador.id del ciudadano
Añadir dineroañadir dinero / agregarCuentaDineroFunciones.AddMoneyFunciones.AddMoney o exportaciones.qbx_core:Añadir dinero
Nombre del trabajoxPlayer.job.namePlayerData.job.namePlayerData.job.name
Evento cargado por el jugadoresx:playerLoadedQBCore:Servidor:Jugador cargadoA menudo reutiliza eventos de QBCore; específico de la bifurcación

En caso de duda sobre QBOX, Inspeccione su tenedor núcleo qbx exportaciones y cablearlo en consecuencia.


Notas finales

  • Mantener los adaptadores aburrido:sin efectos secundarios, sin llamadas a la base de datos.
  • Tratar los manejadores del marco como opaco; extrae lo que necesitas a través del contrato.
  • Cuando debes desviarte por un cliente, copiar el adaptador, no su lógica de negocio.

Leer a continuación: Conversión de scripts de FiveM entre ESX, QBCore y QBOX (página principal)

Lucas
Lucas

Soy Luke, gamer y me encanta escribir sobre FiveM, GTA y juegos de rol. Dirijo una comunidad de juegos de rol y tengo unos 10 años de experiencia administrando servidores.

Artículos: 436