Pattern degli Adattatori: ESX↔QBCore↔QBOX (Esportazioni, Eventi &a…

Pattern di adattatori: ESX↔QBCore↔QBOX (Export, Eventi e…

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 shared/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.Events), 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 accesso (ESX getSharedObject, QBCore GetCoreObject, 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', -- optional: if you use SQL
 'server/*.lua'
}

Nel tuo codice (server o client):

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

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) -> string
  • getGrade(p) -> number|string
  • onChange(handler(src, oldJob, newJob)) (si attiva quando cambia lavoro, se rilevabile)

FW.Money

  • get(p, account: 'cash'|'bank'|'black_money'?) -> number
  • add(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) -> boolean
  • removeItem(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 ox_inventory se rilevato.

FW.Events

  • notify(target: number, msg: string, type?: 'info'|'success'|'error')
  • onPlayerLoaded(handler(src)) (best-effort, con fallback tramite playerJoining)

Adattatori Drop-in (copia/incolla)

Queste sono impostazioni predefinite pragmatiche. Se il tuo fork è diverso (soprattutto per QBOX), modifica i pochi commenti evidenziati.

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

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

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)

Catalogo anti-pattern (e correzioni)

Anti-schemaPerché mordeFissare con adattatore
Oggetto core hard‑coding (ESX = exports['es_extended']:getSharedObject() sparsi ovunque)Ti blocca in ESX, noioso da migrareChiama 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 frameworkRecupera tramite FW.Player.getBySrc(src) quando agisci o nascondi getStateId chiave e risolvi nuovamente.
Supponendo identificatori sono gli stessi (ESX identifier contro QB/QBOX citizenid)Interrompe le relazioni/migrazioni del DBUtilizzo 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 forchetteIscriviti tramite FW.Events.onPlayerLoaded.
Ipotesi di inventario misteI server scambiano spesso gli inventariUtilizzo FW.Inv.* che rileva ox_inventory prima, poi il framework.
Schemi SQL congelati in un frameworkaccounts, identifier, ecc. divergonoUtilizzare colonne neutre (state_id, money_cash, money_bank) e gli strumenti di supporto alla migrazione di seguito.

Note sulla migrazione di SQL e identificatori (riferimento rapido)

  • Chiave della persona principale:
    • ESX → identifier (licenza/steam)
    • QB/QBOX → citizenid
  • Chiave neutra nelle tue tabelle: state_id (stringa). Negozio FW.Player.getStateId(p).
  • Soldi:
    • ESX: money (contanti), accounts.bank, accounts.black_money
    • QB/QBOX: PlayerData.money.cash|bank
  • Attraversamento pedonale minimo (riempimento una tantum):
-- 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;

Mantenere le strisce pedonali (id_map) solo durante la transizione; le scritture future dovrebbero sempre usare state_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

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 (linea di base)

std = 'lua54'
unused_args = false
max_line_length = 140
ignore = { '211', '212' } -- adjust for your style

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/*.lua E shared/fw.lua nella tua risorsa
  • Sostituisci tutte le chiamate dirette ESX/QBCore/QBOX nel tuo codice con FW.*
  • Conservare solo uno chiave di persistenza: state_id nelle 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, true|false) – avvolgi i tuoi toggle di servizio
  • FW.Permissions.has(src, aceOrGroup) – centralizzare i controlli di amministrazione/gruppo
  • FW.Vehicle.spawn(model, coords) – 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

PreoccupazioneESXQBCoreQBOX (tipico)
Accesso al coreexports['es_extended']:getSharedObject()exports['qb-core']:GetCoreObject()Nessun globale; esportazioni su qbx_core
Giocatore di srcESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exports.qbx_core:GetPlayer(src) (regolare se biforcuto)
IdentificatorexPlayer.identifierPlayerData.citizenidPlayerData.citizenid
Aggiungi denaroaddMoney / addAccountMoneyFunctions.AddMoneyFunctions.AddMoney O exports.qbx_core:AddMoney
Nome del lavoroxPlayer.job.namePlayerData.job.namePlayerData.job.name
Evento caricato dal giocatoreesx:playerLoadedQBCore:Server:PlayerLoadedSpesso riutilizza gli eventi QBCore; specifico del fork

In caso di dubbi su QBOX, ispeziona la tua forcella qbx_core esportazioni 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)