Économisez 20 % avec WELCOMEVoir les promotions
Modèles d'adaptateurs : ESX↔QBCore↔QBOX (Exportations, événements et…)

Modèles d'adaptateurs : ESX↔QBCore↔QBOX (Exportations, événements et…)

Il s'agit d'un adaptateur FiveM Framework pour les scripteurs. Fournissez une ressource qui s'exécute sur ESX, QB-Core, et QBOX en isolant les appels spécifiques au framework derrière un adaptateur mince. Laissez tomber le partagé/fw.lua et les adaptateurs par framework ci-dessous dans n'importe quelle ressource, appelez le contrat d'interface stable (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Événements), et rester indépendant du cadre logique métier. Un petit matrice de test avec des stubs détecte les incompatibilités avant le déploiement.


Pourquoi un adaptateur ?

Les différences de cadre se concentrent autour des mêmes coutures :

  • Cœur accéder (ESX obtenir l'objet partagé, QBCore Obtenir l'objet de base(Exportations QBOX uniquement)
  • Modèle de joueur (xPlayer contre Player/PlayerData)
  • Identifiants (licence/steam vs citizenid)
  • Argent et inventaire Apis
  • Noms des événements au moment du chargement/de la connexion/de la mise à jour du travail

UN interface unifiée garde ces coutures hors de la logique de votre jeu. Vous échangez l'adaptateur, pas la base de code.

D'AILLEURS:Vous pouvez utiliser notre adaptateur écrit ici, gratuitement :


Mode d'emploi (Drop-in)

Arbre (suggéré) :

my-resource/
├─ fxmanifest.lua
├─ shared/
│ ├─ adapters/
│ │ ├─ esx.lua
│ │ ├─ qb.lua
│ │ └─ qbox.lua
│ └─ fw.lua
├─ server/
│ └─ main.lua
└─ client/
 └─ main.lua

fxmanifest.lua (chargez d'abord les adaptateurs, puis fw.lua afin que la détection puisse se lier) :

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', -- optionnel : si vous utilisez SQL
 'server/*.lua'
}

Dans votre code (serveur ou client) :

-- utiliser l'interface stable partout 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')

Le seul symbole dont vous dépendez est FW. Tout le reste est interne au adaptateurs.


Contrat d'interface (surface stable)

Objectif de conception : Petit, explicite, documenté. Voici les fonctions sur lesquelles vous pouvez compter dans tous les frameworks.

FW.meta

  • nom() -> 'esx'|'qbcore'|'qbox''
  • a(nom_ressource : chaîne) -> booléen (ressource démarrée ?)

FW.Player

  • getBySrc(src: nombre) -> n'importe quel (poignée du lecteur de framework)
  • getStateId(p) -> chaîne (ESX : identifiant ; QB/QBOX : citizenid)
  • obtenirServerId(p) -> nombre (identifiant numérique)
  • getName(p) -> chaîne

FW.Job

  • getName(p) -> chaîne
  • obtenirGrade(p) -> nombre|chaîne
  • onChange(gestionnaire(src, ancienJob, nouveauJob)) (se déclenche lorsque le travail change, si détectable)

FW.Money

  • obtenir(p, compte : 'espèces'|'banque'|'argent_noir'?) -> nombre
  • add(p, compte, montant : nombre, raison ? : chaîne)
  • remove(p, compte, montant : nombre, raison ? : chaîne)

FW.Inv (meilleur effort; voir Notes)

  • ajouterItem(p, nom : chaîne, nombre : nombre, métadonnées ? : tableau) -> booléen
  • supprimerItem(p, nom : chaîne, nombre : nombre, métadonnées ? : tableau) -> booléen

Note d'inventaire : Les serveurs varient (qb-inventory, ox_inventory, qs-inventory, etc.). L'implémentation par défaut utilise l'inventaire du framework lorsqu'il est disponible et revient à ox_inventaire si détecté.

FW.Événements

  • notify(cible : nombre, msg : chaîne, type ? : 'info'|'succès'|'erreur')
  • onPlayerLoaded(gestionnaire(src)) (meilleur effort, avec repli via joueurRejoindre)

Adaptateurs à insérer (copier/coller)

Ce sont des valeurs par défaut pragmatiques. Si votre fork diffère (notamment pour QBOX), ajustez les quelques commentaires marqués.

partagé/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

partagé/adaptateurs/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

partagé/adaptateurs/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

partagé/adaptateurs/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

Exemples d'utilisation

1) Verser une prime à l'emploi

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) Subvention d'inventaire avec repli sur bœuf déjà gérée

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)

Catalogue d'anti-modèles (et correctifs)

