Inventory System

CityRP's inventory system provides a robust, networked container system for managing items. This document covers the modern inventory API and development patterns with practical examples from the codebase.

Overview

The inventory system consists of: - Inventory Objects: Containers that hold items with size limits - Networking: Automatic synchronization between server and clients - Item Management: Creation, removal, and transfer of items - Access Control: Receiver-based access to inventory data

Core Concepts

Inventory Types

- Player Inventories: Personal storage for players - World Inventories: Containers, storage boxes, etc. - Infinite Inventories: Special inventories with unlimited size (use sparingly) - Temporary Inventories: Short-lived containers for specific purposes

Networking Modes

-- Example: Available sync modes
INV_SYNC_FULL   -- Complete inventory synchronization
INV_SYNC_ITEMS  -- Only sync items
INV_SYNC_META   -- Only sync metadata (size, data, etc.)

Creating Inventories

Standard Inventory Creation

-- Example: Server-side only
cityrp.inventory.Create(owner, size, invType, callback, ...)

-- Examples:
-- Player inventory (automatic - handled by system)
cityrp.inventory.Create(client, cityrp.configuration["Inventory Size"], "player",
    cityrp.inventory.CreatePlayerInitial)

-- Bank inventory (real example from bank plugin)
cityrp.inventory.Create(client, cityrp.bank.configuration["Default"], "bank", function(sid64, inv)
    -- Create initial item
    cityrp.item.Create(inv, "chinese", client, 1)

    -- Set up player reference
    client:SetNetVar("cityrp_BankID", inv:GetID())
    client._BankID = inv:GetID()

    -- Update database
    local update = mysql:Update("players")
    update:Update("_BankID", inv:GetID())
    update:Where("_SteamID64", client:SteamID64())
    update:Execute()
end)

-- World container (using "0" as owner for non-player inventories)
cityrp.inventory.Create("0", 25, "container", function(owner, inv)
    -- Set up container-specific properties
    inv:SetData("containerType", "storage_box")
    inv:SetData("containerEntity", containerEnt:EntIndex())
end)

Infinite Inventories

-- For world items that don't need size limits
-- WARNING: Use sparingly, these don't auto-load items
cityrp.inventory.CreateInf("world_drops")

-- World inventory is automatically created as ID 0
local worldInv = cityrp.inventory.GetWorldInv()

Loading Existing Inventories

-- Load inventory by ID with callback
cityrp.inventory.Load(inventoryID, function(inv)
    if inv then
        -- Inventory loaded successfully
        inv:AllowActions(true)
        inv:AllowTransfers(true)
    end
end)

Inventory Management

Getting Inventories

-- Get inventory by ID
local inv = cityrp.inventory.Get(inventoryID)

-- Get player's inventory
local playerInv = client:GetInventory()

-- Check if inventory exists
if IsValid(inv) then
    -- Inventory is valid and loaded
end

Item Management

Creating Items

-- Create item in inventory (most common usage)
local success, error = inv:Create(uniqueID, amount, client, force)

if success then
    -- Item created successfully
else
    client:Notify("Failed to create item: " .. error)
end

-- Real world examples from codebase:
-- Bank system creating initial item
cityrp.item.Create(inv, "chinese", client, 1)

-- Creating stackable items with specific quantity
if itemBase.stackable then
    cityrp.item.Create(inv, uniqueID, client, quantity)
else
    for i = 1, quantity do
        cityrp.item.Create(inv, uniqueID, client)
    end
end

-- Force creation (ignores space limits)
inv:Create("emergency_kit", 1, client, true)

Removing Items

-- Remove specific amount
local success, error = inv:Remove(item, amount)

-- Remove entire item
item:Remove()

-- Remove by unique ID and amount
local items = inv:GetItems("health_potion")
if items and #items > 0 then
    inv:Remove(items[1], 2) -- Remove 2 health potions
end

Item Queries

-- Get all items of a type (returns table of item objects)
local items = inv:GetItems("weapon_pistol")

-- Get item count (commonly used)
local count = inv:GetItemCount("health_potion")

-- Check if inventory has item (boolean check)
if inv:HasItem("keycard") then
    -- Player has keycard
end

-- Real examples from codebase:
-- Check item count for requirements
local amountInInv = owner:GetItemCount(itemUniqueID)
if amountInInv < requiredAmount then
    return false, "Not enough items"
end

-- Check for specific items in crafting
if not inventory:HasItem(uniqueID) or client:GetItemCount(uniqueID) < amount then
    return false, "Missing required materials"
end

Item Transfers

-- Transfer item between inventories (most common pattern)
local success, error = sourceInv:Transfer(item, targetInv, amount)

