
Adapter Patterns: ESX↔QBCore↔QBOX (Exports, Events &a…
Questo è un adattatore del framework FiveM, per gli scripter. Fornisci una risorsa che funziona su ESX, QBCore, E QBOX isolando le chiamate specifiche del framework dietro un adattatore sottileLascia cadere il condiviso/fw.lua e adattatori per framework di seguito in qualsiasi risorsa, chiamare contratto di interfaccia stabile (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Eventi), e mantenere indipendente dal framework della logica aziendale. Un piccolo matrice di prova con stub rileva le discrepanze prima di procedere alla distribuzione.
Perché un adattatore?
Le differenze di struttura si concentrano sugli stessi punti:
- Nucleo access (ESX
getSharedObject, QBCoreOttieniCoreObject, solo esportazioni QBOX) - Modello del giocatore (xPlayer contro Player/PlayerData)
- Identificatori (licenza/Steam vs citizenid)
- Denaro e inventario API
- Nomi degli eventi al momento del caricamento/accesso/aggiornamento del lavoro
UN interfaccia unificata mantiene queste giunture fuori dalla logica del gioco. Si cambia l'adattatore, non il codice base.
A proposito: Puoi utilizzare gratuitamente il nostro adattatore scritto qui:
Come usare (Drop-in)
Albero (suggerito):
my-resource/ ├─ fxmanifest.lua ├─ shared/ │ ├─ adapters/ │ │ ├─ esx.lua │ │ ├─ qb.lua │ │ └─ qbox.lua │ └─ fw.lua ├─ server/ │ └─ main.lua └─ client/ └─ main.lua
fxmanifest.lua (caricare prima gli adattatori, poi fw.lua quindi il rilevamento può legarsi):
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', -- facoltativo: se usi SQL 'server/*.lua' }
Nel tuo codice (server o client):
-- usa l'interfaccia stabile ovunque 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, 'Bonus di lavoro pagato.', 'success')
L'unico simbolo da cui dipendi è
FWTutto il resto è interno al adattatori.
Contratto di interfaccia (superficie stabile)
Obiettivo di progettazione: Piccolo, esplicito, documentato. Queste sono le funzioni su cui puoi fare affidamento in tutti i framework.
FW.meta
name() -> 'esx'|'qbcore'|'qbox'has(resourceName: string) -> boolean(risorsa avviata?)
FW.Player
getBySrc(src: number) -> any(handle del lettore del framework)getStateId(p) -> string(ESX: identificativo; QB/QBOX: citizenid)getServerId(p) -> number(ID numerico)getName(p) -> string
FW.Job
getName(p) -> stringgetGrade(p) -> number|stringonChange(handler(src, oldJob, newJob))(si attiva quando cambia lavoro, se rilevabile)
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 (con il massimo impegno; vedi Note)
addItem(p, name: string, count: number, metadata?: table) -> booleanremoveItem(p, name: string, count: number, metadata?: table) -> boolean
Nota di inventario: i server variano (qb-inventory, ox_inventory, qs-inventory, ecc.). L'implementazione predefinita utilizza l'inventario del framework quando disponibile e ricorre a
inventario_di_buese rilevato.
FW.Eventi
notify(target: numero, msg: stringa, tipo?: 'info'|'successo'|'errore')onPlayerLoaded(gestore(src))(best-effort, con fallback tramitegiocatore che si unisce)
Adattatori Drop-in (copia/incolla)
Queste sono impostazioni predefinite pragmatiche. Se il tuo fork è diverso (soprattutto per QBOX), modifica i pochi commenti evidenziati.
condiviso/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
Esempi di utilizzo
1) Pagare un bonus di lavoro
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) Concessione dell'inventario con fallback di bue già gestito
funzione locale giveStarter(src) locale p = FW.Player.getBySrc(src) se p allora FW.Inv.addItem(p, 'acqua', 2) fine fine FW.Events.onPlayerLoaded(giveStarter)
Catalogo anti-pattern (e correzioni)
| Anti-schema | Perché morde | Fissare con adattatore |
|---|---|---|
Oggetto core hard‑coding (ESX = exports['es_extended']:getSharedObject() sparsi ovunque) | Ti blocca in ESX, noioso da migrare | Chiama solo FW.*La risoluzione principale risiede nell'adattatore. |
Memorizzazione del framework player gestire a lungo termine (ad esempio, mantenere xPlayer in una tabella per sempre) | Le maniglie possono diventare obsolete; i riferimenti variano a seconda del framework | Recupera tramite FW.Player.getBySrc(src) quando agisci o nascondi getStateId chiave e risolvi nuovamente. |
Supponendo identificatori sono gli stessi (ESX identificatore contro QB/QBOX cittadinanza) | Interrompe le relazioni/migrazioni del DB | Utilizzo FW.Player.getStateId(p) e un tavolo di attraversamento pedonale durante le migrazioni. |
Nomi di eventi diretti nella logica aziendale (esx:playerLoaded, QBCore:Server:PlayerLoaded) | Fragile sulle forchette | Iscriviti tramite FW.Events.onPlayerLoaded. |
| Ipotesi di inventario miste | I server scambiano spesso gli inventari | Utilizzo FW.Inv.* che rileva inventario_di_bue prima, poi il framework. |
| Schemi SQL congelati in un framework | conti, identificatore, ecc. divergono | Utilizzare colonne neutre (stato_id, denaro_contanti, banca_denaro) e gli strumenti di supporto alla migrazione di seguito. |
Note sulla migrazione di SQL e identificatori (riferimento rapido)
- Chiave della persona principale:
- ESX →
identificatore(licenza/steam) - QB/QBOX →
cittadinanza
- ESX →
- Chiave neutra nelle tue tabelle:
stato_id(stringa). NegozioFW.Player.getStateId(p). - Soldi:
- ESX:
soldi(contanti),conti.banca,conti.denaro_nero - QB/QBOX:
PlayerData.money.cash|bank
- ESX:
- Attraversamento pedonale minimo (riempimento una tantum):
-- Esempio: popolare la chiave neutrale dagli utenti ESX UPDATE my_table t JOIN utenti u ON u.identifier = t.identifier SET t.state_id = u.identifier WHERE t.state_id IS NULL; -- Esempio: migrare a QB/QBOX dove si ha una tabella di mappatura 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;
Mantenere le strisce pedonali (
id_map) solo durante la transizione; le scritture future dovrebbero sempre usarestato_id.
Matrice di test e CI: convalida di uno script tra framework
Non è necessario avviare un server CFX completo in CI per individuare la maggior parte dei problemi dell'adattatore. Esportazioni di stub ed eseguire test unitari per la superficie del contratto.
1) Test minimo (Busted)
test/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) Azioni GitHub (luacheck + busted)
.github/workflows/lua.yml
nome: Lua CI su: [push, pull_request] lavori: test: runs-on: ubuntu-latest strategia: matrix: lua: [ '5.4' ] passaggi: - usi: actions/checkout@v4 - nome: Installa Lua e LuaRocks usi: leafo/gh-actions-lua@v10 con: { luaVersion: ${{ matrix.lua }} } - nome: Installa rocks usi: leafo/gh-actions-luarocks@v4 - esegui: luarocks installa luacheck - esegui: luarocks installa busted - nome: Lint esegui: luacheck . --no-color --codes - nome: Test esegui: busted -v
.luacheckrc (linea di base)
std = 'lua54' unused_args = false max_line_length = 140 ignore = { '211', '212' } -- adattalo al tuo stile
Per test di integrazione completi, avvia il tuo server di sviluppo una volta e fai un test di fumo con un piccolo set di comandi. Gli stub di CI sono sufficienti per rilevare le interruzioni di superficie.
Lista di controllo per l'implementazione
- Gocciolare
shared/adapters/*.luaEcondiviso/fw.luanella tua risorsa - Sostituisci tutte le chiamate dirette ESX/QBCore/QBOX nel tuo codice con
FW.* - Conservare solo uno chiave di persistenza:
stato_idnelle vostre tabelle - Configurare le preferenze di inventario (per impostazione predefinita, prima il bue)
- Aggiungi CI (luacheck + busted) e un test minimo per ogni chiamata utilizzata
- Documentare eventuali deviazioni locali (eventi specifici del fork) nella parte superiore del file dell'adattatore
Superficie frequentemente estesa (componenti aggiuntivi opzionali)
FW.Duty.set(p, vero|falso)– avvolgi i tuoi toggle di servizioFW.Permissions.has(src, aceOrGroup)– centralizzare i controlli di amministrazione/gruppoFW.Vehicle.spawn(modello, coordinate)– nascondi gli helper di spawn del framework
Mantieni il nucleo contratto minuscolo; inserire gli helper opzionali in un modulo separato.
Tabella rapida di mappatura a tre vie
| Preoccupazione | ESX | QBCore | QBOX (tipico) |
|---|---|---|---|
| Accesso al core | esportazioni['es_extended']:getSharedObject() | esportazioni['qb-core']:GetCoreObject() | Nessun globale; esportazioni su qbx_core |
| Giocatore di src | ESX.GetPlayerFromId(src) | QBCore.Functions.GetPlayer(src) | exports.qbx_core:GetPlayer(src) (regolare se biforcuto) |
| Identificatore | xPlayer.identificatore | PlayerData.citizenid | PlayerData.citizenid |
| Aggiungi denaro | aggiungiDenaro / aggiungiContoDenaro | Funzioni.AggiungiDenaro | Funzioni.AggiungiDenaro O exports.qbx_core:Aggiungi denaro |
| Nome del lavoro | xPlayer.job.name | PlayerData.job.name | PlayerData.job.name |
| Evento caricato dal giocatore | esx:playerLoaded | QBCore:Server:PlayerLoaded | Spesso riutilizza gli eventi QBCore; specifico del fork |
In caso di dubbi su QBOX, ispeziona la tua forcella
qbx_coreesportazioni e cablare di conseguenza.
Note finali
- Conservare gli adattatori noioso: nessun effetto collaterale, nessuna chiamata al database.
- Tratta le maniglie del framework come opaco; estrai ciò di cui hai bisogno tramite il contratto.
- Quando devi deviare per un cliente, copiare l'adattatore, non la logica del tuo business.
Leggi anche: Conversione di script FiveM tra ESX, QBCore e QBOX (pagina principale)