Anti-modèlePourquoi ça mordRéparer avec un adaptateur
Objet principal codé en dur (ESX = exportations['es_extended']:getSharedObject() dispersés partout)Vous enferme dans ESX, fastidieux à migrerAppeler uniquement FW.*La résolution principale réside dans l'adaptateur.
Stockage à long terme du gestionnaire de lecteur de framework (par exemple, garder xPlayer dans une table pour toujours)Les poignées peuvent devenir obsolètes ; les références diffèrent selon le frameworkRécupérer via FW.Player.getBySrc(src) lorsque vous agissez, ou cachez par obtenir l'identifiant d'état clé et résoudre à nouveau.
En supposant des identifiants sont les mêmes (ESX identifiant contre QB/QBOX citoyenid)Interrompt les relations/migrations de la base de donnéesUtiliser FW.Player.getStateId(p) et une table de croisement lors des migrations.
Noms d'événements directs dans la logique métier (esx:playerLoaded, QBCore : Serveur : PlayerLoaded)Fragile à travers les fourchesAbonnez-vous via FW.Events.onPlayerLoaded.
Hypothèses d'inventaire mixtesLes serveurs échangent souvent leurs inventairesUtiliser FW.Inv.* qui détecte ox_inventaire d'abord, puis le cadre.
Schémas SQL gelés dans un seul frameworkcomptes, identifiant, etc. divergentUtiliser des colonnes neutres (identifiant d'état, argent comptant, banque d'argent) et les aides à la migration ci-dessous.

Notes de migration SQL et des identifiants (référence rapide)

  • Clé de la personne principale :
    • ESX → identifiant (licence/vapeur)
    • QB/QBOX → citoyenid
  • Clé neutre dans vos tables : identifiant d'état (chaîne). Magasin FW.Player.getStateId(p).
  • Argent:
    • ESX : argent (espèces), comptes.bancaires, comptes.black_money
    • QB/QBOX : PlayerData.money.cash|banque
  • Passage piéton minimal (remblai unique) :
-- Exemple : remplissez votre clé neutre à partir des utilisateurs ESX UPDATE my_table t JOIN users u ON u.identifier = t.identifier SET t.state_id = u.identifier WHERE t.state_id IS NULL; -- Exemple : migrez vers QB/QBOX où vous avez une table de mappage 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;

Gardez le passage piéton (id_map) uniquement pendant la transition ; les écritures futures doivent toujours utiliser identifiant d'état.


Matrice de tests et CI : valider un script sur plusieurs frameworks

Vous n’avez pas besoin de démarrer un serveur CFX complet dans CI pour détecter la plupart des problèmes d’adaptateur. Exportations de stub et exécuter des tests unitaires pour la surface du contrat.

1) Test minimal (Busted)

tests/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) Actions 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: Installer Lua et LuaRocks
 uses: leafo/gh-actions-lua@v10
 with: { luaVersion: ${{ matrix.lua }} }
 - name: Installer les 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 (ligne de base)

std = 'lua54' unused_args = false max_line_length = 140 ignore = { '211', '212' } -- ajustez selon votre style

Pour des tests d'intégration complets, lancez votre serveur de développement une fois et effectuez un test de fumée avec un petit jeu de commandes. Les stubs CI suffisent à détecter les failles superficielles.


Liste de contrôle de mise en œuvre

  • Baisse partagé/adaptateurs/*.lua et partagé/fw.lua dans votre ressource
  • Remplacez tous les appels directs ESX/QBCore/QBOX dans votre code par FW.*
  • Garder uniquement un clé de persistance : identifiant d'état dans vos tables
  • Configurer la préférence d'inventaire (bœuf en premier par défaut)
  • Ajoutez CI (luacheck + busted) et un test minimal pour chaque appel que vous utilisez
  • Documentez toutes les déviations locales (événements spécifiques au fork) en haut de votre fichier d'adaptateur

Surface fréquemment étendue (modules complémentaires en option)

  • FW.Duty.set(p, vrai|faux) – enroulez vos boutons de service
  • FW.Permissions.has(src, aceOrGroup) – centraliser les contrôles administrateur/groupe
  • FW.Vehicle.spawn(modèle, coordonnées) – masquer les assistants de génération du framework

Gardez le cœur contrat minuscule ; placez les aides facultatives dans un module séparé.


Tableau rapide de cartographie à trois voies

PréoccupationESXQB-CoreQBOX (typique)
Accès au noyauexportations['es_extended']:getSharedObject()exportations['qb-core']:GetCoreObject()Pas de global; exportations sur qbx_core
Joueur par srcESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exportations.qbx_core:GetPlayer(src) (ajuster si fourchu)
IdentifiantxPlayer.identifierPlayerData.citizenidPlayerData.citizenid
Ajout d'argentajouter de l'argent / ajouterAccountMoneyFonctions.AddMoneyFonctions.AddMoney ou exports.qbx_core:AddMoney
Nom du postexPlayer.job.namePlayerData.job.namePlayerData.job.name
Événement chargé par le joueuresx:playerLoadedQBCore : Serveur : PlayerLoadedRéutilise souvent les événements QBCore ; spécifique au fork

En cas de doute sur QBOX, inspectez votre fourche qbx_core exportations et câblez en conséquence.


Notes finales

  • Conserver les adaptateurs ennuyeux: aucun effet secondaire, aucun appel à la base de données.
  • Traiter les poignées du framework comme opaque; extrayez ce dont vous avez besoin grâce au contrat.
  • Lorsque vous devez diverger pour un client, copier l'adaptateur, pas votre logique métier.

Lire la suite : Conversion de scripts FiveM entre ESX, QBCore et QBOX (page pilier)