$ USD
  • $ USD
  • € EUR
  • Britse pond (£ GBP)
  • $ AUD
  • R$ BRL
  • CHF CHF
  • ¥ JPY
Adapterpatronen: ESX↔QBCore↔QBOX (Exports, Events &a…

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

Dit is een FiveM Framework Adapter – voor scripters. Lever één resource die draait op ESX, QBCore, en QBOX door frameworkspecifieke aanroepen te isoleren achter een dunne adapter. Plaats de shared/fw.lua en per‑framework adapters hieronder in elke resource, roep de stabiele interfacecontract (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events), en houd bedrijfslogica framework‑agnostisch. Een kleine testmatrix met stubs vangt mismatches voordat je implementeert.


Waarom een Adapter?

Frameworkverschillen clusteren rond dezelfde naden:

  • Core toegang (ESX getSharedObject, QBCore GetCoreObject, QBOX exports only)
  • Playermodel (xPlayer vs Player/PlayerData)
  • Identificatoren (licentie/steam vs citizenid)
  • Geld & inventaris APIs
  • Evenementnamen tijdens het laden/inloggen/baan-update

A unified interface houdt deze koppelingen buiten uw game logica. U wisselt de adapter, niet de codebase.

BTW: Je kunt hier onze geschreven adapter gratis gebruiken:


Hoe te gebruiken (Drop-in)

Boom (voorgesteld):

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

fxmanifest.lua (laad eerst adapters, dan fw.lua zodat detectie kan binden):

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

In uw code (server of 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')

The only symbol you depend on is FW. Al het andere is intern aan de adapters.


Interface Contract (stabiel oppervlak)

Ontwerpdoel: Klein, expliciet, gedocumenteerd. Dit zijn de functies waarop u kunt vertrouwen binnen frameworks.

FW.meta

  • name() -> 'esx'|'qbcore'|'qbox'
  • has(resourceName: string) -> boolean (resource gestart?)

FW.Player

  • getBySrc(src: number) -> any (framework speler handle)
  • getStateId(p) -> string (ESX: identifier; QB/QBOX: citizenid)
  • getServerId(p) -> number (numeriek id)
  • getName(p) -> string

FW.Job

  • getName(p) -> string
  • getGrade(p) -> number|string
  • onChange(handler(src, oldJob, newJob)) (wordt geactiveerd wanneer de baan verandert, indien detecteerbaar)

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 (best‑effort; zie Notities)

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

Inventaris opmerking: servers variëren (qb-inventory, ox_inventory, qs‑inventory, etc.). De standaardimplementatie gebruikt de framework-inventaris indien beschikbaar en valt terug op ox_inventory indien gedetecteerd.

FW.Events

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

Drop-in Adapters (kopiëren/plakken)

Dit zijn pragmatische standaardinstellingen. Als je fork verschilt (vooral voor QBOX), pas dan de paar gemarkeerde opmerkingen aan.

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

Gebruiksvoorbeelden

1) Een baanbonus betalen

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) Inventarisverlening met ox fallback al afgehandeld

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)

Anti-Pattern Catalogus (en Oplossingen)

Anti-patroonWaarom het bijtFix met adapter
Hard‑coderen van kernobjecten (ESX = exports['es_extended']:getSharedObject() verspreid)Sluit je op ESX, omslachtig om te migrerenAlleen aanroepen FW.*. Core-resolutie bevindt zich in de adapter.
Probleem met framework player handle op lange termijn (bijv. behouden xPlayer in een tabel voor altijd)Handles kunnen verouderen; referenties verschillen per frameworkOpnieuw ophalen via FW.Player.getBySrc(src) wanneer je handelt, of cache door getStateId sleutel en opnieuw oplossen.
Aannemen van identifiers zijn hetzelfde (ESX identifier vs QB/QBOX citizenid)Verbreekt DB-relaties/migratiesGebruik FW.Player.getStateId(p) en een crosswalk-tabel tijdens migraties.
Directe eventnamen in bedrijfslogica (esx:playerLoaded, QBCore:Server:PlayerLoaded)Fragiel over forksAbonneren via FW.Events.onPlayerLoaded.
Gemengde inventaris aannamesServers wisselen vaak van inventarisGebruik FW.Inv.* dat detecteert ox_inventory eerst, dan framework.
SQL schema's bevroren op één frameworkaccounts, identifier, etc. wijken afGebruik neutrale kolommen (state_id, money_cash, money_bank) en migratiehulpmiddelen hieronder.

