$ ABD Doları
  • $ ABD Doları
  • € AVRO
  • £ GBP
  • $ Avustralya Doları
  • R$ BRL
  • İsviçre Frangı İsviçre Frangı
  • ¥ Japon Yeni
Adaptör Modelleri: ESX↔QBCore↔QBOX (Exports, Events &a…

Adaptör Desenleri: ESX↔QBCore↔QBOX (Exports, Events &a…

Bu, betik yazarları için bir FiveM Framework Adaptörüdür. Üzerinde çalışan tek bir kaynağı gönderin ESX, QBCore, Ve QBOX çerçeveye özgü çağrıları bir arkasına izole ederek ince adaptör. Aşağıdaki shared/fw.lua ve çerçeve başına adaptörleri herhangi bir kaynağa ekleyin, şurayı çağırın kararlı arayüz sözleşmesi (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events), ve iş mantığını çerçeveden bağımsız tutun. Küçük bir test matrisi saplamalarla dağıtmadan önce uyumsuzlukları yakalar.


Neden bir Adaptör?

Çerçeve farklılıkları aynı dikişler etrafında toplanır:

  • Çekirdek erişim (ESX getSharedObject, QBCore GetCoreObject, QBOX yalnızca dışa aktarımlar)
  • Oyuncu modeli (xPlayer vs Player/PlayerData)
  • Tanımlayıcılar (lisans\/steam vs citizenid)
  • Para & envanter API'ler
  • Etkinlik adları yükleme\/giriş\/iş‑güncelleme zamanında

A birleşik arayüz bu dikişleri oyun mantığınızdan uzak tutar. Adaptörü değiştirirsiniz, kod tabanını değil.

BTW: Burada yazdığımız adaptörü ücretsiz kullanabilirsiniz:


Nasıl Kullanılır (Drop‑in)

Ağaç (önerilen):

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

fxmanifest.lua (önce adaptörleri yükle, sonra fw.lua böylece algılama bağlanabilir):

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

Kodunuzda (sunucu veya istemci):

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

Bağlı olduğunuz tek sembol FW. Geriye kalan her şey adaptörler.


Arayüz Sözleşmesi (kararlı yüzey)

Tasarım hedefi: Küçük, açık, belgelenmiş. Bunlar, çerçeveler boyunca güvenebileceğiniz işlevlerdir.

FW.meta

  • name() -> 'esx'|'qbcore'|'qbox'
  • has(resourceName: string) -> boolean (kaynak başlatıldı?)

FW.Player

  • getBySrc(src: number) -> any (oyuncu tanıtıcısı)
  • getStateId(p) -> string (ESX: tanımlayıcı; QB/QBOX: vatandaş kimliği)
  • getServerId(p) -> number (sayısal kimlik)
  • getName(p) -> string

FW.Job

  • getName(p) -> string
  • getGrade(p) -> number|string
  • onChange(handler(src, oldJob, newJob)) (tespit edilebilirse, iş değiştiğinde tetiklenir)

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 (en iyi çaba; Notlara bakın)

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

Envanter notu: sunucular değişiklik gösterir (qb-inventory, ox_inventory, qs‑inventory, vb.). Varsayılan uygulama, mevcut olduğunda çerçeve envanterini kullanır ve şuna geri döner: ox_inventory algılandığında.

FW.Events

  • notify(target: number, msg: string, type?: 'info'|'success'|'error')
  • onPlayerLoaded(handler(src)) (en iyi çaba, aracılığıyla yedekleme ile playerJoining)

Tak-Çıkar Adaptörler (kopyala/yapıştır)

Bunlar pratik varsayılanlardır. Fork'unuz farklıysa (özellikle QBOX için), işaretlenmiş birkaç yorumu ayarlayın.

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

Kullanım Örnekleri

1) İş ikramiyesi ödeme

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) ox fallback ile envanter verme zaten halledildi

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-Desen Kataloğu (ve Düzeltmeleri)

Anti-desenNeden ısırıyorDüzeltme adaptörüyle
Çekirdek nesneyi sabit kodlama (ESX = exports['es_extended']:getSharedObject() her yere dağılmış)ESX'ye kilitler, geçiş yapmak zahmetliYalnızca çağır FW.*. Çekirdek çözüm adaptörde yaşıyor.
Çerçeve oyuncu tanıtıcısını uzun süreli saklama (örneğin, tutmak xPlayer sonsuza dek bir tabloda)Handle'lar bayatlayabilir; referanslar çerçeveye göre farklılık gösterirŞunu yeniden al: FW.Player.getBySrc(src) harekete geçtiğinizde veya önbelleğe aldığınızda getStateId anahtar ve yeniden çözümleme.
Tanımlayıcıları varsaymak aynıdır (ESX identifier vs QB/QBOX citizenid)Veritabanı ilişkilerini/geçişlerini bozarKullanmak FW.Player.getStateId(p) ve geçişler sırasında bir çapraz tablo.
İş mantığında doğrudan olay adları (esx:playerLoaded, QBCore:Server:PlayerLoaded)Çatallar arasında kırılganAbone ol FW.Events.onPlayerLoaded.
Karışık envanter varsayımlarıSunucular envanterleri sık sık değiştirirKullanmak FW.Inv.* tespit eden ox_inventory önce, sonra çerçeve.
SQL şemaları tek bir çerçeveye sabitlendiaccounts, identifier, vb. ayrılırTarafsız sütunlar kullan (state_id, money_cash, money_bank) ve aşağıda geçiş yardımcıları.

