{"id":193012,"date":"2025-08-16T17:26:39","date_gmt":"2025-08-16T15:26:39","guid":{"rendered":"https:\/\/fivemx.com\/?p=193012"},"modified":"2025-12-23T16:43:20","modified_gmt":"2025-12-23T15:43:20","slug":"adaptermuster","status":"publish","type":"post","link":"https:\/\/fivemx.com\/de\/adapter-patterns\/","title":{"rendered":"Adaptermuster: ESX\u2194QBCore\u2194QBOX (Exporte, Ereignisse &amp;a\u2026)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This is a FiveM Framework Adapter &#8211; for scripters. Ship one resource that runs on <strong>ESX<\/strong>, <strong>QBCore<\/strong>, and <strong>QBOX<\/strong> by isolating framework\u2011specific calls behind a <strong>thin adapter<\/strong>. Drop the <code>shared\/fw.lua<\/code> and per\u2011framework adapters below into any resource, call the <strong>stable interface contract<\/strong> (<code>FW.Player<\/code>, <code>FW.Job<\/code>, <code>FW.Money<\/code>, <code>FW.Inv<\/code>, <code>FW.Events<\/code>), and keep business logic framework\u2011agnostic. A small <strong>test matrix<\/strong> with stubs catches mismatches before you deploy.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Why an Adapter?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Framework differences cluster around the same seams:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><a class=\"wpil_keyword_link\" href=\"https:\/\/fivemx.com\/brand\/core\/\" title=\"Core\" data-wpil-keyword-link=\"linked\" data-wpil-monitor-id=\"1826\">Core<\/a> access<\/strong> (ESX <code>getSharedObject<\/code>, QBCore <code>GetCoreObject<\/code>, QBOX exports only)<\/li>\n\n\n\n<li><strong>Player model<\/strong> (xPlayer vs Player\/PlayerData)<\/li>\n\n\n\n<li><strong>Identifiers<\/strong> (license\/steam vs citizenid)<\/li>\n\n\n\n<li><strong>Money &amp; inventory<\/strong> APIs<\/li>\n\n\n\n<li><strong>Event names<\/strong> at load\/login\/job\u2011update time<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>unified interface<\/strong> keeps these seams out of your game logic. You swap the adapter, not the codebase.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>BTW<\/strong>: You can use our written adapter here, for free:<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/github.com\/kashuax\/FiveM-Framework-Adapter\" target=\"_blank\" rel=\"noreferrer noopener\">Framework Adapter<\/a><\/div>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">How to Use (Drop\u2011in)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tree<\/strong> (suggested):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">my-resource\/\n\u251c\u2500 fxmanifest.lua\n\u251c\u2500 shared\/\n\u2502  \u251c\u2500 adapters\/\n\u2502  \u2502  \u251c\u2500 esx.lua\n\u2502  \u2502  \u251c\u2500 qb.lua\n\u2502  \u2502  \u2514\u2500 qbox.lua\n\u2502  \u2514\u2500 fw.lua\n\u251c\u2500 server\/\n\u2502  \u2514\u2500 main.lua\n\u2514\u2500 client\/\n   \u2514\u2500 main.lua\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>fxmanifest.lua<\/strong> (load adapters first, then <code>fw.lua<\/code> so detection can bind):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">fx_version 'cerulean'\ngame 'gta5'\nlua54 'yes'\n\nshared_scripts {\n  'shared\/adapters\/*.lua',\n  'shared\/fw.lua'\n}\n\nclient_scripts {\n  'client\/*.lua'\n}\n\nserver_scripts {\n  '@oxmysql\/lib\/MySQL.lua', -- optional: if you use SQL\n  'server\/*.lua'\n}\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>In your code<\/strong> (server or client):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- use the stable interface everywhere\nlocal src = source\nlocal p = FW.Player.getBySrc(src)\nlocal job = FW.Job.getName(p)\nFW.Money.add(p, 'cash', 250, 'delivery-bonus')\nFW.Inv.addItem(p, 'water', 1)\nFW.Events.notify(src, 'Job bonus paid.', 'success')\n<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">The only symbol you depend on is <code>FW<\/code>. Everything else is internal to the <strong>adapters<\/strong>.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Interface Contract (stable surface)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Design goal: <strong>Small, explicit, documented.<\/strong> These are the functions you can rely on across frameworks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.meta<\/code><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>name() -&gt; 'esx'|'qbcore'|'qbox'<\/code><\/li>\n\n\n\n<li><code>has(resourceName: string) -&gt; boolean<\/code> (resource started?)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.Player<\/code><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>getBySrc(src: number) -&gt; any<\/code> (framework player handle)<\/li>\n\n\n\n<li><code>getStateId(p) -&gt; string<\/code> (ESX: identifier; QB\/QBOX: citizenid)<\/li>\n\n\n\n<li><code>getServerId(p) -&gt; number<\/code> (numeric id)<\/li>\n\n\n\n<li><code>getName(p) -&gt; string<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.Job<\/code><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>getName(p) -&gt; string<\/code><\/li>\n\n\n\n<li><code>getGrade(p) -&gt; number|string<\/code><\/li>\n\n\n\n<li><code>onChange(handler(src, oldJob, newJob))<\/code> (fires when job changes, if detectable)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.Money<\/code><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>get(p, account: 'cash'|'bank'|'black_money'?) -&gt; number<\/code><\/li>\n\n\n\n<li><code>add(p, account, amount: number, reason?: string)<\/code><\/li>\n\n\n\n<li><code>remove(p, account, amount: number, reason?: string)<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.Inv<\/code> (best\u2011effort; see Notes)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>addItem(p, name: string, count: number, metadata?: table) -&gt; boolean<\/code><\/li>\n\n\n\n<li><code>removeItem(p, name: string, count: number, metadata?: table) -&gt; boolean<\/code><\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Inventory note:<\/strong> servers vary (qb-inventory, ox_inventory, qs\u2011inventory, etc.). The default implementation uses framework inventory when available and falls back to <code>ox_inventory<\/code> if detected.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\"><code>FW.Events<\/code><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>notify(target: number, msg: string, type?: 'info'|'success'|'error')<\/code><\/li>\n\n\n\n<li><code>onPlayerLoaded(handler(src))<\/code> (best\u2011effort, with fallback via <code>playerJoining<\/code>)<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Drop\u2011in Adapters (copy\/paste)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These are pragmatic defaults. If your fork differs (especially for QBOX), adjust the few marked comments.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>shared\/fw.lua<\/code><\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- framework bridge bootstrap\nFW = FW or {}\n\nlocal function started(name)\n  local st = GetResourceState(name)\n  return st == 'started' or st == 'starting'\nend\n\nlocal which\nif started('qbx_core') then which = 'qbox'\nelseif started('qb-core') then which = 'qbcore'\nelseif started('es_extended') then which = 'esx' end\n\nif which == 'qbcore' then\n  FW = Adapters.qb()\nelseif which == 'qbox' then\n  FW = Adapters.qbox()\nelseif which == 'esx' then\n  FW = Adapters.esx()\nelse\n  error('[FW] No supported framework found (es_extended \/ qb-core \/ qbx_core).')\nend\n\n-- tiny helpers common to all adapters\nfunction FW.meta.has(res)\n  return started(res)\nend\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>shared\/adapters\/esx.lua<\/code><\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Adapters = Adapters or {}\n\nAdapters.esx = function()\n  local ESX = exports['es_extended']:getSharedObject()\n\n  local M = {\n    meta = { name = function() return 'esx' end },\n    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}\n  }\n\n  -- Player\n  function M.Player.getBySrc(src) return ESX.GetPlayerFromId(src) end\n  function M.Player.getStateId(p) return p.identifier end\n  function M.Player.getServerId(p) return p.source end\n  function M.Player.getName(p) return p.getName and p.getName() or GetPlayerName(p.source) end\n\n  -- Job\n  function M.Job.getName(p) return (p.getJob and p.getJob().name) or (p.job and p.job.name) end\n  function M.Job.getGrade(p)\n    local j = p.getJob and p.getJob() or p.job\n    return j and (j.grade or (j.grade and j.grade.grade))\n  end\n  function M.Job.onChange(handler)\n    -- ESX fires when job changes (commonly 'esx:setJob')\n    RegisterNetEvent('esx:setJob', function(job)\n      local src = source\n      handler(src, nil, job and job.name)\n    end)\n  end\n\n  -- Money\n  local function norm(account) return account == 'cash' and 'money' or account end\n  function M.Money.get(p, account)\n    account = norm(account)\n    if account == 'money' then return p.getMoney() end\n    local acc = p.getAccount and p.getAccount(account)\n    return acc and acc.money or 0\n  end\n  function M.Money.add(p, account, amount)\n    account = norm(account)\n    if account == 'money' then p.addMoney(amount) else p.addAccountMoney(account, amount) end\n  end\n  function M.Money.remove(p, account, amount)\n    account = norm(account)\n    if account == 'money' then p.removeMoney(amount) else p.removeAccountMoney(account, amount) end\n  end\n\n  -- Inventory (ESX native, with ox fallback)\n  local hasOX = GetResourceState('ox_inventory') == 'started'\n  if hasOX then\n    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.source, name, count, meta) end\n    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.source, name, count, meta) end\n  else\n    function M.Inv.addItem(p, name, count) p.addInventoryItem(name, count); return true end\n    function M.Inv.removeItem(p, name, count) p.removeInventoryItem(name, count); return true end\n  end\n\n  -- Events\n  function M.Events.notify(target, msg, kind)\n    kind = kind or 'info'\n    -- Implement your UI notify event here. Example placeholder:\n    TriggerClientEvent('fw:notify', target, msg, kind)\n  end\n  function M.Events.onPlayerLoaded(handler)\n    RegisterNetEvent('esx:playerLoaded', function(_)\n      handler(source)\n    end)\n  end\n\n  return M\nend\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>shared\/adapters\/qb.lua<\/code> (QBCore)<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Adapters = Adapters or {}\n\nAdapters.qb = function()\n  local QBCore = exports['qb-core']:GetCoreObject()\n\n  local M = {\n    meta = { name = function() return 'qbcore' end },\n    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}\n  }\n\n  -- Player\n  function M.Player.getBySrc(src) return QBCore.Functions.GetPlayer(src) end\n  function M.Player.getStateId(p) return p.PlayerData.citizenid end\n  function M.Player.getServerId(p) return p.PlayerData.source end\n  function M.Player.getName(p)\n    local pd = p.PlayerData\n    return (pd.charinfo and (pd.charinfo.firstname .. ' ' .. pd.charinfo.lastname)) or GetPlayerName(pd.source)\n  end\n\n  -- Job\n  function M.Job.getName(p) return p.PlayerData.job.name end\n  function M.Job.getGrade(p)\n    local g = p.PlayerData.job.grade\n    return type(g) == 'table' and (g.level or g.grade) or g\n  end\n  function M.Job.onChange(handler)\n    -- QBCore client event relays job update; mirror serverside via simple relay if needed.\n    RegisterNetEvent('QBCore:Server:OnJobUpdate', function(job)\n      handler(source, nil, job and job.name)\n    end)\n  end\n\n  -- Money\n  function M.Money.get(p, account) return p.PlayerData.money[account] or 0 end\n  function M.Money.add(p, account, amount, reason) p.Functions.AddMoney(account, amount, reason or 'fw') end\n  function M.Money.remove(p, account, amount, reason) p.Functions.RemoveMoney(account, amount, reason or 'fw') end\n\n  -- Inventory (qb-inventory or ox)\n  local hasOX = GetResourceState('ox_inventory') == 'started'\n  if hasOX then\n    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.PlayerData.source, name, count, meta) end\n    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.PlayerData.source, name, count, meta) end\n  else\n    function M.Inv.addItem(p, name, count, meta) return p.Functions.AddItem(name, count, false, meta) end\n    function M.Inv.removeItem(p, name, count) return p.Functions.RemoveItem(name, count) end\n  end\n\n  -- Events\n  function M.Events.notify(target, msg, kind)\n    TriggerClientEvent('fw:notify', target, msg, kind or 'info')\n  end\n  function M.Events.onPlayerLoaded(handler)\n    RegisterNetEvent('QBCore:Server:PlayerLoaded', function()\n      handler(source)\n    end)\n  end\n\n  return M\nend\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>shared\/adapters\/qbox.lua<\/code> (QBOX \/ qbx_core)<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Adapters = Adapters or {}\n\nAdapters.qbox = function()\n  -- QBOX typically exposes functions via exports only.\n  -- If your fork also ships a GetCoreObject, swap accordingly.\n  local QBX = exports['qbx_core']\n\n  local M = {\n    meta = { name = function() return 'qbox' end },\n    Player = {}, Job = {}, Money = {}, Inv = {}, Events = {}\n  }\n\n  -- Player (QBOX uses Player with PlayerData similar to QBCore)\n  function M.Player.getBySrc(src) return QBX:GetPlayer(src) end -- adjust if your API differs\n  function M.Player.getStateId(p) return p.PlayerData.citizenid end\n  function M.Player.getServerId(p) return p.PlayerData.source end\n  function M.Player.getName(p)\n    local pd = p.PlayerData\n    return (pd.charinfo and (pd.charinfo.firstname .. ' ' .. pd.charinfo.lastname)) or GetPlayerName(pd.source)\n  end\n\n  -- Job\n  function M.Job.getName(p) return p.PlayerData.job.name end\n  function M.Job.getGrade(p)\n    local g = p.PlayerData.job.grade\n    return type(g) == 'table' and (g.level or g.grade) or g\n  end\n  function M.Job.onChange(handler)\n    -- Some QBOX builds forward QBCore job events; if not, wire your own when setting jobs.\n    RegisterNetEvent('QBCore:Server:OnJobUpdate', function(job)\n      handler(source, nil, job and job.name)\n    end)\n  end\n\n  -- Money\n  function M.Money.get(p, account) return p.PlayerData.money[account] or 0 end\n  function M.Money.add(p, account, amount, reason)\n    if p.Functions and p.Functions.AddMoney then p.Functions.AddMoney(account, amount, reason or 'fw')\n    else QBX:AddMoney(p.PlayerData.source, account, amount, reason or 'fw') end\n  end\n  function M.Money.remove(p, account, amount, reason)\n    if p.Functions and p.Functions.RemoveMoney then p.Functions.RemoveMoney(account, amount, reason or 'fw')\n    else QBX:RemoveMoney(p.PlayerData.source, account, amount, reason or 'fw') end\n  end\n\n  -- Inventory (ox preferred on many QBOX servers)\n  local hasOX = GetResourceState('ox_inventory') == 'started'\n  if hasOX then\n    function M.Inv.addItem(p, name, count, meta) return exports.ox_inventory:AddItem(p.PlayerData.source, name, count, meta) end\n    function M.Inv.removeItem(p, name, count, meta) return exports.ox_inventory:RemoveItem(p.PlayerData.source, name, count, meta) end\n  else\n    -- fall back to qb-style if present\n    if p and p.Functions and p.Functions.AddItem then\n      function M.Inv.addItem(p, name, count, meta) return p.Functions.AddItem(name, count, false, meta) end\n      function M.Inv.removeItem(p, name, count) return p.Functions.RemoveItem(name, count) end\n    else\n      function M.Inv.addItem() return false end\n      function M.Inv.removeItem() return false end\n    end\n  end\n\n  -- Events\n  function M.Events.notify(target, msg, kind)\n    TriggerClientEvent('fw:notify', target, msg, kind or 'info')\n  end\n  function M.Events.onPlayerLoaded(handler)\n    -- Some QBOX builds reuse QBCore load events; if yours differs, relay from your login logic.\n    RegisterNetEvent('QBCore:Server:PlayerLoaded', function()\n      handler(source)\n    end)\n  end\n\n  return M\nend\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Usage Examples<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1) Paying a job bonus<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">RegisterNetEvent('myres:payBonus', function()\n  local src = source\n  local p = FW.Player.getBySrc(src)\n  if not p then return end\n\n  if FW.Job.getName(p) == 'delivery' then\n    FW.Money.add(p, 'cash', 250, 'delivery-bonus')\n    FW.Events.notify(src, 'Bonus paid (+$250).', 'success')\n  else\n    FW.Events.notify(src, 'You are not on duty as Delivery.', 'error')\n  end\nend)\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2) Inventory grant with ox fallback already handled<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">local function giveStarter(src)\n  local p = FW.Player.getBySrc(src)\n  if p then FW.Inv.addItem(p, 'water', 2) end\nend\nFW.Events.onPlayerLoaded(giveStarter)\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Anti\u2011Pattern Catalog (and Fixes)<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Anti\u2011pattern<\/th><th>Why it bites<\/th><th>Fix with adapter<\/th><\/tr><\/thead><tbody><tr><td><strong>Hard\u2011coding core object<\/strong> (<code>ESX = exports['es_extended']:getSharedObject()<\/code> scattered everywhere)<\/td><td>Locks you into ESX, tedious to migrate<\/td><td>Only call <code>FW.*<\/code>. Core resolution lives in adapter.<\/td><\/tr><tr><td><strong>Storing framework player handle long\u2011term<\/strong> (e.g., keep <code>xPlayer<\/code> in a table forever)<\/td><td>Handles can go stale; references differ per framework<\/td><td>Re\u2011fetch via <code>FW.Player.getBySrc(src)<\/code> when you act, or cache by <code>getStateId<\/code> key and re\u2011resolve.<\/td><\/tr><tr><td><strong>Assuming identifiers<\/strong> are the same (ESX <code>identifier<\/code> vs QB\/QBOX <code>citizenid<\/code>)<\/td><td>Breaks DB relations\/migrations<\/td><td>Use <code>FW.Player.getStateId(p)<\/code> and a crosswalk table during migrations.<\/td><\/tr><tr><td><strong>Direct event names in business logic<\/strong> (<code>esx:playerLoaded<\/code>, <code>QBCore:Server:PlayerLoaded<\/code>)<\/td><td>Fragile across forks<\/td><td>Subscribe via <code>FW.Events.onPlayerLoaded<\/code>.<\/td><\/tr><tr><td><strong>Mixed inventory assumptions<\/strong><\/td><td>Servers swap inventories often<\/td><td>Use <code>FW.Inv.*<\/code> which detects <code>ox_inventory<\/code> first, then framework.<\/td><\/tr><tr><td><strong>SQL schemas frozen to one framework<\/strong><\/td><td><code>accounts<\/code>, <code>identifier<\/code>, etc. diverge<\/td><td>Use neutral columns (<code>state_id<\/code>, <code>money_cash<\/code>, <code>money_bank<\/code>) and migration helpers below.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">SQL &amp; Identifier Migration Notes (Quick Reference)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Primary person key:<\/strong>\n<ul class=\"wp-block-list\">\n<li>ESX \u2192 <code>identifier<\/code> (license\/steam)<\/li>\n\n\n\n<li>QB\/QBOX \u2192 <code>citizenid<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Neutral key in your tables:<\/strong> <code>state_id<\/code> (string). Store <code>FW.Player.getStateId(p)<\/code>.<\/li>\n\n\n\n<li><strong>Money:<\/strong>\n<ul class=\"wp-block-list\">\n<li>ESX: <code>money<\/code> (cash), <code>accounts.bank<\/code>, <code>accounts.black_money<\/code><\/li>\n\n\n\n<li>QB\/QBOX: <code>PlayerData.money.cash|bank<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Minimal crosswalk (one\u2011time backfill):<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- Example: populate your neutral key from ESX users\nUPDATE my_table t\nJOIN users u ON u.identifier = t.identifier\nSET t.state_id = u.identifier\nWHERE t.state_id IS NULL;\n\n-- Example: migrate to QB\/QBOX where you have a mapping table esx_identifier\u2192citizenid\nUPDATE my_table t\nJOIN id_map m ON m.esx_identifier = t.state_id\nSET t.state_id = m.citizenid\nWHERE m.citizenid IS NOT NULL;\n<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Keep the crosswalk (<code>id_map<\/code>) only during the transition; future writes should always use <code>state_id<\/code>.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Matrix &amp; CI: Validate a Script Across Frameworks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You don\u2019t need to boot a full CFX server in CI to catch most adapter issues. <strong>Stub exports<\/strong> and run unit tests for the contract surface.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1) Minimal test (Busted)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>tests\/fw_spec.lua<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">local function makeStub(framework)\n  _G.Adapters = {}\n  if framework == 'esx' then\n    _G.exports = { ['es_extended'] = { getSharedObject = function()\n      return {\n        GetPlayerFromId = function(src)\n          return {source = src, identifier = 'license:abc', getMoney = function() return 100 end,\n                  addMoney = function() end, removeMoney = function() end,\n                  getJob = function() return {name='mechanic', grade=2} end,\n                  addAccountMoney=function() end, removeAccountMoney=function() end,\n                  addInventoryItem=function() end, removeInventoryItem=function() end,\n                  getName=function() return 'Alex ESX' end }\n        end\n      }\n    end } }\n    _G.GetResourceState = function(n) return n=='es_extended' and 'started' or 'missing' end\n    dofile('shared\/adapters\/esx.lua')\n  elseif framework == 'qbcore' then\n    _G.exports = { ['qb-core'] = { GetCoreObject = function()\n      return { Functions = { GetPlayer=function(src)\n        return { PlayerData={source=src,citizenid='CITZ123',job={name='mechanic',grade=2},\n                              money={cash=100,bank=500},charinfo={firstname='Alex',lastname='QB'}},\n                 Functions={AddMoney=function() end, RemoveMoney=function() end, AddItem=function() return true end, RemoveItem=function() return true end} }\n      end } }\n    end } }\n    _G.GetResourceState = function(n) return n=='qb-core' and 'started' or 'missing' end\n    dofile('shared\/adapters\/qb.lua')\n  elseif framework == 'qbox' then\n    _G.exports = { ['qbx_core'] = setmetatable({}, { __index = function()\n      return function(name) end\n    end }) }\n    _G.GetResourceState = function(n) return n=='qbx_core' and 'started' or 'missing' end\n    dofile('shared\/adapters\/qbox.lua')\n  end\n  dofile('shared\/fw.lua')\nend\n\ndescribe('FW contract', function()\n  it('resolves player and money (esx)', function()\n    makeStub('esx')\n    assert.are.equal('esx', FW.meta.name())\n    local p = FW.Player.getBySrc(1)\n    assert.are.equal('license:abc', FW.Player.getStateId(p))\n    assert.are.equal(100, FW.Money.get(p, 'cash'))\n  end)\n\n  it('resolves player and money (qbcore)', function()\n    makeStub('qbcore')\n    assert.are.equal('qbcore', FW.meta.name())\n    local p = FW.Player.getBySrc(2)\n    assert.are.equal('CITZ123', FW.Player.getStateId(p))\n    assert.are.equal(100, FW.Money.get(p, 'cash'))\n  end)\nend)\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2) GitHub Actions (luacheck + busted)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>.github\/workflows\/lua.yml<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">name: Lua CI\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        lua: [ '5.4' ]\n    steps:\n      - uses: actions\/checkout@v4\n      - name: Install Lua &amp; LuaRocks\n        uses: leafo\/gh-actions-lua@v10\n        with: { luaVersion: ${{ matrix.lua }} }\n      - name: Install rocks\n        uses: leafo\/gh-actions-luarocks@v4\n      - run: luarocks install luacheck\n      - run: luarocks install busted\n      - name: Lint\n        run: luacheck . --no-color --codes\n      - name: Test\n        run: busted -v\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>.luacheckrc<\/strong> (baseline)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">std = 'lua54'\nunused_args = false\nmax_line_length = 140\nignore = { '211', '212' } -- adjust for your style\n<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">For full integration tests, spin up your dev server once and smoke\u2011test with a tiny command set. CI stubs are sufficient to catch surface breaks.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Implementation Checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Drop <code>shared\/adapters\/*.lua<\/code> and <code>shared\/fw.lua<\/code> into your resource<\/li>\n\n\n\n<li>Replace all direct ESX\/QBCore\/QBOX calls in your code with <code>FW.*<\/code><\/li>\n\n\n\n<li>Keep only <strong>one<\/strong> persistence key: <code>state_id<\/code> in your tables<\/li>\n\n\n\n<li>Configure inventory preference (ox first by default)<\/li>\n\n\n\n<li>Add CI (luacheck + busted) and a minimal test for each call you use<\/li>\n\n\n\n<li>Document any local deviations (fork\u2011specific events) at the top of your adapter file<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Extended Surface (optional add\u2011ons)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>FW.Duty.set(p, true|false)<\/code> \u2013 wrap your duty toggles<\/li>\n\n\n\n<li><code>FW.Permissions.has(src, aceOrGroup)<\/code> \u2013 centralize admin\/group checks<\/li>\n\n\n\n<li><code>FW.Vehicle.spawn(model, coords)<\/code> \u2013 hide framework spawn helpers<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the <strong>core<\/strong> contract tiny; put optional helpers in a separate module.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Tri\u2011way Mapping Quick Table<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concern<\/th><th>ESX<\/th><th>QBCore<\/th><th>QBOX (typical)<\/th><\/tr><\/thead><tbody><tr><td>Core access<\/td><td><code>exports['es_extended']:getSharedObject()<\/code><\/td><td><code>exports['qb-core']:GetCoreObject()<\/code><\/td><td><em>No global;<\/em> exports on <code>qbx_core<\/code><\/td><\/tr><tr><td>Player by src<\/td><td><code>ESX.GetPlayerFromId(src)<\/code><\/td><td><code>QBCore.Functions.GetPlayer(src)<\/code><\/td><td><code>exports.qbx_core:GetPlayer(src)<\/code> <em>(adjust if forked)<\/em><\/td><\/tr><tr><td>Identifier<\/td><td><code>xPlayer.identifier<\/code><\/td><td><code>PlayerData.citizenid<\/code><\/td><td><code>PlayerData.citizenid<\/code><\/td><\/tr><tr><td>Money add<\/td><td><code>addMoney<\/code> \/ <code>addAccountMoney<\/code><\/td><td><code>Functions.AddMoney<\/code><\/td><td><code>Functions.AddMoney<\/code> or <code>exports.qbx_core:AddMoney<\/code><\/td><\/tr><tr><td>Job name<\/td><td><code>xPlayer.job.name<\/code><\/td><td><code>PlayerData.job.name<\/code><\/td><td><code>PlayerData.job.name<\/code><\/td><\/tr><tr><td>Player loaded event<\/td><td><code>esx:playerLoaded<\/code><\/td><td><code>QBCore:Server:PlayerLoaded<\/code><\/td><td>Often reuses QBCore events; fork\u2011specific<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">When in doubt on QBOX, <strong>inspect your fork\u2019s <code>qbx_core<\/code> exports<\/strong> and wire accordingly.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Final Notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Keep adapters <strong>boring<\/strong>: no side effects, no database calls.<\/li>\n\n\n\n<li>Treat framework handles as <strong>opaque<\/strong>; extract what you need through the contract.<\/li>\n\n\n\n<li>When you must diverge for a client, <strong>copy the adapter<\/strong>, not your business logic.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read next:<\/strong> <a href=\"https:\/\/fivemx.com\/converting-fivem-scripts\/\">Converting FiveM Scripts Between ESX, QBCore &amp; QBOX (Pillar Page)<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is a FiveM Framework Adapter &#8211; for scripters. Ship one resource that runs on ESX, QBCore, and QBOX by isolating framework\u2011specific calls behind a thin adapter. Drop the shared\/fw.lua and per\u2011framework adapters below into any resource, call the stable interface contract (FW.Player, FW.Job, FW.Money, FW.Inv, FW.Events), and keep business logic framework\u2011agnostic. A small test [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":193013,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2882],"tags":[],"class_list":["post-193012","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-framework-conversion"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts\/193012","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/comments?post=193012"}],"version-history":[{"count":0,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts\/193012\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/media\/193013"}],"wp:attachment":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/media?parent=193012"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/categories?post=193012"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/tags?post=193012"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}