SQL & Migratieopmerkingen voor identifiers (Snelreferentie)

  • Primaire persoons-ID:
    • ESX → identifier (licentie\/steam)
    • QB/QBOX → citizenid
  • Neutrale sleutel in je tabellen: state_id (string). Opslaan FW.Player.getStateId(p).
  • Geld:
    • ESX: money (contant geld), accounts.bank, accounts.black_money
    • QB/QBOX: PlayerData.money.cash|bank
  • Minimale kruispunt (eenmalige backfill):
-- 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;

Houd de crosswalk (id_map) alleen tijdens de overgang; toekomstige schrijfacties moeten altijd gebruiken state_id.


Testmatrix & CI: Valideer een script over frameworks heen

Je hoeft geen volledige CFX-server te starten in CI om de meeste adapterproblemen op te vangen. Stub exports en voer unit tests uit voor het contractoppervlak.

1) Minimale test (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) GitHub Acties (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 (basislijn)

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

Voor volledige integratietests, start uw dev server één keer op en test met een kleine set commando's. CI stubs zijn voldoende om oppervlakkige fouten te detecteren.


Implementatie Checklist

  • Plaats shared/adapters/*.lua En shared/fw.lua in uw resource
  • Vervang alle directe ESX/QBCore/QBOX-aanroepen in je code met FW.*
  • Alleen behouden een persistentiesleutel: state_id in uw tabellen
  • Configureer inventaarvoorkeur (standaard eerst ox)
  • Voeg CI (luacheck + busted) toe en een minimale test voor elke aanroep die je gebruikt
  • Documenteer eventuele lokale afwijkingen (fork-specifieke gebeurtenissen) bovenaan uw adapterbestand

Frequently Extended Surface (optionele add-ons)

  • FW.Duty.set(p, true|false) – wikkel uw dienst-toggles
  • FW.Permissions.has(src, aceOrGroup) – centraliseer admin/groepscontroles
  • FW.Vehicle.spawn(model, coords) – verberg framework spawn helpers

Behoud de core contract klein; plaats optionele helpers in een aparte module.


Tri-way Mapping Sneloverzicht

ZorgESXQBCoreQBOX (typisch)
Toegang tot de coreexports['es_extended']:getSharedObject()exports['qb-core']:GetCoreObject()Geen globaal; exports aan qbx_core
Speler via srcESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exports.qbx_core:GetPlayer(src) (pas aan indien geforkt)
IdentifierxPlayer.identifierPlayerData.citizenidPlayerData.citizenid
Geld toevoegenaddMoney / addAccountMoneyFunctions.AddMoneyFunctions.AddMoney of exports.qbx_core:AddMoney
TaaknaamxPlayer.job.namePlayerData.job.namePlayerData.job.name
Speler geladen gebeurtenisesx:playerLoadedQBCore:Server:PlayerLoadedHergebruikt vaak QBCore events; fork-specifiek

Bij twijfel over QBOX, inspecteer je fork's qbx_core exports en bedraad dienovereenkomstig.


Laatste opmerkingen

  • Houd adapters saai: geen neveneffecten, geen databaseaanroepen.
  • Behandel framework-handlers als ondoorzichtig; haal eruit wat je nodig hebt via het contract.
  • Wanneer u moet afwijken voor een klant, Kopieer de adapter, niet uw bedrijfslogica.

Lees verder: FiveM Scripts converteren tussen ESX, QBCore & QBOX (Pillar Page)