Example: Item Rotation Plugin
Source: ItemRotation.cs
A config-driven plugin that rotates item sets between players on a timer.
Overview
- Commands:
/ir_start,/ir_swap,/ir_reset,/ir_sets - Features: JSON config, sequential/random modes, duplicate prevention, HUD announcements
- Concepts demonstrated: Configuration system, player tracking, timed game logic, chat messaging
Architecture
/ir_start
│
├── Validate players & item sets
├── Assign initial sets (sequential or random)
├── Apply items to all players
└── Start swap timer
│
└── Every N seconds:
├── Rotate set assignments
├── Remove old items
├── Give new items
├── Play sound
└── Show announcement
Configuration
Uses IPluginConfig<T> with BasePluginConfig for JSON-serialized settings:
public class ItemRotationConfig : BasePluginConfig
{
[JsonPropertyName("SwapIntervalSeconds")]
public int SwapIntervalSeconds { get; set; } = 10;
[JsonPropertyName("SelectionMode")]
public string SelectionMode { get; set; } = "sequential";
[JsonPropertyName("AllowDuplicateSets")]
public bool AllowDuplicateSets { get; set; } = true;
[JsonPropertyName("ItemSets")]
public List<ItemSet> ItemSets { get; set; } = new()
{
new() { Name = "Speed Demons", Items = new() { "upgrade_sprint_booster", "upgrade_kinetic_sash" } },
new() { Name = "Cardio Kings", Items = new() { "upgrade_fleetfoot_boots", "upgrade_cardio_calibrator" } },
// ...more sets
};
}
Config Validation
public void OnConfigParsed(ItemRotationConfig config)
{
if (config.SwapIntervalSeconds < 1) config.SwapIntervalSeconds = 1;
Config = config;
}
Player Tracking
Tracks active players by slot index:
private readonly Dictionary<int, int> _playerSetIndex = new(); // slot -> set index
private readonly HashSet<int> _activePlayers = new();
// On disconnect, clean up
public override void OnClientDisconnect(ClientDisconnectedEvent args)
{
_activePlayers.Remove(args.Slot);
_playerSetIndex.Remove(args.Slot);
}
Chat Messaging Helpers
Reusable helpers for sending targeted messages:
private static void SendChat(int slot, string text)
{
var msg = new CCitadelUserMsg_ChatMsg
{
PlayerSlot = -1,
Text = text,
AllChat = true
};
NetMessages.Send(msg, RecipientFilter.Single(slot));
}
private static void SendChatAll(string text)
{
var msg = new CCitadelUserMsg_ChatMsg
{
PlayerSlot = -1,
Text = text,
AllChat = true
};
NetMessages.Send(msg, RecipientFilter.All);
}
Key Patterns
Sequential vs Random Assignment
if (Config.SelectionMode == "random")
AssignRandomSets(players);
else
{
// Sequential: each player gets the next set
for (int i = 0; i < players.Count; i++)
_playerSetIndex[players[i]] = i % Config.ItemSets.Count;
}
Duplicate Prevention
When AllowDuplicateSets is false, validates at startup:
if (!Config.AllowDuplicateSets && players.Count > Config.ItemSets.Count)
{
SendChat(slot, "Not enough item sets for all players!");
return HookResult.Handled;
}
Item Application with Old/New Swap
// Remove old items
if (previousSets.TryGetValue(slot, out int oldIndex))
foreach (var item in Config.ItemSets[oldIndex].Items)
pawn.RemoveItem(item);
// Give new items
foreach (var item in Config.ItemSets[setIndex].Items)
pawn.AddItem(item);
API Features Used
| Feature | Reference |
|---|---|
IPluginConfig<T> | Configuration |
[ChatCommand] | Chat Commands |
Timer.Every | Timers |
NetMessages.Send, RecipientFilter | Networking |
Players.FromSlot, Players.MaxSlots | Players |
CCitadelUserMsg_HudGameAnnouncement | Networking |
OnClientDisconnect | Plugin Base |