Custom Inventory System
Connect another inventory to JG Mechanic
Use this page to connect an inventory that isn't built into JG Mechanic. You will add an adapter to the editable framework files. The adapter passes JG Mechanic's item and stash data to your inventory's API.
You need the API documentation for your inventory. It must provide server-side ways to register usable items, count items, add items and remove items. You also need a client or server API for opening stashes.
A server event that doesn't return a value can't be used to count items. Framework.Server.HasItem needs the count before it can continue.
What the adapter must do
JG Mechanic already provides the values in the table below. Your code only needs to pass them to the matching inventory API and normalise its result.
| JG Mechanic function | Runs on | Values available | Required result |
|---|---|---|---|
Framework.Server.RegisterUsableItem | Server | itemName, cb | Run cb(src) when the player uses the item |
Framework.Server.HasItem | Server | src, itemName, qty | Set itemCount to the amount the player has |
Framework.Server.GiveItem | Server | src, itemName, qty | Add the items, or return false when the inventory rejects them |
Framework.Server.RemoveItem | Server | src, itemName, qty | Remove the items, or return false when the inventory rejects them |
Framework.Client.OpenInventoryStash | Client | stashId, stashName | Open the named stash |
Framework.Server.OpenInventoryStash | Server | src, stashId, stashName | Open the named stash when your inventory uses a server API |
src is the player's server ID. Keep that value separate from a character ID, licence or inventory owner ID unless your inventory API specifically requires one of those values.
The examples use these placeholder export names:
CreateUsableItemGetItemCountAddItemRemoveItemOpenStashRegisterStash
Replace each placeholder with the exact export or event from your inventory's documentation. Keep the JG Mechanic variable names shown in the examples.
Build the adapter
Find the inventory APIs
Open your inventory's API documentation and find the server functions for:
- registering a usable item
- getting an item or item count for a player
- adding an item
- removing an item
Then find its stash documentation. Check whether stashes open on the client or server and whether they must be registered before they can open.
Pay attention to argument order and return values. For example, your inventory documentation might show:
local count = exports["inventory-name"]:GetItemCount(playerId, item)The same call inside JG Mechanic uses its existing variable names:
itemCount = exports[Config.CustomInventory]:GetItemCount(src, itemName) or 0Here, playerId becomes src and item becomes itemName. or 0 makes a missing item count as zero instead of nil.
Configure the inventory resource
Back up these files before editing them:
config/config.luaframework/sv-functions.luaframework/cl-functions.luaserver/sv-shops-stashes.lua
Keep the backup outside the jg-mechanic folder.
Open config/config.lua. Set Config.Inventory to custom, then add Config.CustomInventory directly below it:
Config.Inventory = "custom"
Config.CustomInventory = "your-inventory-folder"Replace your-inventory-folder with the exact resource folder name. Config.CustomInventory is used by the adapter code below. JG Mechanic doesn't read it anywhere else.
Make sure the inventory starts before JG Mechanic in server.cfg:
ensure your-inventory-folder
ensure jg-mechanicIf either resource is started through a folder such as ensure [standalone], keep that existing line. The inventory still needs to start first.
Register usable items
Open framework/sv-functions.lua and find:
function Framework.Server.RegisterUsableItem(itemName, cb)Inside that function, find the existing branch that starts with:
elseif Config.Framework == "QBCore" or Config.Inventory == "qb-inventory" thenPaste this block directly above that branch:
elseif Config.Inventory == "custom" then
exports[Config.CustomInventory]:CreateUsableItem(itemName, function(src)
cb(src)
end)Replace CreateUsableItem with your inventory's server export. The callback must pass the player's server ID to cb.
If your inventory supplies the server ID in a table, adapt only the callback. For example, if it calls your handler with data.source:
elseif Config.Inventory == "custom" then
exports[Config.CustomInventory]:CreateUsableItem(itemName, function(data)
cb(data.source)
end)Keep the new elseif aligned with the existing elseif lines. Don't add another end.
Return the item count
In framework/sv-functions.lua, find:
function Framework.Server.HasItem(src, itemName, qty)Inside that function, paste this block directly above the existing QBCore branch:
elseif Config.Inventory == "custom" then
itemCount = exports[Config.CustomInventory]:GetItemCount(src, itemName) or 0Replace GetItemCount and change the argument order if your inventory requires it. This branch must set itemCount to a number. Don't return from the branch. The rest of Framework.Server.HasItem compares itemCount with qty and displays the missing-item notification.
If your inventory returns an item table instead of a number, read the amount from that table:
elseif Config.Inventory == "custom" then
local item = exports[Config.CustomInventory]:GetItem(src, itemName)
itemCount = item and item.amount or 0Replace GetItem and item.amount with the names used by your inventory.
If the inventory only returns true or false from a HasItem export, pass qty to it and convert the result:
elseif Config.Inventory == "custom" then
local hasItem = exports[Config.CustomInventory]:HasItem(src, itemName, qty)
itemCount = hasItem and qty or 0Add items
In framework/sv-functions.lua, find:
function Framework.Server.GiveItem(src, itemName, qty)Inside that function, paste this block directly above the existing QBCore branch:
elseif Config.Inventory == "custom" then
local added = exports[Config.CustomInventory]:AddItem(src, itemName, qty)
if added == false then return false endReplace AddItem and change the argument order to match your inventory.
The == false check is intentional. It supports inventories that return false when full and inventories that don't return a value on success. If your API uses a different failure value, convert that result to false here.
Don't add return true to this branch. The function already returns true after the inventory branches.
Remove items
In framework/sv-functions.lua, find:
function Framework.Server.RemoveItem(src, itemName, qty)This function already calls Framework.Server.HasItem before trying to remove anything. Inside the inventory branches, paste this block directly above the existing QBCore branch:
elseif Config.Inventory == "custom" then
local removed = exports[Config.CustomInventory]:RemoveItem(src, itemName, qty)
if removed == false then return false endReplace RemoveItem and change the argument order to match your inventory. As with GiveItem, convert the inventory's failure result to false and let the existing function return true after a successful removal.
Open stashes
Use one of the two options below. Don't add both.
Client API
Use this option when your inventory documentation opens a stash from client code.
Open framework/cl-functions.lua and find:
function Framework.Client.OpenInventoryStash(stashId, stashName)Paste this block directly above the final else in that function:
elseif Config.Inventory == "custom" then
exports[Config.CustomInventory]:OpenStash(stashId, stashName)Replace OpenStash and its arguments with the client API from your inventory. Use stashId as the unique stash identifier and stashName as the display label.
Server API
Use this option when your inventory opens a stash from server code.
In framework/cl-functions.lua, paste this block directly above the final else in Framework.Client.OpenInventoryStash:
elseif Config.Inventory == "custom" then
TriggerServerEvent("jg-mechanic:server:open-inventory-stash", stashId, stashName)The event already exists in JG Mechanic. Don't register another copy.
Then open framework/sv-functions.lua and find:
function Framework.Server.OpenInventoryStash(src, stashId, stashName)Paste this block directly above the final else:
elseif Config.Inventory == "custom" then
exports[Config.CustomInventory]:OpenStash(src, stashId, stashName)Replace OpenStash and its arguments with the server API from your inventory. If the inventory uses a server event, replace the export line with its TriggerEvent call.
Register stashes if required
Skip this step when your inventory creates a stash the first time it opens.
If the inventory has a separate RegisterStash API, open server/sv-shops-stashes.lua. Find the CreateThread under the -- Stashes comment. It currently registers stashes for ox_inventory.
Add this elseif branch after the complete ox_inventory branch and before the end that closes the inventory check:
elseif Config.Inventory == "custom" then
for mechanicId, location in pairs(Config.MechanicLocations) do
for stashIndex, stash in ipairs(location.stashes or {}) do
local stashId = mechanicId .. "_" .. stash.name:gsub(" ", "_"):lower() .. "_" .. stashIndex
local label = stash.name
local slots = stash.slots or 50
local maxWeight = stash.weight or 50000
exports[Config.CustomInventory]:RegisterStash(stashId, label, slots, maxWeight)
end
endReplace RegisterStash and its argument order with the inventory's server API. The generated stashId matches the ID passed to Framework.Client.OpenInventoryStash.
Some inventories require coordinates, an owner or allowed groups when registering a stash. The current stash coordinates are available as stash.coords. Add the other values only when your inventory API requires them.
Add the items and images
The source item definitions are in jg-mechanic/install/inventory. Start with the file closest to your inventory's item format:
ox_inventory-items.luafor table-based item definitionsqb-core-items.luafor QBCore-style item definitionsesx-items.sqlfor database item definitionsquasar-inventory-items.luafor Quasar-style definitions
Convert the entries into your inventory's format. Keep each item name unchanged because JG Mechanic refers to those exact names in code.
If your inventory has a usable-item flag, enable it for:
lighting_controllerstancing_kitnitrous_bottlecleaning_kitrepair_kitduct_tapemechanic_tablet
Copy every image from jg-mechanic/install/inventory/images into the item-image folder used by your inventory.
Check the adapter
Before restarting, check each file:
config/config.luacontainsConfig.Inventory = "custom"and the correct resource folder inConfig.CustomInventory.framework/sv-functions.luahas custom branches inRegisterUsableItem,HasItem,GiveItemandRemoveItem.framework/cl-functions.luahas one custom stash branch.framework/sv-functions.luaalso has a custom stash branch when you chose the server option.server/sv-shops-stashes.luahas a custom registration branch only when the inventory requires one.- Every new
elseiflines up with the existing branches and remains inside its function. - You didn't add or remove an
endaround an inventory branch. - Every placeholder export name has been replaced unless it is the real name used by your inventory.
Search all four files for Config.Inventory == "custom". You should find at least five branches: four server item branches and one client stash branch.
Test each operation
Fully restart your server and watch the console for errors from jg-mechanic or your inventory.
Test the adapter in this order:
- Give yourself a
repair_kitand use it. The repair action should start. This tests usable-item registration and the callback'ssrcvalue. - Give yourself one
engine_oil, then service a vehicle. JG Mechanic should detect it and remove it. This tests both the count and remove calls. - Buy an item from a mechanic shop. The item should appear in your inventory. Fill the inventory first and repeat the purchase to confirm a rejected add returns
false. - Open a mechanic stash. Deposit an item, close the stash, then open it again. The same item should still be there.
- Restart the server and open the same stash again. If it is empty or has a different ID, compare the ID used by the registration and open calls.
Troubleshooting
The export name is wrong, the inventory isn't started yet or the API runs on the other side. Confirm the spelling and whether the documentation labels it client or server. Make sure the inventory starts before jg-mechanic.
Log or print the value assigned to itemCount inside the custom HasItem branch. It must be a number. Check whether the inventory expects itemName before src, or returns an item table instead of a count.
The custom RegisterUsableItem callback must call cb with the player's server ID. Check the callback data from your inventory and convert it to cb(src).
Check the custom branch in Framework.Server.RemoveItem. Confirm its argument order and make sure the inventory's failure result is converted to false.
Confirm Config.Inventory is set to custom. The client branch belongs directly above the final else in Framework.Client.OpenInventoryStash. If you chose the server option, add the matching branch above the final else in Framework.Server.OpenInventoryStash.
Use stashId as the unique ID in both the registration and open calls. Don't use stashName as the ID. The display name can change, but the ID must stay the same.
The custom branch is outside the existing if chain or an extra end was added. Move the custom elseif directly above the named existing branch or final else. Keep it aligned with the other elseif lines.
Last updated 26 July 2026