Before custom items can be used in-game, they must be registered with the CustomItems framework. Registration assigns unique IDs, validates item names, and makes items available for spawning and event handling.
Registers a single custom item instance.What it does:
Validates the item name is unique
Assigns a unique ID
Adds to internal dictionaries (_itemsById and _itemsByName)
Calls OnRegistered() on the item
Implementation:
CustomItems.cs:26-38
public static void Register(CustomItem item){ if (_itemsByName.ContainsKey(item.Name)) throw new InvalidOperationException($"Item '{item.Name}' already registered."); item.Id = GetNextId(); _itemsById[item.Id] = item; _itemsByName[item.Name] = item; item.OnRegistered(); Log.Debug($"Registered item '{item.Name}' with ID {item.Id} and type {item.Type}.");}
Example usage:
var healingSyringe = new HealingSyringe();CustomItems.Register(healingSyringe);
Attempting to register an item with a duplicate name will throw an InvalidOperationException.
Automatically discovers and registers all CustomItem subclasses in the calling assembly.What it does:
Uses reflection to find all non-abstract CustomItem subclasses
Creates instances using Activator.CreateInstance()
Registers each instance via Register()
Returns a list of successfully registered items
Implementation:
CustomItems.cs:40-64
public static List<CustomItem> RegisterAll(){ var itemTypes = Assembly.GetCallingAssembly().GetTypes().Where(t => !t.IsAbstract && typeof(CustomItem).IsAssignableFrom(t)); var createdItems = new List<CustomItem>(); foreach (var type in itemTypes) { if (Activator.CreateInstance(type) is not CustomItem item) continue; try { Register(item); createdItems.Add(item); } catch (Exception ex) { Log.Warn($"Failed to register item '{item.Name}': {ex.Message}"); } } Log.Debug($"Registered {createdItems.Count} items from assembly '{Assembly.GetCallingAssembly().GetName().Name}'."); return createdItems;}
Example usage:
CustomItemsPlugin.cs:56
if (Config.EnableExampleItems) API.CustomItems.RegisterAll();
RegisterAll() only registers items from the calling assembly. If you have items in a different assembly/plugin, call RegisterAll() from that assembly or use Register() manually.
Removes all active instances (by serial number) from CurrentItems
Calls OnUnregistered() on the item
Implementation:
CustomItems.cs:68-83
public static void Unregister(CustomItem item){ if (!_itemsByName.ContainsKey(item.Name)) throw new InvalidOperationException($"Item '{item.Name}' not registered."); _itemsById.Remove(item.Id); _itemsByName.Remove(item.Name); foreach (var serial in CurrentItems.Where(kvp => kvp.Value == item).Select(kvp => kvp.Key).ToList()) { CurrentItems.Remove(serial); } item.OnUnregistered(); Log.Debug($"Unregistered item '{item.Name}' with ID {item.Id}.");}
Typically, you register items in your plugin’s Enable() method and unregister in Disable():
public class MyPlugin : Plugin{ private List<CustomItem> _registeredItems; public override void Enable() { // Option 1: Register all items in this assembly _registeredItems = CustomItems.RegisterAll(); // Option 2: Register specific items CustomItems.Register(new MyCustomItem()); } public override void Disable() { // Unregister all items from this assembly CustomItems.UnregisterAll(); // Or unregister specific items foreach (var item in _registeredItems) { CustomItems.Unregister(item); } }}
public static ushort GetIdByName(string name){ if (_itemsByName.TryGetValue(name, out CustomItem item)) { return item.Id; } throw new KeyNotFoundException($"Item with name '{name}' not found.");}
Example usage:
// Get item by IDvar item = CustomItems.GetById(0);// Get ID by namevar empId = CustomItems.GetIdByName("EMP Grenade");
public static bool TryGive(ushort id, Player player, out Item item, ItemAddReason addReason = ItemAddReason.Undefined)
Gives a custom item to a player’s inventory.
CustomItems.cs:135-147
public static bool TryGive(ushort id, Player player, out Item item, ItemAddReason addReason = ItemAddReason.Undefined){ if (!_itemsById.TryGetValue(id, out CustomItem cItem)) { item = null; return false; } item = player.AddItem(cItem.Type, addReason); if (item == null) return false; CurrentItems.Add(item.Serial, (CustomItem)Activator.CreateInstance(cItem.GetType())); Log.Debug($"Gave item '{cItem.Name}' ({id}) to '{player.Nickname}' with serial {item.Serial}."); return true;}
Example:
if (CustomItems.TryGive(empGrenadeId, player, out var item)){ player.SendHint("You received an EMP Grenade!");}
Both TrySpawn() and TryGive() create new instances of your custom item class (via Activator.CreateInstance()). This means each spawned/given item has its own state.
For most plugins, RegisterAll() is the simplest approach:
public override void Enable(){ CustomItems.RegisterAll();}public override void Disable(){ CustomItems.UnregisterAll();}
Manual registration for fine control
Use Register() when you need:
Conditional registration based on config
Specific registration order
Items from multiple assemblies
public override void Enable(){ if (Config.EnableEMPGrenades) CustomItems.Register(new EMPGrenade()); if (Config.EnableHealingItems) CustomItems.Register(new HealingSyringe());}
Always unregister in Disable
Failing to unregister can cause:
Memory leaks
Duplicate registration errors on reload
Events firing for disabled plugins
public override void Disable(){ CustomItems.UnregisterAll(); // or foreach (var item in myItems) CustomItems.Unregister(item);}
Handle registration errors gracefully
RegisterAll() catches exceptions, but Register() throws:
try{ CustomItems.Register(new MyItem());}catch (InvalidOperationException ex){ Log.Error($"Failed to register item: {ex.Message}");}