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)