-- Real example from item pickup system
local state, msg = self._Item:Transfer(activator:GetInventory(), self._Amount or 1)
if not state then
    activator:Notify("Failed to pick up item: " .. msg)
    return false
end

-- Item-level transfer (calls inventory Transfer internally)
local status, reason = item:Transfer(targetInv, amount)
if not status then
    client:Notify("Transfer failed: " .. reason)
end

-- Force transfer (bypasses space and validation checks)
sourceInv:Transfer(item, targetInv, amount, true)

Inventory Properties

Size Management

-- Get current size and max size
local currentSize = inv:GetSize()
local maxSize = inv:GetMaxSize()

-- Check if item can fit
if inv:CanFit(itemSize) then
    -- Can fit item
end

-- Set maximum size (server only)
inv:SetMaxSize(100)

Ownership

-- Get owner
local owner = inv:GetOwner() -- Returns Player object or nil

-- Set owner (server only)
inv:SetOwner(client)
inv:SetOwner(client:SteamID64()) -- Using SteamID64 string

Data Storage

-- Set inventory data (server only)
inv:SetData("containerType", "safe")
inv:SetData("locked", true)

-- Get inventory data
local containerType = inv:GetData("containerType")
local isLocked = inv:GetData("locked", false) -- Default value

Networking

Access Control

-- Add receiver (who can see this inventory)
inv:AddReceiver(client)

-- Remove receiver
inv:RemoveReceiver(client)

-- Get all receivers
local receivers = inv:GetReceivers()

Manual Synchronization

-- Sync full inventory to all receivers
inv:NetSync(INV_SYNC_FULL)

-- Sync only to specific client
inv:NetSync(INV_SYNC_FULL, client)

-- Sync only items
inv:NetSync(INV_SYNC_ITEMS)

-- Sync only metadata
inv:NetSync(INV_SYNC_META)

Inventory Snapshots

-- Send read-only snapshot to client (used for viewing other player inventories)
-- Real example from character system:
local invID = char:GetInventory():GetID()
if invID then
    cityrp.inventory.SendSnapshot(client, invID, true)
end

Hooks and Events

Available Hooks

-- Called when inventory is created
hook.Add("OnInventoryCreated", "MyPlugin", function(inv, invType)
    if invType == "container" then
        -- Set up container properties
    end
end)

-- Called when inventory is loaded (real usage from codebase)
hook.Add("InventoryLoaded", "MyPlugin", function(inv)
    -- Inventory has finished loading all items
    inv:NetSync(INV_SYNC_FULL)
end)

-- Called when inventory is updated (client-side mainly)
hook.Add("InventoryUpdate", "MyPlugin", function(invID, inv, itemID, item)
    -- Handle inventory changes - item may be nil for bulk updates
end)

-- Called when creating player inventories (real bank plugin example)
hook.Add("CreatePlayerInventories", "MyPlugin", function(client, mainInv, legacyInv)
    -- Called after player's main inventory is created
    -- Use this to create additional inventories like bank storage
    if not client:IsBot() then
        cityrp.bank.Create(client, legacyInv)
    end
end)

-- Called when loading existing player inventories
hook.Add("LoadPlayerInventories", "MyPlugin", function(client, inv)
    -- Inventory has been loaded from database
    inv:AllowActions(true)
    inv:AllowTransfers(true)
end)

Player Integration

Player Metatable Methods

-- Get player's main inventory
local inv = client:GetInventory()

-- Check if player has item (real usage examples from codebase)
if client:HasItem("keycard") then
    -- Player has keycard
end

if not client:HasItem("weed") then
    return false, "You don't have any weed to sell"
end

-- Get item count (commonly used for requirements)
local amountInInv = client:GetItemCount("health_potion")
if amountInInv < requiredAmount then
    return false, "Not enough health potions"
end

-- Check if player can fit item
if client:CanFit(itemSize) then
    -- Player has space for the item
end

-- Real examples from item pickup system
if not activator:GetInventory():CanFitItem(self._Item) then
    activator:Notify("You don't have enough space in your inventory to pick up this item.")
    return false
end

Best Practices

Performance

- Use appropriate sync modes (INV_SYNC_ITEMS vs INV_SYNC_FULL) - Limit receivers to only necessary clients - Use noSave parameter for frequently changing data - Batch operations when possible

Data Management

- Store inventory-specific data using SetData()/GetData() - Use meaningful inventory types for easier identification - Implement proper cleanup in hooks

Error Handling

local success, error = inv:Create("item", 1, client)
if not success then
    client:Notify("Failed to create item: " .. error)
    return
end

-- Real error handling patterns from codebase
local state, msg = item:Transfer(targetInv, amount)
if not state then
    activator:Notify("Transfer failed: " .. msg)
    return false
end

Memory Management

- Clean up temporary inventories - Remove receivers when no longer needed - Use infinite inventories sparingly

