Adapter Patterns: ESX↔QBCore↔QBOX (Exports, Events &a…
Dies ist ein FiveM Framework Adapter – für Skripter. Liefern Sie eine Ressource, die läuft auf ESX, QBCore, Und QBOX durch die Isolierung von Framework-spezifischen Aufrufen hinter einem dünner Adapter. Lassen Sie die shared/fw.lua und pro Framework-Adapter unten in eine beliebige Ressource, rufen Sie die stabiler Schnittstellenvertrag (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events), und halten Sie die Geschäftslogik Framework-agnostisch. Ein kleiner Testmatrix mit Stubs werden Nichtübereinstimmungen vor der Bereitstellung erkannt.
Warum ein Adapter?
Die Unterschiede im Framework konzentrieren sich auf dieselben Nahtstellen:
- Kern Zugang (ESX
getSharedObject, QBCoreGetCoreObject, nur QBOX-Exporte) - Spielermodell (xPlayer vs. Player/PlayerData)
- Kennungen (Lizenz/Steam vs. CitizenID)
- Geld & Inventar APIs
- Ereignisnamen beim Laden/Anmelden/Job-Update
A einheitliche Schnittstelle hält diese Lücken aus Ihrer Spiellogik heraus. Sie tauschen den Adapter aus, nicht die Codebasis.
Übrigens: Hier können Sie unseren schriftlichen Adapter kostenlos nutzen:
Verwendung (Drop-in)
Baum (empfohlen):
meine-Ressource/ ├─ fxmanifest.lua ├─ geteilt/ │ ├─ Adapter/ │ │ ├─ esx.lua │ │ ├─ qb.lua │ │ └─ qbox.lua │ └─ fw.lua ├─ Server/ │ └─ main.lua └─ Client/ └─ main.lua
fxmanifest.lua (Laden Sie zuerst die Adapter, dann fw.lua damit die Erkennung binden kann):
fx_version 'cerulean' Spiel 'gta5' lua54 'ja' shared_scripts { 'shared/adapters/*.lua', 'shared/fw.lua' } client_scripts { 'client/*.lua' } server_scripts { '@oxmysql/lib/MySQL.lua', -- optional: wenn Sie SQL 'server/*.lua' verwenden }
In Ihrem Code (Server oder Client):
-- verwenden Sie überall die stabile Schnittstelle local src = Quelle local p = FW.Player.getBySrc(src) local job = FW.Job.getName(p) FW.Money.add(p, ‚Bargeld‘, 250, ‚Lieferbonus‘) FW.Inv.addItem(p, ‚Wasser‘, 1) FW.Events.notify(src, ‚Jobbonus ausgezahlt.‘, ‚Erfolg‘)
Das einzige Symbol, auf das Sie angewiesen sind, ist
FWAlles andere ist intern im Adapter.
Interface Contract (stabile Oberfläche)
Designziel: Klein, explizit, dokumentiert. Dies sind die Funktionen, auf die Sie sich Framework-übergreifend verlassen können.
FW.meta
name() -> 'esx'|'qbcore'|'qbox'has(resourceName: string) -> boolean(Ressource gestartet?)
FW.Player
getBySrc(src: number) -> any(Framework-Player-Handle)getStateId(p) -> string(ESX: Kennung; QB/QBOX: Bürger-ID)getServerId(p) -> number(numerische ID)getName(p) -> string
FW.Job
getName(p) -> stringgetGrade(p) -> number|stringbeiÄnderung(handler(src, alterJob, neuerJob))(wird ausgelöst, wenn sich der Job ändert, sofern erkennbar)
FW.Money
get(p, account: 'cash'|'bank'|'black_money'?) -> numberadd(p, Konto, Betrag: Zahl, Grund?: Zeichenfolge)entfernen(p, Konto, Betrag: Zahl, Grund?: Zeichenfolge)
FW.Inv (nach bestem Wissen und Gewissen; siehe Hinweise)
addItem(p, name: string, count: number, metadata?: table) -> booleanremoveItem(p, name: string, count: number, metadata?: table) -> boolean
Inventarnotiz: Server variieren (qb-inventory, ox_inventory, qs‑inventory, etc.). Die Standardimplementierung verwendet Framework-Inventar, sofern verfügbar, und greift auf
ox_inventoryfalls erkannt.
FW.Events
benachrichtigen (Ziel: Zahl, Nachricht: Zeichenfolge, Typ?: „Info“ | „Erfolg“ | „Fehler“)onPlayerLoaded(handler(src))(Best Effort, mit Fallback überSpielerbeitritt)
Drop-in-Adapter (Kopieren/Einfügen)
Dies sind pragmatische Vorgaben. Sollte Ihr Fork abweichen (insbesondere für QBOX), passen Sie die wenigen markierten Kommentare an.
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
Anwendungsbeispiele
1) Zahlung eines Jobbonus
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) Inventarzuweisung mit Ox-Fallback bereits behandelt
lokale Funktion giveStarter(src) lokales p = FW.Player.getBySrc(src) wenn p dann FW.Inv.addItem(p, 'Wasser', 2) Ende Ende FW.Events.onPlayerLoaded(giveStarter)
Anti-Pattern-Katalog (und Fixes)
| Anti-Muster | Warum es beißt | Mit Adapter befestigen |
|---|---|---|
Kernobjekt fest codieren (ESX = exports['es_extended']:getSharedObject() überall verstreut) | Bindet Sie an ESX, mühsame Migration | Nur anrufen FW.*. Die Kernauflösung befindet sich im Adapter. |
Framework-Player-Handle langfristig speichern (zB halten xPlayer in einer Tabelle für immer) | Handles können veralten; Referenzen unterscheiden sich je nach Framework | Erneutes Abrufen über FW.Player.getBySrc(src) wenn Sie handeln oder cachen durch getStateId Schlüssel und erneute Auflösung. |
Annahme von Kennungen sind gleich (ESX Kennung gegen QB/QBOX Bürger-ID) | Bricht DB-Beziehungen/Migrationen ab | Verwenden FW.Player.getStateId(p) und ein Zebrastreifentisch während der Migration. |
Direkte Ereignisnamen in der Geschäftslogik (esx:playerLoaded, QBCore:Server:PlayerLoaded) | Zerbrechlich über Gabeln | Abonnieren über FW.Events.onPlayerLoaded. |
| Gemischte Bestandsannahmen | Server tauschen häufig Inventare aus | Verwenden FW.Inv.* die erkennt ox_inventory zuerst, dann Rahmen. |
| SQL-Schemas auf ein Framework fixiert | Konten, Kennungusw. divergieren | Verwenden Sie neutrale Spalten (Status-ID, Geld_Bargeld, Geldbank) und Migrationshelfer unten. |
Hinweise zur SQL- und Kennungsmigration (Kurzreferenz)
- Primärer Personenschlüssel:
- ESX →
Kennung(Lizenz/Steam) - QB/QBOX →
Bürger-ID
- ESX →
- Neutraler Schlüssel in Ihren Tabellen:
Status-ID(Zeichenfolge). SpeichernFW.Player.getStateId(p). - Geld:
- ESX:
Geld(Kasse),accounts.bank,Konten.Schwarzgeld - QB/QBOX:
PlayerData.money.cash|bank
- ESX:
- Minimaler Fußgängerüberweg (einmalige Hinterfüllung):
-- Beispiel: Füllen Sie Ihren neutralen Schlüssel aus ESX-Benutzern UPDATE my_table t JOIN users u ON u.identifier = t.identifier SET t.state_id = u.identifier WHERE t.state_id IS NULL; -- Beispiel: Migrieren Sie zu QB/QBOX, wo Sie eine Zuordnungstabelle esx_identifier→citizenid haben 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;
Halten Sie den Zebrastreifen (
ID-Karte) nur während des Übergangs; zukünftige Schreibvorgänge sollten immerStatus-ID.
Testmatrix und CI: Validieren Sie ein Skript über Frameworks hinweg
Sie müssen keinen vollständigen CFX-Server in CI booten, um die meisten Adapterprobleme zu beheben. Stub-Exporte und führen Sie Unit-Tests für die Vertragsoberfläche durch.
1) Minimaltest (geplatzt)
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-Aktionen (luacheck + busted)
.github/workflows/lua.yml
Name: Lua CI auf: [push, pull_request] Jobs: Test: läuft auf: Ubuntu-Neueste Strategie: Matrix: Lua: ['5.4'] Schritte: – verwendet: actions/checkout@v4 – Name: Lua & LuaRocks installieren verwendet: leafo/gh-actions-lua@v10 mit: { luaVersion: ${{ matrix.lua }} } – Name: Rocks installieren verwendet: leafo/gh-actions-luarocks@v4 – ausführen: luarocks install luacheck – ausführen: luarocks install busted – Name: Lint-Ausführung: luacheck . --no-color --codes – Name: Testausführung: busted -v
.luacheckrc (Basislinie)
std = 'lua54' unused_args = false max_line_length = 140 ignore = { '211', '212' } -- an Ihren Stil anpassen
Für vollständige Integrationstests starten Sie Ihren Entwicklungsserver einmal und führen einen Smoke-Test mit einem kleinen Befehlssatz durch. CI-Stubs reichen aus, um Oberflächenbrüche zu erkennen.
Checkliste für die Implementierung
- Fallen
freigegeben/Adapter/*.luaUndshared/fw.luain Ihre Ressource - Ersetzen Sie alle direkten ESX/QBCore/QBOX-Aufrufe in Ihrem Code durch
FW.* - Behalten Sie nur eins Persistenzschlüssel:
Status-IDin Ihren Tabellen - Inventarpräferenz konfigurieren (standardmäßig zuerst „ox“)
- Fügen Sie CI (luacheck + busted) und einen minimalen Test für jeden Anruf hinzu, den Sie verwenden
- Dokumentieren Sie alle lokalen Abweichungen (forkspezifische Ereignisse) oben in Ihrer Adapterdatei.
Häufig erweiterte Oberfläche (optionale Add-Ons)
FW.Duty.set(p, wahr|falsch)– wickeln Sie Ihre DienstknebelFW.Permissions.has(src, aceOrGroup)– Zentralisieren Sie Admin-/GruppenprüfungenFW.Vehicle.spawn(Modell, Koordinaten)– Framework-Spawn-Helfer ausblenden
Behalten Sie die Kern Vertrag winzig; optionale Helfer in ein separates Modul einfügen.
Dreiwege-Mapping-Schnelltabelle
| Sorge | ESX | QBCore | QBOX (typisch) |
|---|---|---|---|
| Kernzugriff | exports['es_extended']:getSharedObject() | exports['qb-core']:GetCoreObject() | Kein Global; Exporte auf qbx_core |
| Spieler von src | ESX.GetPlayerFromId(src) | QBCore.Functions.GetPlayer(src) | exports.qbx_core:GetPlayer(src) (bei Gabelung anpassen) |
| Kennung | xPlayer.identifier | PlayerData.citizenid | PlayerData.citizenid |
| Geld hinzufügen | Geld hinzufügen / addAccountMoney | Funktionen.Geld hinzufügen | Funktionen.Geld hinzufügen oder exports.qbx_core:AddMoney |
| Auftragsname | xPlayer.job.name | PlayerData.job.name | PlayerData.job.name |
| Vom Spieler geladenes Ereignis | esx:playerLoaded | QBCore:Server:PlayerLoaded | Verwendet QBCore-Ereignisse häufig wieder; Fork-spezifisch |
Wenn Sie Zweifel an QBOX haben, Überprüfen Sie Ihre Gabel
qbx_coreExporte und entsprechend verdrahten.
Abschließende Anmerkungen
- Adapter behalten langweilig: keine Nebenwirkungen, keine Datenbankaufrufe.
- Behandeln Sie Framework-Handles als undurchsichtig; holen Sie sich aus dem Vertrag, was Sie brauchen.
- Wenn Sie für einen Kunden abweichen müssen, Kopieren Sie den Adapter, nicht Ihre Geschäftslogik.
Weiterlesen: Konvertieren von FiveM-Skripten zwischen ESX, QBCore und QBOX (Pillar Page)






