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 shared/fw.lua y los adaptadores por marco a continuación en cualquier recurso, llame al contrato de interfaz estable (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events) 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
getSharedObject, QBCoreGetCoreObject, 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):
my-resource/ ├─ fxmanifest.lua ├─ shared/ │ ├─ adapters/ │ │ ├─ esx.lua │ │ ├─ qb.lua │ │ └─ qbox.lua │ └─ fw.lua ├─ server/ │ └─ main.lua └─ client/ └─ main.lua
fxmanifest.lua (primero cargue los adaptadores y luego fw.lua para que la detección pueda unirse):
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', -- optional: if you use SQL
'server/*.lua'
}
En tu código (servidor o cliente):
-- use the stable interface everywhere 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, 'Job bonus paid.', 'success')
El único símbolo del que dependes es
FW. 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
FW.Job
getName(p) -> stringgetGrade(p) -> number|stringonChange(handler(src, oldJob, newJob))(se activa cuando cambia el trabajo, si es detectable)
FW.Money
get(p, account: 'cash'|'bank'|'black_money'?) -> numberadd(p, account, amount: number, reason?: string)remove(p, account, amount: number, reason?: string)
FW.Inv (máximo esfuerzo; ver Notas)
addItem(p, name: string, count: number, metadata?: table) -> booleanremoveItem(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
ox_inventorySi se detecta.
FW.Events
notify(target: number, msg: string, type?: 'info'|'success'|'error')onPlayerLoaded(handler(src))(máximo esfuerzo, con respaldo medianteplayerJoining)
Adaptadores directos (copiar/pegar)
Estos son valores predeterminados pragmáticos. Si tu bifurcación es diferente (especialmente para QBOX), ajusta los comentarios marcados.
shared/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
shared/adapters/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
shared/adapters/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
shared/adapters/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
local function giveStarter(src) local p = FW.Player.getBySrc(src) if p then FW.Inv.addItem(p, 'water', 2) end end 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 = exports['es_extended']:getSharedObject() dispersos por todas partes) | Te encierra en ESX y es tedioso migrar | Solo llamar FW.*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 marco | Recuperar mediante FW.Player.getBySrc(src) cuando actúas, o escondes por getStateId clave y volver a resolver. |
Suponiendo identificadores son los mismos (ESX identifier contra QB/QBOX citizenid) | Rompe las relaciones/migraciones de bases de datos | Usar 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:Server:PlayerLoaded) | Frágil entre las bifurcaciones | Suscríbete vía FW.Events.onPlayerLoaded. |
| Supuestos de inventario mixto | Los servidores intercambian inventarios con frecuencia | Usar FW.Inv.* que detecta ox_inventory Primero, luego el marco. |
| Esquemas SQL congelados en un marco | accounts, identifier, etc. divergen | Utilice columnas neutrales (state_id, money_cash, money_bank) 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 →
identifier(licencia/steam) - QB/QBOX →
citizenid
- ESX →
- Clave neutral en tus tablas:
state_id(cadena). AlmacenarFW.Player.getStateId(p). - Dinero:
- ESX:
money(dinero en efectivo),accounts.bank,accounts.black_money - QB/QBOX:
PlayerData.money.cash|bank
- ESX:
- Paso de peatones mínimo (relleno único):
-- Example: populate your neutral key from 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; -- Example: migrate to QB/QBOX where you have a mapping table 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_map) solo durante la transición; las escrituras futuras siempre deben usarstate_id.
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
name: Lua CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
lua: [ '5.4' ]
steps:
- uses: actions/checkout@v4
- name: Install Lua & LuaRocks
uses: leafo/gh-actions-lua@v10
with: { luaVersion: ${{ matrix.lua }} }
- name: Install rocks
uses: leafo/gh-actions-luarocks@v4
- run: luarocks install luacheck
- run: luarocks install busted
- name: Lint
run: luacheck . --no-color --codes
- name: Test
run: busted -v
.luacheckrc (base)
std = 'lua54'
unused_args = false
max_line_length = 140
ignore = { '211', '212' } -- adjust for your style
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
shared/adapters/*.luayshared/fw.luaen su recurso - Reemplace todas las llamadas directas ESX/QBCore/QBOX en su código con
FW.* - Conservar únicamente uno clave de persistencia:
state_iden 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, true|false)– envuelve tus conmutadores de servicioFW.Permissions.has(src, aceOrGroup)– centralizar los controles de administración/grupoFW.Vehicle.spawn(model, coords)– 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
| Inquietud | ESX | QBCore | QBOX (típico) |
|---|---|---|---|
| Acceso al núcleo | exports['es_extended']:getSharedObject() | exports['qb-core']:GetCoreObject() | No global; exportaciones en qbx_core |
| Jugador por src | ESX.GetPlayerFromId(src) | QBCore.Functions.GetPlayer(src) | exports.qbx_core:GetPlayer(src) (ajustar si está bifurcado) |
| Identificador | xPlayer.identifier | PlayerData.citizenid | PlayerData.citizenid |
| Añadir dinero | addMoney / addAccountMoney | Functions.AddMoney | Functions.AddMoney o exports.qbx_core:AddMoney |
| Nombre del trabajo | xPlayer.job.name | PlayerData.job.name | PlayerData.job.name |
| Evento cargado por el jugador | esx:playerLoaded | QBCore:Server:PlayerLoaded | A menudo reutiliza eventos de QBCore; específico de la bifurcación |
En caso de duda sobre QBOX, Inspeccione su tenedor
qbx_coreexportaciones 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)