SQL & Tanımlayıcı Taşıma Notları (Hızlı Başvuru)

  • Birincil kişi anahtarı:
    • ESX → identifier (lisans/steam)
    • QB/QBOX → citizenid
  • Tablolarınızda nötr anahtar: state_id (string). Store FW.Player.getStateId(p).
  • Para:
    • ESX: money (nakit), accounts.bank, accounts.black_money
    • QB/QBOX: PlayerData.money.cash|bank
  • Minimal yaya geçidi (tek seferlik geri doldurma):
-- 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;

Yaya geçidini tut (id_map) yalnızca geçiş sırasında; gelecekteki yazmalar her zaman şunu kullanmalıdır: state_id.


Test Matrisi ve CI: Bir Betiği Çerçeveler Boyunca Doğrulama

Çoğu adaptör sorununu yakalamak için CI'da tam bir CFX sunucusunu başlatmanıza gerek yok. Stub dışa aktarımları ve sözleşme yüzeyi için birim testlerini çalıştırın.

1) Minimal 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 Eylemler (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 (temel)

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

Tam entegrasyon testleri için, geliştirme sunucunuzu bir kez başlatın ve küçük bir komut setiyle hızlı bir şekilde test edin. CI yamaları yüzeydeki hataları yakalamak için yeterlidir.


Uygulama Kontrol Listesi

  • Düşürmek shared/adapters/*.lua Ve shared/fw.lua kaynağınızın içine
  • Kodunuzdaki tüm doğrudan ESX/QBCore/QBOX çağrılarını şununla değiştirin: FW.*
  • Sadece tut bir kalıcılık anahtarı: state_id tablolarınızda
  • Envanter tercihini yapılandırın (varsayılan olarak önce ox)
  • Her kullandığınız çağrı için CI (luacheck + busted) ve minimum bir test ekleyin
  • Yerel sapmaları (fork'a özgü olaylar) bağdaştırıcı dosyanızın en üstünde belgeleyin

Sıkça Genişletilmiş Yüzey (isteğe bağlı eklentiler)

  • FW.Duty.set(p, true|false) – görev geçişlerinizi sarın
  • FW.Permissions.has(src, aceOrGroup) – yönetici/grup kontrollerini merkezileştirin
  • FW.Vehicle.spawn(model, coords) – çerçeve spawn yardımcılarını gizle

Sakla çekirdek sözleşme küçük; isteğe bağlı yardımcıları ayrı bir modüle koyun.


Üç Yollu Haritalama Hızlı Tablosu

EndişeESXQBCoreQBOX (tipik)
Çekirdek erişimiexports['es_extended']:getSharedObject()exports['qb-core']:GetCoreObject()Global yok; dışa aktarımlar açık qbx_core
Kaynak tarafından OyuncuESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exports.qbx_core:GetPlayer(src) (fork'landıysa ayarla)
TanımlayıcıxPlayer.identifierPlayerData.citizenidPlayerData.citizenid
Para ekleaddMoney / addAccountMoneyFunctions.AddMoneyFunctions.AddMoney veya exports.qbx_core:AddMoney
İş adıxPlayer.job.namePlayerData.job.namePlayerData.job.name
Oyuncu yüklendi olayıesx:playerLoadedQBCore:Server:PlayerLoadedGenellikle QBCore olaylarını yeniden kullanır; çatal özel

Şüpheye düştüğünüzde QBOX, fork'unuzu inceleyin qbx_core ihracat ve buna göre bağlayın.


Son Notlar

  • Bağdaştırıcıları sakla sıkıcı: yan etkisi yok, veritabanı çağrısı yok.
  • Çerçeve işleyicilerini şu şekilde ele alın saydam; sözleşme aracılığıyla ihtiyacınız olanı çıkarın.
  • Bir müşteri için farklılaşmanız gerektiğinde, adaptörü kopyala, iş mantığınızı değil.

Sonrakini oku: FiveM Scriptlerini ESX, QBCore ve QBOX Arasında Dönüştürme (Ana Sayfa)