Sichern Sie sich heute 20%. Verwenden Sie beim Bezahlvorgang den Code WELCOME. WILLKOMMEN

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, 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 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) -> string
  • getGrade(p) -> number|string
  • 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)

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-MusterWarum es beißtMit Adapter befestigen
Kernobjekt fest codieren (ESX = exports['es_extended']:getSharedObject() überall verstreut)Bindet Sie an ESX, mühsame MigrationNur 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 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 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. divergierenVerwenden 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
  • Neutraler Schlüssel in Ihren 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: 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 immer Status-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/*.lua Und shared/fw.lua in Ihre Ressource
  • Ersetzen Sie alle direkten ESX/QBCore/QBOX-Aufrufe in Ihrem Code durch FW.*
  • Behalten Sie nur eins Persistenzschlüssel: Status-ID in 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 Dienstknebel
  • FW.Permissions.has(src, aceOrGroup) – Zentralisieren Sie Admin-/Gruppenprüfungen
  • FW.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

SorgeESXQBCoreQBOX (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 Sie Zweifel an QBOX haben, Überprüfen Sie Ihre Gabel qbx_core Exporte 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)

Lukas
Lukas

Ich bin Luke, ein Gamer und schreibe gerne über FiveM, GTA und Rollenspiele. Ich betreibe eine Rollenspiel-Community und habe etwa 10 Jahre Erfahrung in der Verwaltung von Servern.

Artikel570