Spare 20 % mit WELCOMEAngebote ansehen
Adaptermuster: ESX↔QBCore↔QBOX (Exporte, Ereignisse &a…)

Adaptermuster: ESX↔QBCore↔QBOX (Exporte, Ereignisse &a…)

Dies ist ein FiveM-Framework-Adapter – für Scripter. Versende eine Ressource, die auf ESX, Abonnieren, Und QBOX durch die Isolierung von Framework-spezifischen Aufrufen hinter einem dünner Adapter. Lass die shared/fw.lua und pro-Framework-Adapter unten in jede Ressource aufrufen stabiler Schnittstellenvertrag (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events), und halte die Geschäftslogik framework-unabhängig. Ein kleines 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, QBCore GetCoreObject, 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 Nähte aus deiner Spiel-Logik heraus. Du tauschst den Adapter, nicht die Codebasis.

Übrigens: Du kannst unseren schriftlichen Adapter hier kostenlos verwenden:


Verwendung (Drop-in)

Baum (empfohlen):

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

fxmanifest.lua (lade Adapter zuerst, dann fw.lua damit die Erkennung binden kann):

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: falls du SQL verwendest
 'server/*.lua'
}

In deinem Code (Server oder Client):

-- verwende die stabile Schnittstelle überall
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')

Das einzige Symbol, von dem du abhängig bist, ist FWAlles andere ist intern im Adapter.


Interface Contract (stabile Oberfläche)

Designziel: Klein, explizit, dokumentiert. Das sind die Funktionen, auf die du dich über verschiedene Frameworks hinweg verlassen kannst.

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) -> Zahl (numerische ID)
  • getName(p) -> string

FW.Job

  • getName(p) -> string
  • getGrade(p) -> Zahl|Zeichenkette
  • beiÄnderung(handler(src, alterJob, neuerJob)) (wird ausgelöst, wenn sich der Job ändert, sofern erkennbar)

FW.Money

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

FW.Events

  • benachrichtigen (Ziel: Zahl, Nachricht: Zeichenfolge, Typ?: „Info“ | „Erfolg“ | „Fehler“)
  • onPlayerLoaded(handler(src)) (Best Effort, mit Fallback über Spielerbeitritt)

Drop-in-Adapter (Kopieren/Einfügen)

Das sind pragmatische Standardeinstellungen. Wenn dein Fork abweicht (besonders für QBOX), passe 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

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-Katalog (und Fixes)

Anti-MusterWarum es beißtMit Adapter befestigen
Kernobjekt fest codieren (ESX = exports['es_extended']:getSharedObject() überall verstreut)Schließt dich an ESX an, mühsam zu migrierenNur 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 FrameworkErneutes Abrufen über FW.Player.getBySrc(src) wenn du handelst, oder zwischenspeicherst getStateId Schlüssel und erneute Auflösung.
Annahme von Kennungen sind gleich (ESX Kennung gegen QB/QBOX Bürger-ID)Bricht DB-Beziehungen/Migrationen abVerwenden FW.Player.getStateId(p) und ein Zebrastreifentisch während der Migration.
Direkte Ereignisnamen in der Geschäftslogik (esx:playerLoaded, QBCore:Server:PlayerLoaded)Zerbrechlich über GabelnAbonnieren über FW.Events.onPlayerLoaded.
Gemischte BestandsannahmenServer tauschen häufig Inventare ausVerwenden FW.Inv.* die erkennt ox_inventory zuerst, dann Rahmen.
SQL-Schemas auf ein Framework fixiertKonten, Kennungusw. divergierenVerwende 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
  • Neutraler Schlüssel in deinen Tabellen: Status-ID (Zeichenfolge). Speichern FW.Player.getStateId(p).
  • Geld:
    • ESX: Geld (Kasse), accounts.bank, Konten.Schwarzgeld
    • QB/QBOX: PlayerData.money.cash|bank
  • Minimaler Fußgängerüberweg (einmalige Hinterfüllung):
