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, QBCoreGetCoreObject, 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) -> stringgetGrade(p) -> number|stringonChange(handler(src, oldJob, newJob))(tespit edilebilirse, iş değiştiğinde tetiklenir)
FW.Money
get(p, account: 'cash'|'bank'|'black_money'?) -> numberadd(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) -> booleanremoveItem(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_inventoryalgılandığında.
FW.Events
notify(target: number, msg: string, type?: 'info'|'success'|'error')onPlayerLoaded(handler(src))(en iyi çaba, aracılığıyla yedekleme ileplayerJoining)
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-desen | Neden ısırıyor | Düzeltme adaptörüyle |
|---|---|---|
Çekirdek nesneyi sabit kodlama (ESX = exports['es_extended']:getSharedObject() her yere dağılmış) | ESX'ye kilitler, geçiş yapmak zahmetli | Yalnı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 bozar | Kullanmak 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ılgan | Abone ol FW.Events.onPlayerLoaded. |
| Karışık envanter varsayımları | Sunucular envanterleri sık sık değiştirir | Kullanmak FW.Inv.* tespit eden ox_inventory önce, sonra çerçeve. |
| SQL şemaları tek bir çerçeveye sabitlendi | accounts, identifier, vb. ayrılır | Tarafsı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
- ESX →
- Tablolarınızda nötr anahtar:
state_id(string). StoreFW.Player.getStateId(p). - Para:
- ESX:
money(nakit),accounts.bank,accounts.black_money - QB/QBOX:
PlayerData.money.cash|bank
- ESX:
- 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/*.luaVeshared/fw.luakaynağı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_idtabloları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ınFW.Permissions.has(src, aceOrGroup)– yönetici/grup kontrollerini merkezileştirinFW.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şe | ESX | QBCore | QBOX (tipik) |
|---|---|---|---|
| Çekirdek erişimi | exports['es_extended']:getSharedObject() | exports['qb-core']:GetCoreObject() | Global yok; dışa aktarımlar açık qbx_core |
| Kaynak tarafından Oyuncu | ESX.GetPlayerFromId(src) | QBCore.Functions.GetPlayer(src) | exports.qbx_core:GetPlayer(src) (fork'landıysa ayarla) |
| Tanımlayıcı | xPlayer.identifier | PlayerData.citizenid | PlayerData.citizenid |
| Para ekle | addMoney / addAccountMoney | Functions.AddMoney | Functions.AddMoney veya exports.qbx_core:AddMoney |
| İş adı | xPlayer.job.name | PlayerData.job.name | PlayerData.job.name |
| Oyuncu yüklendi olayı | esx:playerLoaded | QBCore:Server:PlayerLoaded | Genellikle QBCore olaylarını yeniden kullanır; çatal özel |
Şüpheye düştüğünüzde QBOX, fork'unuzu inceleyin
qbx_coreihracat 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)
