Inventory¶
The inventory systems diverge more than any other core subsystem between Helix and Parallax. Helix models inventories as a fixed w × h grid with every item occupying rectangular cells. Parallax models them as a flat weight-capped bag — no grid, no cell positions. This is the single most disruptive port for UI code and for items that participated in grid-specific logic (bags, shipments, containers).
Table of Contents¶
- The Model Change
- When You Can Ignore This Chapter
- API Surface Comparison
- Inventory Creation and Restore
- Item Placement
- Querying
- Receivers and Sync
- Bags and Sub-Inventories
- UI Implications
- Per-Item Data Migration
- Pitfalls
The Model Change¶
Helix inventory:
- Constructed with explicit width and height (ix.inventory.Create(w, h, id)).
- Internal slots[x][y] 2D table; items occupy w × h cells each.
- inventory:FindEmptySlot(w, h) finds a free rectangle; items refuse to add if no rectangle fits.
- Capacity = total free cells.
- UI displays a grid with drag-and-drop between cells.
Parallax inventory:
- Constructed with a maxWeight value (ax.inventory:Create({ maxWeight = N }, cb)).
- Internal items[itemID] flat map; each item contributes its weight.
- inventory:CanStoreWeight(w) returns true if the item's weight fits in remaining capacity.
- Capacity = maxWeight - sum(item:GetWeight()).
- UI displays a list/grid decoupled from cell positions.
The weight model is strictly simpler and it's better for schemas that want continuous scaling (a pistol weighs different from a rifle even if both are "2x1" in Helix), but it trades away visual spatial reasoning in the inventory UI.
When You Can Ignore This Chapter¶
If your Helix schema used the default inventory UI and bag item and did not write any of the following, you can skim this chapter and accept Parallax's defaults:
- Custom derma panels that called
:GetSize(),:FindEmptySlot, or:GetItemAt(x, y). - Items that cared about their position in inventory (e.g. a "belt slot at y=0" mechanic).
- Plugins that created their own sub-inventories with custom dimensions (shipments, containers, vehicle trunks).
Most core item-use logic (pistol.OnRun, bread.OnRun) doesn't care about the grid — it only uses item:SetData, client:Give, etc., which all port cleanly.
API Surface Comparison¶
| Operation | Helix | Parallax |
|---|---|---|
| Get by ID | ix.item.inventories[id] |
ax.inventory.instances[id] |
| Get ID | inventory:GetID() |
inventory:GetID() |
| Get size | inventory:GetSize() returns (w, h) |
inventory:GetMaxWeight() returns one number |
| Get items | inventory:GetItems(onlyMain) |
inventory:GetItems() |
| Has item | inventory:HasItem(uid, data) |
inventory:HasItem(identifier) |
| Has item of base | inventory:HasItemOfBase(baseID) |
inventory:HasItemOfBase(baseName) |
| Get item by ID | inventory:GetItemByID(id, onlyMain) |
inventory:GetItemByID(id) |
| Count of class | inventory:GetItemCount(uid, onlyMain) |
inventory:GetItemCount(class) |
| Owner | inventory:GetOwner() |
inventory:GetOwner() |
| Set owner | inventory:SetOwner(id, fullUpdate) |
inventory:SetOwner(owner) |
| Iterate | for item, _ in inventory:Iter() |
for id, item in pairs(inventory:GetItems()) |
| Get at (x, y) | inventory:GetItemAt(x, y) |
(no equivalent) |
| Find empty slot | inventory:FindEmptySlot(w, h, onlyMain) |
inventory:CanStoreWeight(w) |
| Add | inventory:Add(uid, qty, data, x, y, noRep) |
inventory:Add(class, qty, data) |
| Remove | inventory:Remove(id, ...) |
inventory:Remove(id) |
| Receivers | inventory:GetReceivers() |
inventory:GetReceivers() |
| Add receiver | inventory:AddReceiver(client) |
inventory:AddReceiver(receiver) |
| Remove receiver | inventory:RemoveReceiver(client) |
inventory:RemoveReceiver(receiver) |
| Sync | inventory:Sync(receiver) |
ax.inventory:Sync(inventory) |
| Bags list | inventory:GetBags() |
(no equivalent) |
The naming is mostly parallel; the semantic shift is around size/placement.
Inventory Creation and Restore¶
Helix:
-- Create a new inventory of fixed size
ix.inventory.Create(w, h, id) -- synchronous
-- Restore an existing inventory from the database
ix.inventory.Restore(invID, w, h, cb) -- async with callback
-- Bag inventories have their own IDs registered via:
ix.inventory.Register("bag_type", w, h, isBag)
Parallax:
if ( SERVER ) then
-- Create a new persistent inventory
ax.inventory:Create({ maxWeight = 30 }, function(inventory)
-- inventory is the new object with a database ID
end)
-- Or a temporary, non-persistent inventory (for containers that reset)
local inv = ax.inventory:CreateTemporary({ maxWeight = 100 })
-- Restore on server start happens automatically — no explicit Restore call
end
Note the async-first pattern in Parallax: even creation is a callback-returning function because database inserts are async. Helix occasionally offered synchronous creation that glossed over this.
The automatic restore-on-boot means you don't need to manually walk the database during server start; Parallax loads inventories as needed when characters are selected.
Item Placement¶
Helix:
Parallax:
The Helix noReplication flag is not exposed on Parallax's Add because Parallax pushes sync through ax.inventory:Sync(inventory) which you can opt out of separately.
Querying¶
Port patterns for common queries:
-- "Does this inventory have any bread?"
-- Helix
if ( inventory:HasItem("bread") ) then ... end
-- Parallax
if ( inventory:HasItem("bread") ) then ... end -- same
-- "How many bread are there?"
-- Helix
local n = inventory:GetItemCount("bread")
-- Parallax
local n = inventory:GetItemCount("bread") -- same
-- "Give me all items with base 'weapon'"
-- Helix
local weapons = inventory:GetItemsByBase("base_weapons")
-- Parallax
local weapons = inventory:GetItemsByBase("weapon")
-- "Is the first stack of bread full?" - grid-only concept
-- Helix
local item = inventory:GetItemAt(0, 0)
-- Parallax
-- Not meaningful. Use GetItems and pick by other criteria.
The onlyMain flag on Helix's queries referred to excluding items inside bag sub-inventories. Parallax inventories don't have nested inventories, so this flag has no meaning.
Receivers and Sync¶
Both frameworks track a list of "receivers" — clients who are subscribed to live updates from the inventory. The API is mostly parallel:
-- Adding a client
inventory:AddReceiver(client)
-- Removing
inventory:RemoveReceiver(client)
-- Listing
local list = inventory:GetReceivers()
The difference: Parallax's receivers are added per inventory per operation (e.g. viewing a container adds the client as a receiver; closing the panel removes them). The framework auto-sync-fires on writes through ax.inventory:Sync(inventory) which targets all current receivers.
Helix fired sync separately with inventory:Sync(optionalReceiver) where you could pass a specific client. Parallax syncs to everyone in GetReceivers() — no per-call targeting.
Bags and Sub-Inventories¶
Helix bags work by:
- A bag item has
isBag = true,invWidth,invHeight. OnInstancedcreates a sub-inventory and stores its ID on the item's data.- The bag's
Viewfunction opens the sub-inventory in a popup. - Transferring the bag also transfers its sub-inventory's contents.
Parallax has no built-in bag item and no framework-level nested inventory concept. If your schema relies on bags, you have three options:
Option 1 — Model bags as weight modifiers¶
The simplest port: a "bag" item, when equipped or carried, increases the carrier's maxWeight. You lose the "open the bag in a separate panel" UI but keep the gameplay effect of "carrying a bag lets you carry more".
-- items/bags/sh_backpack.lua
ITEM.name = "Backpack"
ITEM.description = "A sturdy backpack that increases your carrying capacity."
ITEM.model = "models/props_c17/suitcase001a.mdl"
ITEM.category = "Storage"
ITEM.weight = 1.0
ITEM.capacityBonus = 20.0 -- custom field
ITEM:AddAction("equip", {
name = "Equip",
OnRun = function(action, client, item)
local inv = client:GetCharacter():GetInventory()
inv.maxWeight = (inv.maxWeight or 30) + item.capacityBonus
item:SetData("equipped", true)
return false
end,
})
ITEM:AddAction("unequip", {
name = "Unequip",
OnRun = function(action, client, item)
local inv = client:GetCharacter():GetInventory()
inv.maxWeight = (inv.maxWeight or 30) - item.capacityBonus
item:SetData("equipped", nil)
return false
end,
})
Option 2 — Manual sub-inventory¶
Use ax.inventory:Create to make a second persistent inventory owned by the bag item, store its ID on the item's data, and add View as an action that opens a separate inventory panel.
This is close to Helix's behaviour but requires you to: - Serialize/deserialize the sub-inventory ID on the bag item itself. - Handle bag-drop and bag-destroy transitions explicitly (when the bag is removed, its sub-inventory should be either deleted or re-parented). - Implement your own open-in-panel UI since there's no framework convention.
Option 3 — Containers module¶
The cleanest path for "outside a character" storage (lockers, crates, vehicle trunks) is a dedicated module that uses temporary or persistent inventories bound to map entities. See the Parallax source's module patterns for inspiration; this is how most modern Helix container plugins were actually structured anyway.
UI Implications¶
Parallax's inventory UI treats items as cards in a list, not rectangles in a grid. If you ported a Helix derma panel that called inventory:GetSize(), placed items with x, y, or drew a grid overlay, none of that applies. Approaches:
- Use Parallax's built-in inventory UI. Easiest if your schema's aesthetic tolerates it.
- Derive from Parallax's panels. Override
PaintandPaintItemto change visual style without touching data. - Write a fresh panel. Consult the main framework UI docs (
08-UI_THEME_GUIDELINES.md) for the theme system and drag-drop conventions.
One pattern that does carry over: "equip slots" for specific item types (a pistol slot, a melee slot). Both frameworks support this as a character var (weaponEquipped = <itemID>) and an item property (weaponCategory = "sidearm"). The UI and the data model for equip slots are orthogonal to the inventory's bag/grid model.
Per-Item Data Migration¶
When restoring a Helix database into Parallax, legacy items may have x, y, w, h fields in their ax_items.data JSON blob. Parallax ignores them. If the database migration happens in-place:
- Accept the stale fields as harmless metadata.
- Optionally write a one-shot migration that drops them from existing rows:
- New items written after the port omit those fields automatically — Parallax doesn't set them.
Pitfalls¶
- Assuming a grid. UI code, drag-drop code, or item-placement code that references
x, y, w, hwill silently fail to move after port — items go where the weight model puts them, which is nowhere in particular. - Calling
GetSize(). Returns nothing meaningful. UseGetMaxWeight()/GetWeight()instead. - Bag restoration. If you have persistent bag items in the database with attached sub-inventories, you must migrate the relationship or those sub-inventories orphan on port.
- Weight accounting drift. The inventory's cached
GetWeight()is computed; make sure your items report honestGetWeight()values. An item that lies about its weight lets players exceedmaxWeightsilently. - Shipment plugin. The Helix shipment plugin (bulk-spawnable item packs) is grid-specific. The port is a module that spawns individual world items at staggered positions or drops them directly into a target inventory.
Iter()gone. Helix's iterator helper returns items and their positions. Usepairs(inventory:GetItems())in Parallax — the key is the item ID, the value is the item object.
Next: 09-data-persistence.md