-- Beispiel: Befülle deinen 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: Migriere zu QB/QBOX, wenn du eine Mapping-Tabelle esx_identifier→citizenid hast
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;

Halte die Fußgängerüberführung (ID-Karte) nur während des Übergangs; zukünftige Schreibvorgänge sollten immer Status-ID.


Testing Matrix & CI: Validierung eines Skripts über verschiedene Frameworks

Du musst keinen vollständigen CFX-Server im CI starten, um die meisten Adapterprobleme zu erkennen. Stub-Exporte und führe Unit-Tests für die Vertragsoberfläche aus.

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
on: [push, pull_request]
jobs:
 test:
 runs-on: ubuntu-latest
 strategy:
 matrix:
 lua: [ '5.4' ]
 steps:
 - uses: actions/checkout@v4
 - name: Lua & LuaRocks installieren
 uses: leafo/gh-actions-lua@v10
 with: { luaVersion: ${{ matrix.lua }} }
 - name: Rocks installieren
 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 (Basislinie)

std = 'lua54'
unused_args = false
max_line_length = 140
ignore = { '211', '212' } -- passe es an deinen Stil an

Für vollständige Integrationstests, starte deinen Dev-Server einmal und teste mit einem kleinen Befehlssatz. CI-Stubs reichen aus, um Oberflächenbrüche zu erkennen.


Checkliste für die Implementierung

  • Fallen freigegeben/Adapter/*.lua Und shared/fw.lua in deine Ressource
  • Ersetze alle direkten ESX/QBCore/QBOX-Aufrufe in deinem Code durch FW.*
  • Behalte nur eins Persistenzschlüssel: Status-ID in deinen Tabellen
  • Inventarpräferenz konfigurieren (standardmäßig zuerst „ox“)
  • Füge CI (luacheck + busted) und einen minimalen Test für jeden Aufruf hinzu, den du verwendest
  • Dokumentiere lokale Abweichungen (fork-spezifische Ereignisse) am Anfang deiner Adapterdatei

Häufig erweiterte Oberfläche (optionale Add-Ons)

  • FW.Duty.set(p, wahr|falsch) – wickle deine Dienst-Toggles ein
  • FW.Permissions.has(src, aceOrGroup) – zentralisiere Admin/Gruppenprüfungen
  • FW.Vehicle.spawn(Modell, Koordinaten) – Framework-Spawn-Helfer ausblenden

Behalte die Kern Vertrag winzig; optionale Helfer in ein separates Modul einfügen.


Dreiwege-Mapping-Schnelltabelle

SorgeESXAbonnierenQBOX (typisch)
Kernzugriffexports['es_extended']:getSharedObject()exports['qb-core']:GetCoreObject()Kein Global; Exporte auf qbx_core
Spieler von srcESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exports.qbx_core:GetPlayer(src) (bei Gabelung anpassen)
KennungxPlayer.identifierPlayerData.citizenidPlayerData.citizenid
Geld hinzufügenGeld hinzufügen / addAccountMoneyFunktionen.Geld hinzufügenFunktionen.Geld hinzufügen oder exports.qbx_core:AddMoney
AuftragsnamexPlayer.job.namePlayerData.job.namePlayerData.job.name
Vom Spieler geladenes Ereignisesx:playerLoadedQBCore:Server:PlayerLoadedVerwendet QBCore-Ereignisse häufig wieder; Fork-spezifisch

Wenn du unsicher bist bei QBOX, Untersuche deinen Forks qbx_core Exporte und entsprechend verdrahten.


Abschließende Anmerkungen

  • Adapter behalten langweilig: keine Nebenwirkungen, keine Datenbankaufrufe.
  • Behandle Framework-Handles als undurchsichtig; extrahiere, was du brauchst, über den Vertrag.
  • Wenn du für einen Kunden abweichen musst, kopiere den Adapter, nicht deine Geschäftslogik.

Weiterlesen: Konvertieren von FiveM-Skripten zwischen ESX, QBCore und QBOX (Pillar Page)