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, 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 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) -> stringgetGrade(p) -> Zahl|ZeichenkettebeiÄ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)
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-Muster | Warum es beißt | Mit Adapter befestigen |
|---|---|---|
Kernobjekt fest codieren (ESX = exports['es_extended']:getSharedObject() überall verstreut) | Schließt dich an ESX an, mühsam zu migrieren | 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 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 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 | Verwende 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 deinen 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: 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 immerStatus-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/*.luaUndshared/fw.luain deine Ressource - Ersetze alle direkten ESX/QBCore/QBOX-Aufrufe in deinem Code durch
FW.* - Behalte nur eins Persistenzschlüssel:
Status-IDin 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 einFW.Permissions.has(src, aceOrGroup)– zentralisiere Admin/GruppenprüfungenFW.Vehicle.spawn(Modell, Koordinaten)– Framework-Spawn-Helfer ausblenden
Behalte die Kern Vertrag winzig; optionale Helfer in ein separates Modul einfügen.
Dreiwege-Mapping-Schnelltabelle
| Sorge | ESX | Abonnieren | 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 du unsicher bist bei QBOX, Untersuche deinen Forks
qbx_coreExporte 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)