Common Patterns

Container Implementation

-- Create container inventory
local containerInv = cityrp.inventory.Create("0", 50, "container", function(owner, inv)
    inv:SetData("containerEntity", containerEnt:EntIndex())
    inv:SetData("position", containerEnt:GetPos())
end)

-- Add access for nearby players
for _, client in ipairs(ents.FindInSphere(containerEnt:GetPos(), 100)) do
    if IsPlayer(client) then
        containerInv:AddReceiver(client)
    end
end

Bank System

-- Real bank system implementation from bank plugin
function cityrp.bank.Create(client, legacyInv)
    cityrp.inventory.Create(client, cityrp.bank.configuration["Default"], "bank", function(sid64, inv)
        -- Create initial item
        cityrp.item.Create(inv, "chinese", client, 1)

        -- Handle legacy inventory conversion
        if legacyInv then
            for uniqueID, quantity in pairs(legacyInv) do
                local itemBase = cityrp.item.stored[uniqueID]
                if itemBase.stackable then
                    cityrp.item.Create(inv, uniqueID, client, quantity)
                else
                    for i = 1, quantity do
                        cityrp.item.Create(inv, uniqueID, client)
                    end
                end
            end
        end

        -- Set up player networking
        client:SetNetVar("cityrp_BankID", inv:GetID())
        client._BankID = inv:GetID()

        -- Update database
        local update = mysql:Update("players")
        update:Update("_BankID", inv:GetID())
        update:Where("_SteamID64", client:SteamID64())
        update:Execute()
    end)
end

-- Open bank interface (real example)
cityrp.inventory.OpenMenu(client, client:GetInventory(), client:GetBankInventory())

Temporary Trading

-- Create temporary trade inventory
local tradeInv = cityrp.inventory.Create("0", 20, "trade", function(owner, inv)
    inv:SetData("tradePartners", {client1:SteamID64(), client2:SteamID64()})
    inv:AddReceiver(client1)
    inv:AddReceiver(client2)

    -- Auto-cleanup after 5 minutes
    timer.Simple(300, function()
        if IsValid(inv) then
            inv:Delete()
        end
    end)
end)

Advanced Topics

Custom Inventory Types

When creating custom inventory types, use meaningful names and implement appropriate hooks:

-- Register custom inventory behavior
hook.Add("OnInventoryCreated", "CustomVault", function(inv, invType)
    if invType == "vault" then
        inv:SetData("securityLevel", 5)
        inv:SetData("accessCode", math.random(1000, 9999))

        -- Vaults have special size calculation
        inv:SetMaxSize(inv:GetMaxSize() * 2)
    end
end)

Inventory Validation

Implement validation for inventory operations:

-- Validate item creation
hook.Add("CanCreateItem", "InventoryValidation", function(inv, uniqueID, amount, client)
    if inv:GetData("locked") then
        return false, "Inventory is locked"
    end

    if inv.type == "bank" and not client:HasAccess("bank") then
        return false, "No bank access"
    end

    return true
end)

Performance Optimization

For high-traffic inventories:

-- Batch updates
inv:SetData("batchMode", true, true) -- noSave = true
inv:SetData("lastUpdate", os.time(), true)
inv:SetData("updateCount", (inv:GetData("updateCount", 0) + 1), true)

-- Save all at once
inv:SaveData()

Migration from Legacy System

If migrating from the old inventory system:

-- Legacy compatibility layer
function Player:UpdateInvLegacy(uniqueID, amount, client, force)
    local inv = self:GetInventory()
    if amount > 0 then
        return inv:Create(uniqueID, amount, client, force)
    else
        local items = inv:GetItems(uniqueID)
        if items and #items > 0 then
            return inv:Remove(items[1], math.abs(amount))
        end
    end
end

Troubleshooting

Common Issues

Inventory not syncing:

  • Check if client is added as receiver: inv:AddReceiver(client)
  • Verify network sync mode: inv:NetSync(INV_SYNC_FULL)
  • Ensure inventory is valid: IsValid(inv)

Items not appearing:

  • Confirm item was created successfully: check return values
  • Verify inventory has space: inv:CanFit(itemSize)
  • Check item base exists: cityrp.item.get(uniqueID)

Performance issues:

  • Use appropriate sync modes
  • Limit receiver count

Debug Commands

-- Debug inventory state
concommand.Add("debug_inventory", function(client, cmd, args)
    if not client:IsAdmin() then return end

    local inv = client:GetInventory()
    print("Inventory ID: " .. inv:GetID())
    print("Size: " .. inv:GetSize() .. "/" .. inv:GetMaxSize())
    print("Items: " .. table.Count(inv:GetItems()))
    print("Receivers: " .. #inv:GetReceivers())
end)