Item System
CityRP's item system uses a modern template-based architecture that separates reusable behavior (bases) from item configuration (definitions). This document covers the modern item system for developers with practical examples from the codebase.
Overview
The item system consists of: - Item Bases (Templates): Reusable behavior patterns that define how item types work - Item Definitions: Configuration-only files that create actual items using bases - Action Builder System: Modern replacement for legacy onUse/canUse functions - Lifecycle Hooks: Clean event system for item registration, creation, and player events - Auto-Action Conversion: Backwards compatibility layer for legacy items
Core Concepts
Bases vs Definitions
Item Bases (Templates):
- Define reusable behavior patterns
- Located in gamemode/core/items/base/ or plugins/*/items/
- Use :DisableAutoActions() to prevent legacy conversion
- Define lifecycle hooks and action builders
- Not directly usable by players
Item Definitions:
- Configuration-only files that create actual items
- Use existing bases with specific properties
- Located in gamemode/core/items/ or plugins/*/items/
- Directly usable by players
Available Core Bases
base_item: Root base for all items (basic properties and lifecycle)base_equip: Equipment items with slot managementbase_swep: Weapon items that give SWEPs when equippedbase_place: Items that can be placed as entities in the worldbase_clothing: Clothing items with bodygroup/skin support
Creating Item Bases
Basic Base Structure
--- @class base_mytype : base_item --- @field SetMyProperty fun(self: self, value: type): self Description of property --- @field GetMyProperty fun(self: self): type Gets the property value local ITEM = ITEM:New("base_parent") :SetName("Base Template Name") :SetCost(0) :SetStore(0) :SetSize(1) :SetCategory("Category") --[[@as base_mytype]] :DisableAutoActions() -- Auto-generate getter/setter for custom properties :AutoFunction("MyProperty", "myProperty")(defaultValue) -- Lifecycle hooks function ITEM:ItemRegistered() -- Called once when this base is registered -- Use for one-time setup like registering hooks end function ITEM:ItemCreated(item) -- Called when an instance of this item is created -- Use for per-item initialization end function ITEM:PlayerSpawnHook(client, isInitial) -- Called when a player holding this item spawns end function ITEM:PlayerDeathHook(client, attacker, dmgInfo) -- Called when a player holding this item dies end -- Action definitions local function UseCondition(baseItem, item, client, ...) if not client:Alive() then return false, "You must be alive to use this item" end return true end local function UseAction(baseItem, item, client, ...) client:Notify("Used " .. item:GetName()) -- Return true to consume the item, false to keep it return true end ITEM:AddAction("Use") :SetName("Use Item") :SetIcon("icon16/accept.png") :SetCondition(UseCondition) :SetAction(UseAction) :SetSortRank(10) :Build() ITEM:AutoRegister()
Advanced Base Example
--- @class base_consumable : base_item --- @field SetHealAmount fun(self: self, amount: integer): self Sets heal amount --- @field GetHealAmount fun(self: self): integer Gets heal amount local ITEM = ITEM:New("base_item") :SetName("Base Consumable") :SetCategory("Consumables") :SetSize(1) :DisableAutoActions() -- Custom properties with auto-generated getters/setters :AutoFunction("HealAmount", "healAmount")(25) :AutoFunction("ConsumeSound", "consumeSound")("items/medshot4.wav") :AutoFunction("ConsumeTime", "consumeTime")(2.0) -- Example: Consumption logic with validation local function ConsumeCondition(baseItem, item, client) if not client:Alive() then return false, "You must be alive to consume this" end if client:Health() >= client:GetMaxHealth() then return false, "You are already at full health" end return true end local function ConsumeAction(baseItem, item, client) local healAmount = baseItem:GetHealAmount() local sound = baseItem:GetConsumeSound() -- Heal the player client:SetHealth(math.min(client:Health() + healAmount, client:GetMaxHealth())) -- Play consumption sound if sound then client:EmitSound(sound) end -- Notify player client:Notify("Restored " .. healAmount .. " health") return true -- Consume the item end -- Example: Build consumption action ITEM:AddAction("Use") :SetName("Consume") :SetIcon("icon16/heart.png") :SetCondition(ConsumeCondition) :SetAction(ConsumeAction) :SetSortRank(1) :Build() ITEM:AutoRegister()
Creating Item Definitions
Basic Item Definition
-- Example: Simple item definition using a base ITEM:New("base_consumable") :SetName("Health Potion") :SetDescription("Restores 50 health when consumed") :SetModel("models/props_junk/PopCan01a.mdl") :SetCost(100) :SetSize(2) :SetCategory("Medical") --[[@as base_consumable]] :SetHealAmount(50) --[[@as base_consumable]] :SetConsumeSound("items/medshot4.wav") --[[@as base_consumable]] :AutoRegister()
Complex Item Definition
-- Example: Equipment item with multiple properties ITEM:New("base_equip") :SetName("Police Vest") :SetDescription("Protective vest for law enforcement") :SetModel("models/props_c17/BriefCase001a.mdl") :SetCost(500) :SetSize(4) :SetCategory("Equipment") --[[@as base_equip]] :SetEquipSlot("vest") --[[@as base_equip]] :SetDropOnDeath(DEATH_DROP_EQUIPPED) --[[@as base_equip]] :AutoRegister() -- Example: Real weapon item from codebase style ITEM:New("base_swep") :SetName("Crowbar") :SetDescription("A sturdy crowbar for breaking things") :SetModel("models/weapons/w_crowbar.mdl") :SetCost(150) :SetSize(3) :SetCategory("Weapons") --[[@as base_swep]] :SetSwepClass("cityrp_crowbar") --[[@as base_swep]] :SetEquipSlot("primary") --[[@as base_swep]] :SetDropOnDeath(DEATH_DROP_EQUIPPED) --[[@as base_swep]] :AutoRegister()
Action Builder System
Action Structure
Actions define how players can interact with items. Each action has: - ID: Unique identifier for the action - Name: Display name shown to players - Icon: Icon shown in menus - Condition: Function that determines if action is available - Action: Function that performs the action - Sort Rank: Display order (lower numbers first)
Building Actions
-- Example: Define condition function local function CanUseItem(baseItem, item, client, ...) -- Return true to allow, false + reason to deny if not client:Alive() then return false, "You must be alive" end if client:GetMoveType() == MOVETYPE_NOCLIP then return false, "Cannot use while in noclip" end return true end -- Example: Define action function local function UseItem(baseItem, item, client, ...) -- Perform the action client:Notify("Used " .. item:GetName()) -- Return true to consume item, false to keep it return false end -- Example: Build the action ITEM:AddAction("Use") :SetName("Use Item") :SetIcon("icon16/accept.png") :SetCondition(CanUseItem) :SetAction(UseItem) :SetSortRank(10) :Build()
Standard Action Types
Common Actions:
- Use: Primary item interaction
- Drop: Drop item on ground
- Give: Give item to another player
- Remove: Delete item from inventory
- Equip: Equip item (for equipment)
- Unequip: Unequip item (for equipment)
- Place: Place item as entity (for placeable items)
Custom Actions
-- Example: Custom repair action with realistic validation local function RepairCondition(baseItem, item, client, target) if not IsValid(target) or not target:IsVehicle() then return false, "Must target a vehicle" end if target:GetPos():Distance(client:GetPos()) > 200 then return false, "Too far from vehicle" end if target:GetHealthFraction() >= 1.0 then return false, "Vehicle is already fully repaired" end return true end local function RepairAction(baseItem, item, client, target) -- Repair the vehicle target:SetHealth(target:GetMaxHealth()) client:Notify("Repaired " .. (target.PrintName or target:GetClass())) -- Play repair sound and effect client:EmitSound("weapons/357/357_reload4.wav") return true -- Consume repair kit end -- Example: Build the custom action ITEM:AddAction("Repair") :SetName("Repair Vehicle") :SetIcon("icon16/wrench.png") :SetCondition(RepairCondition) :SetAction(RepairAction) :SetSortRank(5) :Build()
Lifecycle Hooks
Item Registration
function ITEM:ItemRegistered() -- Called once when item base/definition is registered -- Use for one-time setup hook.Add("PlayerSpawn", "MyItem_PlayerSpawn", function(client) -- Global hook logic end) end
Item Creation
function ITEM:ItemCreated(item) -- Called when an instance of this item is created -- Use for per-item initialization item:SetData("createdTime", os.time()) item:SetData("serialNumber", math.random(100000, 999999)) end
Player Events
function ITEM:PlayerSpawnHook(client, isInitial) -- Called when a player holding this item spawns if isInitial then client:Notify("You have a " .. self:GetName()) end end function ITEM:PlayerDeathHook(client, attacker, dmgInfo) -- Called when a player holding this item dies if self:GetDropOnDeath() == DEATH_DROP_ALWAYS then -- Custom drop logic end end
Item Properties
Core Properties
-- Example: Basic properties :SetName("Item Name") :SetDescription("Item description") :SetModel("models/path.mdl") :SetCost(100) :SetSize(2) :SetCategory("Category") -- Example: Advanced properties :SetStackable(true) :SetMaxStack(10) -- Max stack size :SetStore(true) -- Available in store :SetSkin(1) -- Model skin :Untradable() -- Disable drop/give
Stackable Items
Stackable items can be combined into single inventory slots, trading flexibility for simplicity. When an item is stackable, multiple instances are merged into a single stack with a quantity counter.
Key Characteristics
- No unique data: Stackable items cannot store individual data (
SetData/GetDataaffects the entire stack) - Quantity-based: Items are represented by a number rather than individual instances
- Space efficient: Multiple items occupy a single inventory slot
- Batch operations: Can be split, merged, and transferred in specific quantities
Configuration
ITEM:New("base_item") :SetName("Health Potion") :SetStackable(true) -- Enable stacking :SetMaxStack(5) -- Maximum items per stack (defaults to batch value) :SetBatch(5) -- Default batch size for store purchases :AutoRegister()
Legacy Configuration
-- Legacy format (still supported) local ITEM = {} ITEM.name = "Health Kit" ITEM.stackable = true ITEM.maxstack = 5 -- Maximum stack size ITEM.batch = 5 -- Default batch amount
Stack Management Methods
-- Check if item is stackable if item:Stackable() then -- Get current stack amount local amount = item:GetStack() -- Get maximum stack size local maxStack = item:GetMaxStack() -- Get available space in stack local freeSpace = item:GetFreeStackSpace() -- Split stack (server-side only) item:Split(2) -- Creates new stack with 2 items -- Transfer specific amount (server-side only) item:Transfer(targetInventory, 3) -- Transfer 3 items end
Player Actions
Stackable items automatically support quantity-based actions:
- Split: Divide stack into two separate stacks
- Drop: Drop specific amount from stack
- Give: Give specific amount to another player
- Use: Consume single item from stack (typical behavior)
Lifecycle Hooks
-- Called when stacks are split function ITEM:onSplit() -- Handle split behavior (if needed) end -- Called when stacks are merged function ITEM:onMerge() -- Handle merge behavior (if needed) end
Example: Consumable Stackable Item
--- @class health_potion : base_consumable local ITEM = ITEM:New("base_consumable") :SetName("Health Potion") :SetDescription("Restores 50 health when consumed") :SetModel("models/props_junk/PopCan01a.mdl") :SetCost(100) :SetCategory("Medical") :SetStackable(true) :SetMaxStack(5) :SetBatch(5) --[[@as base_consumable]] :SetHealAmount(50) --[[@as base_consumable]] :AutoRegister()
Important Considerations
- Data Loss: Making an item stackable removes the ability to store unique data per item
- Batch vs MaxStack:
batchsets default purchase quantity,maxstacksets stacking limit - Database Impact: Stackable items are more efficient for storage and network sync
- Action Impact: All actions operate on individual items within the stack
- Split Behavior:
onSplitandonMergehooks provide custom split/merge logic if needed
Custom Properties
-- Define custom properties with auto-generated getters/setters :AutoFunction("Durability", "durability")(100) :AutoFunction("Rarity", "rarity")("common") :AutoFunction("Enchantment", "enchantment")(nil) -- Usage in code local durability = item:GetDurability() item:SetDurability(durability - 10)
Item Management
Creating Items
-- Create item in inventory cityrp.item.Create(inventory, uniqueID, creatorID, stack, noSync, callback, ...) -- Examples local inv = client:GetInventory() cityrp.item.Create(inv, "health_potion", client:SteamID64(), 1, false, function(item, inventory) client:Notify("Created " .. item:GetName()) end) -- Create multiple items cityrp.item.Create(inv, "ammo_pistol", client:SteamID64(), 50)
Getting Items
-- Get item by ID local item = cityrp.item.Get(itemID) -- Get item base local base = cityrp.item.GetBase("health_potion") -- Get all items of type from inventory local items = inventory:GetItems("health_potion") -- Get item count local count = inventory:GetItemCount("health_potion")
Item Actions
-- Perform item action item:PlayerAction(client, "Use", ...) -- Check if action is available local canUse, reason = item:CanPlayerAction(client, "Use") if not canUse then client:Notify(reason) end -- Get available actions local actions = item:GetActions() for actionID, action in pairs(actions) do print(actionID, action.name) end
Equipment System
Equipment Slots
-- Define equipment slot :SetSlot("helmet") -- Equipment slot name -- Slot management local equippedItem = client:GetEquippedItem("helmet") client:EquipItem(item, "helmet") client:UnequipItem("helmet")
Drop on Death Behavior
-- Drop behavior constants DEATH_DROP_NEVER -- Never drop on death DEATH_DROP_SOMETIMES -- Drop based on circumstances DEATH_DROP_ALWAYS -- Always drop on death -- Set drop behavior :SetDropOnDeath(DEATH_DROP_SOMETIMES)
Equipment Actions (Examples)
-- Equipment-specific actions local function EquipCondition(baseItem, item, client) local currentItem = client:GetEquippedItem(item:GetSlot()) if IsValid(currentItem) then return false, "Already have " .. item:GetSlot() .. " equipped" end return true end local function EquipAction(baseItem, item, client) client:EquipItem(item) client:Notify("Equipped " .. item:GetName()) return false -- Don't consume item end ITEM:AddAction("Equip") :SetCondition(EquipCondition) :SetAction(EquipAction) :Build()
Advanced Patterns
Conditional Actions (Examples)
-- Action that changes based on context local function ContextualCondition(baseItem, item, client, ...) if client:InVehicle() then return false, "Cannot use while in vehicle" end if client:GetMoveType() == MOVETYPE_NOCLIP then return false, "Cannot use in noclip" end -- Check for specific job if item:GetRequiredJob() and client:Team() != item:GetRequiredJob() then return false, "Wrong job to use this item" end return true end
Data-Driven Items (Examples)
-- Items that change behavior based on data function ITEM:ItemCreated(item) -- Set random properties item:SetData("quality", math.random(1, 5)) item:SetData("enchantment", table.Random({"fire", "ice", "lightning"})) end local function EnchantedUseAction(baseItem, item, client) local enchantment = item:GetData("enchantment") local quality = item:GetData("quality", 1) if enchantment == "fire" then -- Fire effect local damage = 10 * quality client:TakeDamage(damage, client, item) elseif enchantment == "ice" then -- Ice effect client:Freeze(quality) end return true end
Item Combinations (Examples)
-- Items that can be combined local function CombineCondition(baseItem, item, client, targetItem) if not IsValid(targetItem) then return false, "Must target another item" end if targetItem:GetUniqueID() != "base_component" then return false, "Can only combine with components" end return true end local function CombineAction(baseItem, item, client, targetItem) -- Create new item from combination local inv = item:GetInventory() cityrp.item.Create(inv, "upgraded_item", client:SteamID64(), 1, false, function(newItem) client:Notify("Created " .. newItem:GetName()) end) -- Remove source items item:Remove() targetItem:Remove() return false -- Already handled removal end ITEM:AddAction("Combine") :SetCondition(CombineCondition) :SetAction(CombineAction) :Build()
Migration from Legacy System
Legacy Compatibility
The system provides automatic conversion for legacy items:
-- Legacy item (automatically converted) ITEM.name = "Old Item" ITEM.onUse = function(item, client) -- Legacy use function return true end -- Converted to modern action automatically -- No changes needed for existing legacy items
Modernizing Legacy Items
-- Convert legacy item to modern pattern -- OLD: ITEM.onUse = function(item, client) client:Notify("Used item") return true end -- NEW: local function UseAction(baseItem, item, client) client:Notify("Used item") return true end ITEM:AddAction("Use") :SetAction(UseAction) :Build()
Best Practices
Performance
- Use
:DisableAutoActions()for bases to prevent unnecessary conversion - Cache frequently accessed data in item properties
- Use lifecycle hooks efficiently
- Batch item operations when possible
Code Organization
- Keep bases generic and reusable
- Use meaningful base names (
base_weapon, notbase_item2) - Document custom properties with LuaLS annotations
- Separate complex logic into helper functions
Error Handling
local function SafeUseAction(baseItem, item, client) if not IsValid(client) or not IsValid(item) then return false end local success, err = pcall(function() -- Action logic here client:Notify("Used " .. item:GetName()) end) if not success then ErrorNoHalt("Item use error: " .. err) return false end return true end
Testing
-- Debug command for testing items concommand.Add("give_item", function(client, cmd, args) if not client:IsAdmin() then return end local uniqueID = args[1] local amount = tonumber(args[2]) or 1 if not cityrp.item.GetBase(uniqueID) then client:Notify("Invalid item: " .. uniqueID) return end local inv = client:GetInventory() cityrp.item.Create(inv, uniqueID, client:SteamID64(), amount) end)