Player Wearables
The Unturned wearable system manages clothing, armor, and equipment that players can equip in specific body slots. Each wearable modifies the player's visual appearance and may provide gameplay benefits like protection, insulation, or storage. For roleplay servers, uniform enforcement systems, cosmetic loadout plugins, and item-restriction mechanics, tracking when and what a player equips is essential.
This article covers the RocketMod wearable API in depth. It explains the equipment slot system, how to detect clothing changes, how to read and modify equipped items, and how to build a complete uniform enforcement plugin for RP servers.
57 Studios maintains a suite of server-side plugins for the Horizon Life RP community. The patterns documented here are drawn from production plugin authoring experience, not from documentation alone.
Prerequisites
- A working Unturned dedicated server with RocketMod installed. See RocketMod and OpenMod Plugin Basics for installation guidance.
- Visual Studio 2022 with C# support.
- .NET Framework 4.7.2 SDK.
- Familiarity with RocketMod event subscription and item ID concepts.
- Understanding of the Unturned inventory and clothing system from a player perspective.
What you'll learn
- The wearable equipment slots in Unturned and how RocketMod exposes them.
- The
OnPlayerClothingChangedevent — when it fires and what data it provides. - How to read a player's currently equipped clothing items.
- How to equip and unequip items programmatically.
- How to build a uniform enforcement plugin that restricts clothing by group or zone.
- How to build a cosmetic loadout system that applies outfits on player join.
- How to detect and log clothing changes for analytics.
- Common pitfalls: event timing misconceptions, slot indices, and item ID validation.
Wearable slot system
Equipment slots
Unturned divides wearable items into fixed body slots. Each slot can hold one item at a time:
| Slot name | Slot index | Item types | Purpose |
|---|---|---|---|
| Hat | 0 | Hats, helmets, masks, glasses | Headwear |
| Shirt | 1 | Shirts, jackets, vests | Upper body clothing |
| Pants | 2 | Pants, shorts, skirts | Lower body clothing |
| Backpack | 3 | Backpacks, bags | Storage and appearance |
| Vest | 4 | Body armor, tactical vests | Protection and appearance |
| Mask | 5 | Masks, balaclavas, goggles | Face covering |
| Glasses | 6 | Glasses, sunglasses, goggles | Eyewear |
Slot access via RocketMod
csharp
UnturnedPlayer player = UnturnedPlayer.FromName("Butter");
// Read equipped items by slot
Item hatItem = player.Player.clothing.hat;
Item shirtItem = player.Player.clothing.shirt;
Item pantsItem = player.Player.clothing.pants;
Item backpackItem = player.Player.clothing.backpack;
Item vestItem = player.Player.clothing.vest;
Item maskItem = player.Player.clothing.mask;
Item glassesItem = player.Player.clothing.glasses;Each access returns an SDG.Unturned.Item object or null if the slot is empty.
Item properties
csharp
public string GetEquippedItemName(UnturnedPlayer player, int slotIndex)
{
Item item = GetItemInSlot(player, slotIndex);
if (item == null)
{
return "Empty";
}
Asset asset = Assets.find(EAssetType.ITEM, item.id);
return asset?.itemName ?? string.Format("Unknown ({0})", item.id);
}
private Item GetItemInSlot(UnturnedPlayer player, int slotIndex)
{
switch (slotIndex)
{
case 0: return player.Player.clothing.hat;
case 1: return player.Player.clothing.shirt;
case 2: return player.Player.clothing.pants;
case 3: return player.Player.clothing.backpack;
case 4: return player.Player.clothing.vest;
case 5: return player.Player.clothing.mask;
case 6: return player.Player.clothing.glasses;
default: return null;
}
}OnPlayerClothingChanged
Event signature
csharp
public static event PlayerClothingChanged OnPlayerClothingChanged;
public delegate void PlayerClothingChanged(UnturnedPlayer player, byte slot, Item item);The handler receives the player whose clothing changed, the slot index (0-6), and the Item object that was equipped. The event fires before the item is actually equipped to the player model. This means the handler runs before the item appears on the player, giving plugins a chance to inspect the item before it becomes visible.
Subscription pattern
csharp
using Rocket.Unturned.Events;
protected override void Load()
{
UnturnedPlayerEvents.OnPlayerClothingChanged += HandleClothingChanged;
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerClothingChanged -= HandleClothingChanged;
}
private void HandleClothingChanged(UnturnedPlayer player, byte slot, Item item)
{
string slotName = GetSlotName(slot);
string itemName = GetItemName(item.id);
Logger.Log(string.Format(
"{0} equipped {1} in slot {2} (ID: {3}).",
player.DisplayName, itemName, slotName, item.id));
}Slot name helper
csharp
private string GetSlotName(byte slot)
{
switch (slot)
{
case 0: return "Hat";
case 1: return "Shirt";
case 2: return "Pants";
case 3: return "Backpack";
case 4: return "Vest";
case 5: return "Mask";
case 6: return "Glasses";
default: return string.Format("Slot {0}", slot);
}
}Item name resolver
csharp
private string GetItemName(ushort itemId)
{
Asset asset = Assets.find(EAssetType.ITEM, itemId);
return asset?.itemName ?? string.Format("Unknown ({0})", itemId);
}Clothing change log example
csharp
private void HandleClothingChanged(UnturnedPlayer player, byte slot, Item item)
{
string slotName = GetSlotName(slot);
string itemName = "Empty";
if (item != null)
{
itemName = GetItemName(item.id);
}
Logger.Log(string.Format(
"[Clothing] {0}: {1} -> {2} (ID {3})",
player.DisplayName, slotName, itemName, item?.id ?? 0));
// Log to analytics file
string logLine = string.Format(
"{0:yyyy-MM-dd HH:mm:ss},{1},{2},{3},{4}",
DateTime.UtcNow,
player.CSteamID.m_SteamID,
slot,
item?.id ?? 0,
itemName);
System.IO.File.AppendAllText(
System.IO.Path.Combine(Plugin.DataDirectory, "clothing-log.csv"),
logLine + System.Environment.NewLine);
}Equipping and unequipping items
Equipping an item
To equip an item from the player's inventory into a clothing slot, use the askEquip method on the clothing manager:
csharp
public void EquipItem(UnturnedPlayer player, ushort itemId, byte slot)
{
// Find the item in the player's inventory
for (byte page = 0; page < PlayerInventory.PAGES - 1; page++)
{
for (byte index = 0; index < player.Player.inventory.getItemCount(page); index++)
{
ItemJar jar = player.Player.inventory.getItem(page, index);
if (jar != null && jar.item.id == itemId)
{
player.Player.inventory.askEquip(page, index);
return;
}
}
}
}Unequipping an item
csharp
public void UnequipSlot(UnturnedPlayer player, byte slot)
{
player.Player.clothing.askWearClothing(slot, 0, 0, true); // ID 0 = remove
}Forcing a specific item equip
csharp
public void ForceEquip(UnturnedPlayer player, ushort itemId, byte quality, byte slot)
{
// This equips an item directly without requiring it in the inventory
player.Player.clothing.askWearClothing(slot, itemId, quality, true);
}Building a uniform enforcement plugin
The following plugin enforces specific clothing requirements per group or zone. If a player equips an unauthorized item, it is automatically removed.
Uniform configuration
csharp
public class UniformConfig : IRocketPluginConfiguration
{
public bool Enabled = true;
public bool AutoRemoveOnViolation = true;
public string ViolationMessage = "You cannot equip that item in this area.";
public List<UniformGroup> Groups = new List<UniformGroup>();
public void LoadDefaults()
{
Groups = new List<UniformGroup>
{
new UniformGroup
{
GroupName = "Security",
AllowedItems = new Dictionary<byte, List<ushort>>
{
{ 0, new List<ushort> { 101, 102 } }, // Beret, Helmet
{ 1, new List<ushort> { 201, 202 } }, // Security shirt variants
{ 4, new List<ushort> { 301 } } // Security vest
}
}
};
}
}
public class UniformGroup
{
public string GroupName;
public Dictionary<byte, List<ushort>> AllowedItems;
}Uniform enforcement
csharp
public class UniformEnforcer
{
private readonly UniformConfig _config;
public UniformEnforcer(UniformConfig config)
{
_config = config;
}
public void HandleClothingChanged(UnturnedPlayer player, byte slot, Item item)
{
if (!_config.Enabled) return;
if (!IsItemAllowed(player, slot, item?.id ?? 0))
{
player.SendChat(_config.ViolationMessage, Color.red);
if (_config.AutoRemoveOnViolation)
{
// Unequip the unauthorized item
player.Player.clothing.askWearClothing(slot, 0, 0, true);
}
}
}
private bool IsItemAllowed(UnturnedPlayer player, byte slot, ushort itemId)
{
if (itemId == 0) return true; // Empty slot is always allowed
string groupId = GetPlayerGroup(player);
foreach (UniformGroup group in _config.Groups)
{
if (group.GroupName == groupId)
{
if (group.AllowedItems.TryGetValue(slot, out List<ushort> allowed))
{
return allowed.Contains(itemId);
}
return false; // Slot is regulated but no allowed items defined
}
}
return true; // Player has no uniform group restriction
}
private string GetPlayerGroup(UnturnedPlayer player)
{
// Read the player's RocketMod permission group
foreach (string groupName in R.Permissions.GetGroups(player, false))
{
return groupName;
}
return string.Empty;
}
}Zone-based uniform enforcement
csharp
public class ZoneUniformEnforcer
{
private readonly Dictionary<string, Dictionary<byte, List<ushort>>> _zoneRequirements;
public ZoneUniformEnforcer()
{
_zoneRequirements = new Dictionary<string, Dictionary<byte, List<ushort>>>
{
{
"Hospital", new Dictionary<byte, List<ushort>>
{
{ 1, new List<ushort> { 210, 211 } }, // Medical shirt
{ 0, new List<ushort> { 110 } } // Medical cap
}
},
{
"MilitaryBase", new Dictionary<byte, List<ushort>>
{
{ 1, new List<ushort> { 220 } }, // Military shirt
{ 4, new List<ushort> { 310 } }, // Military vest
{ 0, new List<ushort> { 120, 121 } } // Military helmet
}
}
};
}
public void CheckAndApplyUniform(UnturnedPlayer player, string currentZone)
{
if (!_zoneRequirements.TryGetValue(currentZone, out Dictionary<byte, List<ushort>> requirements))
{
return; // No uniform requirements for this zone
}
foreach (KeyValuePair<byte, List<ushort>> requirement in requirements)
{
byte slot = requirement.Key;
List<ushort> allowedItems = requirement.Value;
Item currentItem = GetItemInSlot(player, slot);
ushort currentId = currentItem?.id ?? 0;
if (currentId != 0 && !allowedItems.Contains(currentId))
{
// Remove unauthorized item
player.Player.clothing.askWearClothing(slot, 0, 0, true);
player.SendChat(string.Format(
"Your {0} is not authorized in this zone. It has been removed.",
GetSlotName(slot)), Color.red);
}
}
}
private Item GetItemInSlot(UnturnedPlayer player, byte slot)
{
switch (slot)
{
case 0: return player.Player.clothing.hat;
case 1: return player.Player.clothing.shirt;
case 2: return player.Player.clothing.pants;
case 3: return player.Player.clothing.backpack;
case 4: return player.Player.clothing.vest;
case 5: return player.Player.clothing.mask;
case 6: return player.Player.clothing.glasses;
default: return null;
}
}
private string GetSlotName(byte slot)
{
string[] names = { "Hat", "Shirt", "Pants", "Backpack", "Vest", "Mask", "Glasses" };
return slot < names.Length ? names[slot] : string.Format("Slot {0}", slot);
}
}Building a cosmetic loadout system
The following plugin applies a cosmetic loadout (full outfit) to a player on join, equipping specific items in each slot.
Outfit definition
csharp
public class Outfit
{
public string Name;
public Dictionary<byte, EquipEntry> Slots = new Dictionary<byte, EquipEntry>();
}
public class EquipEntry
{
public ushort ItemId;
public byte Quality = 100;
}Outfit library
csharp
public static class OutfitLibrary
{
public static readonly Dictionary<string, Outfit> Outfits = new Dictionary<string, Outfit>
{
{
"doctor", new Outfit
{
Name = "Doctor",
Slots = new Dictionary<byte, EquipEntry>
{
{ 0, new EquipEntry { ItemId = 110, Quality = 100 } }, // Medical cap
{ 1, new EquipEntry { ItemId = 210, Quality = 100 } }, // Medical coat
{ 2, new EquipEntry { ItemId = 310, Quality = 100 } } // Medical pants
}
}
},
{
"security", new Outfit
{
Name = "Security",
Slots = new Dictionary<byte, EquipEntry>
{
{ 0, new EquipEntry { ItemId = 101, Quality = 100 } }, // Beret
{ 1, new EquipEntry { ItemId = 201, Quality = 100 } }, // Security shirt
{ 4, new EquipEntry { ItemId = 301, Quality = 100 } } // Security vest
}
}
},
{
"civilian", new Outfit
{
Name = "Civilian",
Slots = new Dictionary<byte, EquipEntry>
{
{ 1, new EquipEntry { ItemId = 200, Quality = 100 } }, // Plain shirt
{ 2, new EquipEntry { ItemId = 300, Quality = 100 } } // Plain pants
}
}
}
};
}Outfit application
csharp
public class OutfitApplier
{
public void ApplyOutfit(UnturnedPlayer player, string outfitName)
{
if (!OutfitLibrary.Outfits.TryGetValue(outfitName, out Outfit outfit))
{
player.SendChat(string.Format("Outfit '{0}' not found.", outfitName), Color.red);
return;
}
foreach (KeyValuePair<byte, EquipEntry> entry in outfit.Slots)
{
player.Player.clothing.askWearClothing(
entry.Key,
entry.Value.ItemId,
entry.Value.Quality,
true);
}
player.SendChat(string.Format("Outfit set to '{0}'.", outfit.Name), Color.green);
Logger.Log(string.Format("{0} applied outfit '{1}'.", player.DisplayName, outfitName));
}
public void ClearOutfit(UnturnedPlayer player)
{
for (byte slot = 0; slot < 7; slot++)
{
player.Player.clothing.askWearClothing(slot, 0, 0, true);
}
player.SendChat("All clothing removed.", Color.yellow);
}
}Outfit command
csharp
public class OutfitCommand : IRocketCommand
{
public string Name => "outfit";
public string Help => "Applies a cosmetic outfit. Usage: /outfit <name> or /outfit clear";
public string Syntax => "/outfit <name>";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "cosmetics.outfit" };
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
if (command.Length < 1)
{
string list = string.Join(", ", OutfitLibrary.Outfits.Keys);
player.SendChat(string.Format("Available outfits: {0}", list), Color.cyan);
return;
}
OutfitApplier applier = new OutfitApplier();
if (command[0].ToLower() == "clear")
{
applier.ClearOutfit(player);
}
else
{
applier.ApplyOutfit(player, command[0].ToLower());
}
}
}Auto-apply on connect
csharp
private void HandlePlayerConnected(UnturnedPlayer player)
{
// Apply saved outfit preference on join
string savedOutfit = GetSavedOutfit(player.CSteamID.m_SteamID);
if (!string.IsNullOrEmpty(savedOutfit))
{
OutfitApplier applier = new OutfitApplier();
applier.ApplyOutfit(player, savedOutfit);
Logger.Log(string.Format("Applied saved outfit '{0}' for {1}.", savedOutfit, player.DisplayName));
}
}
private string GetSavedOutfit(ulong steamId)
{
string path = System.IO.Path.Combine(
Plugin.DataDirectory, "outfits", string.Format("{0}.txt", steamId));
if (System.IO.File.Exists(path))
{
return System.IO.File.ReadAllText(path).Trim();
}
return null;
}Clothing change analytics
csharp
public class ClothingAnalytics
{
private readonly Dictionary<ulong, int> _changeCount =
new Dictionary<ulong, int>();
private readonly Dictionary<string, int> _itemUsage =
new Dictionary<string, int>();
public void RecordChange(UnturnedPlayer player, byte slot, Item item)
{
ulong steamId = player.CSteamID.m_SteamID;
// Track per-player change count
if (!_changeCount.ContainsKey(steamId))
{
_changeCount[steamId] = 0;
}
_changeCount[steamId]++;
// Track per-item usage
string slotName = GetSlotName(slot);
string itemKey = string.Format("{0}:{1}", slotName, item?.id ?? 0);
if (!_itemUsage.ContainsKey(itemKey))
{
_itemUsage[itemKey] = 0;
}
_itemUsage[itemKey]++;
}
public string GenerateReport()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine("=== Clothing Analytics Report ===");
sb.AppendLine();
sb.AppendLine("Top 10 most changed items:");
foreach (KeyValuePair<string, int> entry in _itemUsage.OrderByDescending(x => x.Value).Take(10))
{
sb.AppendLine(string.Format(" {0}: {1} times", entry.Key, entry.Value));
}
sb.AppendLine();
sb.AppendLine(string.Format("Total players tracked: {0}", _changeCount.Count));
sb.AppendLine(string.Format("Total changes recorded: {0}", _changeCount.Values.Sum()));
return sb.ToString();
}
private string GetSlotName(byte slot)
{
string[] names = { "Hat", "Shirt", "Pants", "Backpack", "Vest", "Mask", "Glasses" };
return slot < names.Length ? names[slot] : string.Format("Slot {0}", slot);
}
}Event timing and ordering
The OnPlayerClothingChanged event fires before the item is actually equipped to the player model. The sequence is:
Player right-clicks item in inventory
↓
Player selects "Equip" from context menu
↓
Server receives equip request
↓
OnPlayerClothingChanged fires (item is about to be equipped)
↓
Plugin handler runs
↓
Item is applied to the player model (visible to all clients)
↓
OnPlayerUpdateBleeding/OnPlayerUpdateHealth may fire if the item has gameplay effectsBecause the event fires before the equip completes, plugins have an opportunity to inspect the item and potentially prevent the equip by overriding it with an empty slot. However, once the handler returns, the equip proceeds. To cancel an equip, set the slot back to empty inside the handler:
csharp
private void HandleClothingChanged(UnturnedPlayer player, byte slot, Item item)
{
if (IsRestrictedItem(item?.id ?? 0))
{
// Cancel the equip by setting slot to empty
player.Player.clothing.askWearClothing(slot, 0, 0, true);
player.SendChat("That item is restricted on this server.", Color.red);
}
}Common pitfalls
OnPlayerClothingChanged fires before equip, not after
The OnPlayerClothingChanged event fires before the item is applied to the player model. If you read the player's clothing slot inside the handler, it still shows the previous item (or empty). To confirm that the equip completed, read the slot after yielding a frame:
csharp
private IEnumerator DelayedEquipCheck(UnturnedPlayer player, byte slot)
{
yield return new WaitForEndOfFrame();
Item currentItem = GetItemInSlot(player, slot);
Logger.Log(string.Format("Slot {0} now contains: {1}", slot, currentItem?.id ?? 0));
}Event name confusion
The RocketMod event for clothing changes is OnPlayerClothingChanged, not OnPlayerWear. Some community examples reference OnPlayerWear, which does not exist in the standard RocketMod API. Always use OnPlayerClothingChanged.
Slots are byte values, not string names
Slot parameters are byte values from 0 to 6. Using string-based slot references requires a mapping. Always cast or map to byte when accessing slot data:
csharp
player.Player.clothing.askWearClothing((byte)slot, itemId, quality, true);Item IDs are ushort values
Unturned item IDs are ushort (16-bit unsigned integer). Using int and casting can cause issues if the item ID exceeds the ushort range:
csharp
// WRONG — potential overflow
player.Player.clothing.askWearClothing(slot, (ushort)itemId, quality, true);
// CORRECT — keep as ushort throughout
ushort itemId = 101;
player.Player.clothing.askWearClothing(slot, itemId, quality, true);Setting clothing on a dead player
The clothing manager on a dead player is in an inconsistent state. Always check life state before modifying clothing:
csharp
if (!player.Player.life.isDead)
{
player.Player.clothing.askWearClothing(slot, itemId, quality, true);
}Equipping without the item in inventory
The askWearClothing method equips any item ID to any slot, regardless of whether the item exists in the player's inventory. For anti-exploit purposes, verify that the player actually has the item before equipping it:
csharp
public bool PlayerHasItem(UnturnedPlayer player, ushort itemId)
{
for (byte page = 0; page < PlayerInventory.PAGES - 1; page++)
{
for (byte index = 0; index < player.Player.inventory.getItemCount(page); index++)
{
ItemJar jar = player.Player.inventory.getItem(page, index);
if (jar != null && jar.item.id == itemId)
{
return true;
}
}
}
return false;
}Frequently asked questions
What event fires when a player equips clothing?
The OnPlayerClothingChanged event fires when a player equips or unequips an item in any clothing slot. The handler receives the player, the slot index, and the item being equipped.
Does OnPlayerClothingChanged fire before or after the item is equipped?
The event fires before the item is actually applied to the player model. The handler runs during the equip request processing, before the client receives the visual update.
How do I prevent a player from equipping a specific item?
Inside the OnPlayerClothingChanged handler, check the item ID and call askWearClothing(slot, 0, 0, true) to set the slot back to empty if the item is not allowed.
How do I remove all clothing from a player?
csharp
for (byte slot = 0; slot < 7; slot++)
{
player.Player.clothing.askWearClothing(slot, 0, 0, true);
}Can I equip items that are not in the player's inventory?
Yes. The askWearClothing method accepts any item ID and equips it to the specified slot regardless of the player's current inventory contents. For security, verify that the player owns the item before equipping.
How do I find the item ID for a specific clothing item?
Item IDs are defined in the Unturned asset files. Use the RocketMod item list website or extract the IDs from the game's Items folder. Each clothing item has a unique ushort ID.
Full event and API reference
| API | Type | Purpose |
|---|---|---|
OnPlayerClothingChanged | Event | Fires when a player equips or unequips a clothing item (fires before the equip) |
player.Player.clothing.hat | Property | Currently equipped hat item (or null) |
player.Player.clothing.shirt | Property | Currently equipped shirt item (or null) |
player.Player.clothing.pants | Property | Currently equipped pants item (or null) |
player.Player.clothing.backpack | Property | Currently equipped backpack item (or null) |
player.Player.clothing.vest | Property | Currently equipped vest item (or null) |
player.Player.clothing.mask | Property | Currently equipped mask item (or null) |
player.Player.clothing.glasses | Property | Currently equipped glasses item (or null) |
askWearClothing(slot, id, quality, bool) | Method | Equips or unequips an item in the specified slot |
Cross-references
- Player Gesture and Stance — previous article; gesture animations interact with wearable state.
- Player God Mode and Vanish Mode — next article; god mode and wearable interaction.
- RocketMod and OpenMod Plugin Basics — plugin lifecycle and event subscription.
- Player Stats and Experience — some clothing items affect skill levels.
- Server Commands Reference — command syntax reference.
- Debugging Server Exceptions — diagnosing plugin crashes.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2025-06-18 | 57 Studios | Initial publication. Wearable slot system, OnPlayerClothingChanged event, uniform enforcement plugin, cosmetic loadout system, clothing analytics, event timing, common pitfalls. |
