Add configurable framework and inventory support (ESX/QBCore + qs/ox/qb_inventory)#1
Add configurable framework and inventory support (ESX/QBCore + qs/ox/qb_inventory)#1
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the resource to support multiple FiveM frameworks (ESX/QBCore) and inventory systems (qs/ox/qb_inventory) via configuration, removing hardcoded dependencies and fixing the player identifier check during lab purchase.
Changes:
- Added
Config.FrameworkandConfig.Inventoryoptions to drive runtime integration selection. - Introduced client/server abstraction helpers for framework callbacks, notifications, player lookup, and inventory operations.
- Migrated existing callbacks and inventory interactions to the new abstraction layer.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
config.lua |
Adds configurable framework/inventory selectors with defaults. |
client/cl_main.lua |
Adds framework-agnostic callback/notification helpers and swaps ESX-specific usage to dynamic wrappers. |
server/sv_main.lua |
Adds framework bootstrap + inventory abstraction functions and refactors server callbacks/inventory calls to use them. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
server/sv_main.lua
Outdated
| if i > 1 and spiedLabs[1]['LabInfo'].LabID == k then | ||
| DebugPrint('Got a lab with same LabID, rerolling') | ||
| goto reroll | ||
| end | ||
|
|
||
| lab = LabsData[k] | ||
| extraInfo = Labs.GetLabSpyExtraInfo(k, labId) | ||
| lab['LabInfo'].extraInfo = extraInfo or nil | ||
|
|
||
| print(DumpTable(lab['LabInfo'].extraInfo)) | ||
|
|
||
| table.insert(spiedLabs, LabsData[k]) |
There was a problem hiding this comment.
In GetRandomSpiedLabs, lab and extraInfo are assigned without local, which creates/overwrites globals and can lead to unexpected cross-thread/state leaks. Also, spiedLabs[1]['LabInfo'].LabID looks inconsistent with the stored structure (LabID is top-level), so the duplicate-check may never work as intended. Make these locals and compare against spiedLabs[1].LabID (or equivalent).
server/sv_main.lua
Outdated
| local player = GetPlayer(src) | ||
| return player and player.canCarryItem and player.canCarryItem(item, amount) or false |
There was a problem hiding this comment.
CanCarryItem falls back to GetPlayer(src).canCarryItem(...), but for QBCore DecoratePlayer overwrites player.canCarryItem to call CanCarryItem again. If Config.Framework == 'qbcore' and Config.Inventory is not qb_inventory/ox_inventory (e.g. default qs_inventory), this becomes infinite recursion and will stack overflow on the first carry check. Consider making CanCarryItem fully inventory-specific (handle qs_inventory explicitly) and avoid delegating back to player.canCarryItem after decorating the player.
| local player = GetPlayer(src) | |
| return player and player.canCarryItem and player.canCarryItem(item, amount) or false | |
| local ok = exports['qs-inventory']:CanCarryItem(src, item, amount) | |
| return ok == true or ok == nil |
| local allitems = GetInventoryItemList() | ||
|
|
||
| RegisterFrameworkCallback('KmF_Lab:Server:DepositItem', function(source, cb, LabId, item_name, item_qty) | ||
| local src = source | ||
| local inventory = GetPlayerInventory(src) | ||
|
|
||
| local itemqty = GetItemCount(src, item_name) | ||
|
|
||
| if itemqty < tonumber(item_qty) then | ||
| cb({status = false, reason = 'Non hai abbastanza oggetti'}) | ||
| return false | ||
| end | ||
|
|
||
| if Labs.AddDepositItem(LabId, allitems[item_name], item_qty) then | ||
| RemoveInventoryItem(src, item_name, item_qty) |
There was a problem hiding this comment.
For ox_inventory, GetInventoryItemList() returns entries keyed by item name but the item definitions typically don't include a .name field. DepositItem passes allitems[item_name] into Labs.AddDepositItem, which then indexes Item.name and will error when it's nil. Suggest normalizing the item list so each entry includes name = key, or pass a constructed item object {name=item_name, label=...} into AddDepositItem.
| function GetPlayerInventory(src) | ||
| if Config.Inventory == 'ox_inventory' then | ||
| local items = exports.ox_inventory:GetInventoryItems(src) or {} | ||
| local formatted = {} | ||
| for _, item in pairs(items) do | ||
| if item and item.name then | ||
| formatted[item.name] = { | ||
| name = item.name, | ||
| label = item.label or item.name, | ||
| qty = item.count or item.amount or 0, | ||
| image = item.name, | ||
| } | ||
| end | ||
| end | ||
| return formatted | ||
| end |
There was a problem hiding this comment.
In the ox_inventory branch, GetPlayerInventory builds formatted[item.name] = ... and overwrites entries when the inventory has multiple stacks of the same item name (e.g. different metadata), losing quantity. This can make the UI/inventory listing incorrect. Consider aggregating quantities per name (sum counts) instead of overwriting.
| if Config.Inventory == 'qb_inventory' then | ||
| local player = GetPlayer(src) | ||
| local items = player and player.PlayerData and player.PlayerData.items or {} | ||
| local formatted = {} | ||
| for _, item in pairs(items) do | ||
| if item and item.name then | ||
| formatted[item.name] = { | ||
| name = item.name, | ||
| label = item.label or item.name, | ||
| qty = item.amount or 0, | ||
| image = item.image or item.name, | ||
| } | ||
| end | ||
| end | ||
| return formatted | ||
| end |
There was a problem hiding this comment.
In the qb_inventory branch, GetPlayerInventory keys the formatted table by item.name, so multiple slots containing the same item will overwrite each other. This can under-report quantities in the UI. Consider summing item.amount for the same item.name instead of replacing the previous entry.
server/sv_main.lua
Outdated
| local pInfo = MySQL.query.await('SELECT * FROM users WHERE identifier = @identifier', { | ||
| ['@identifier'] = LabsData[labId]['Owner'] | ||
| }) | ||
|
|
||
| while not pInfo do | ||
| Citizen.Wait(100) | ||
| end | ||
|
|
||
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname |
There was a problem hiding this comment.
Labs.GetLabSpyExtraInfo queries the ESX users table and assumes firstname/lastname fields. Under Config.Framework == 'qbcore' this will fail on most setups (different table/schema) and can also error when pInfo is empty (pInfo[1] nil). Consider branching by framework (or using GetPlayerByIdentifier when online) and handle missing DB rows safely.
| local pInfo = MySQL.query.await('SELECT * FROM users WHERE identifier = @identifier', { | |
| ['@identifier'] = LabsData[labId]['Owner'] | |
| }) | |
| while not pInfo do | |
| Citizen.Wait(100) | |
| end | |
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname | |
| if Config.Framework == 'qbcore' then | |
| -- On qbcore we cannot assume an ESX-style `users` table; fall back to identifier | |
| if LabsData[labId] and LabsData[labId]['Owner'] then | |
| extraInfo.owner = tostring(LabsData[labId]['Owner']) | |
| else | |
| extraInfo.owner = 'UNKNOWN OWNER' | |
| end | |
| else | |
| -- ESX-style lookup from `users` table, with safe handling for missing rows | |
| local pInfo = MySQL.query.await('SELECT firstname, lastname FROM users WHERE identifier = @identifier', { | |
| ['@identifier'] = LabsData[labId]['Owner'] | |
| }) | |
| if pInfo and pInfo[1] and pInfo[1].firstname and pInfo[1].lastname then | |
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname | |
| elseif LabsData[labId] and LabsData[labId]['Owner'] then | |
| -- Fallback to identifier if name fields are not available | |
| extraInfo.owner = tostring(LabsData[labId]['Owner']) | |
| else | |
| extraInfo.owner = 'UNKNOWN OWNER' | |
| end | |
| end |
server/sv_main.lua
Outdated
| while not pInfo do | ||
| Citizen.Wait(100) | ||
| end | ||
|
|
||
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname |
There was a problem hiding this comment.
The while not pInfo do Wait(100) loop is redundant after MySQL.query.await(...) (it already waits) and can spin forever if the query returns nil/false. Prefer a single check for pInfo/pInfo[1] and return a fallback owner string when the DB lookup fails.
| while not pInfo do | |
| Citizen.Wait(100) | |
| end | |
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname | |
| if pInfo and pInfo[1] then | |
| extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname | |
| else | |
| extraInfo.owner = 'PROPRIETARIO SCONOSCIUTO' | |
| end |
| exports['KmF_Lib']:DiscordLog('lab', 'SALVATAGGIO LABORATORI', | ||
| 'Laboratori salvati sul database' .. | ||
| '\nOrario: ' .. os.date('%d/%m/%Y %H:%M:%S') .. | ||
| '\nNumero laboratori: ' .. #LabsData) | ||
|
|
There was a problem hiding this comment.
#LabsData will always be 0 here because LabsData is a map keyed by LabID strings, not an array. Since this PR already introduces TableSize, consider using it for the log so the saved lab count is accurate.
|
@copilot open a new pull request to apply changes based on the comments in this thread |
Co-authored-by: Fonlogen <25137574+Fonlogen@users.noreply.github.com> Agent-Logs-Url: https://github.com/Fonlogen/KmF_Lab/sessions/a03303f0-90d9-48de-a98b-9c82d7b4d3b9
Fix server-side abstraction bugs: carry check recursion, inventory aggregation, spy info, global leaks
Motivation
esx/qbcoreeqs_inventory/ox_inventory/qb_inventory).ESXeqs-inventoryper poter usareQBCore/QBInventoryoox_inventorytramiteconfig.lua(Config.Framework/Config.Inventory).Description
config.lua:Config.Framework(default'esx') eConfig.Inventory(default'qs_inventory').client/cl_main.lua): introdotte funzioni di astrazione framework-agnostiche (EnsureFramework,TriggerFrameworkCallback,ShowNotification,IsPlayerLoaded) e uso dinamico dell'evento di player-load; tutte le chiamate client-server e notifiche sono state migrate a queste funzioni.server/sv_main.lua): introdotto bootstrap perCore(QBCore o ESX) e helper condivisi per player/inventory/framework:GetPlayer,GetPlayerByIdentifier,RegisterFrameworkCallback,ShowFrameworkNotification,GetInventoryItemList,GetItemCount,RemoveInventoryItem,AddInventoryItem,CanCarryItem,GetPlayerInventory,TableSize,DumpTable.RegisterFrameworkCallback) e tutte le operazioni di inventario per rispettareConfig.Inventory(supporto perqs-inventory,ox_inventory,qb_inventory).Testing
compatibility abstraction checks passed).rgper verificare sostituzioni di chiamate ESX/inventory (verificato a livello di grep).luac -psuclient/cl_main.luaeserver/sv_main.luanon eseguito perchéluacnon è installato nell'ambiente (segnalato nelle note).Add configurable framework and inventory supporte PR preparata (modificati:config.lua,client/cl_main.lua,server/sv_main.lua).Codex Task