Skip to content

Add configurable framework and inventory support (ESX/QBCore + qs/ox/qb_inventory)#1

Open
Fonlogen wants to merge 4 commits intomainfrom
codex/refactor-ox_inventory-script-for-config-options
Open

Add configurable framework and inventory support (ESX/QBCore + qs/ox/qb_inventory)#1
Fonlogen wants to merge 4 commits intomainfrom
codex/refactor-ox_inventory-script-for-config-options

Conversation

@Fonlogen
Copy link
Copy Markdown
Owner

Motivation

  • Rendere la risorsa compatibile con più framework/inventory senza hardcode (supporto configurabile per esx / qbcore e qs_inventory / ox_inventory / qb_inventory).
  • Evitare dipendenze fisse su ESX e qs-inventory per poter usare QBCore/QBInventory o ox_inventory tramite config.lua (Config.Framework / Config.Inventory).
  • Sistemare il controllo dell'identifier durante l'acquisto del laboratorio per evitare creazioni errate di lab.

Description

  • Aggiunte due opzioni in config.lua: Config.Framework (default 'esx') e Config.Inventory (default 'qs_inventory').
  • Client (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 (server/sv_main.lua): introdotto bootstrap per Core (QBCore o ESX) e helper condivisi per player/inventory/framework: GetPlayer, GetPlayerByIdentifier, RegisterFrameworkCallback, ShowFrameworkNotification, GetInventoryItemList, GetItemCount, RemoveInventoryItem, AddInventoryItem, CanCarryItem, GetPlayerInventory, TableSize, DumpTable.
  • Rifattorizzate tutte le callback server per usare l'astrazione (RegisterFrameworkCallback) e tutte le operazioni di inventario per rispettare Config.Inventory (supporto per qs-inventory, ox_inventory, qb_inventory).
  • Corretto il controllo nell'acquisto laboratorio per verificare l'identifier del player corrente anziché una variabile non inizializzata.
  • Aggiornate le interazioni NUI/client (buyLab, craft, deposit/withdraw, hire/fire/promote/degrade, daily chest, spy shop, ecc.) per chiamare le callback unificate.

Testing

  • Eseguito un controllo automatico che verifica l'assenza del bootstrap ESX hardcoded nei file client/server con uno script Python; il controllo è passato (compatibility abstraction checks passed).
  • Cercate/rilevate le occorrenze rilevanti via rg per verificare sostituzioni di chiamate ESX/inventory (verificato a livello di grep).
  • Tentativo di validazione sintattica Lua con luac -p su client/cl_main.lua e server/sv_main.lua non eseguito perché luac non è installato nell'ambiente (segnalato nelle note).
  • Commit delle modifiche eseguito con messaggio Add configurable framework and inventory support e PR preparata (modificati: config.lua, client/cl_main.lua, server/sv_main.lua).

Codex Task

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Framework and Config.Inventory options 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.

Comment on lines +1404 to +1415
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])
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +148
local player = GetPlayer(src)
return player and player.canCarryItem and player.canCarryItem(item, amount) or false
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +1013 to +1027
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)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +166
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
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +183
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
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1346 to +1354
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
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +1350 to +1354
while not pInfo do
Citizen.Wait(100)
end

extraInfo.owner = pInfo[1].firstname .. ' ' .. pInfo[1].lastname
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +1277 to +1281
exports['KmF_Lib']:DiscordLog('lab', 'SALVATAGGIO LABORATORI',
'Laboratori salvati sul database' ..
'\nOrario: ' .. os.date('%d/%m/%Y %H:%M:%S') ..
'\nNumero laboratori: ' .. #LabsData)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#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 uses AI. Check for mistakes.
@Fonlogen
Copy link
Copy Markdown
Owner Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Mar 23, 2026

@Fonlogen I've opened a new pull request, #2, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits March 23, 2026 19:28
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants