Skip to content

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 OnPlayerClothingChanged event — 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 nameSlot indexItem typesPurpose
Hat0Hats, helmets, masks, glassesHeadwear
Shirt1Shirts, jackets, vestsUpper body clothing
Pants2Pants, shorts, skirtsLower body clothing
Backpack3Backpacks, bagsStorage and appearance
Vest4Body armor, tactical vestsProtection and appearance
Mask5Masks, balaclavas, gogglesFace covering
Glasses6Glasses, sunglasses, gogglesEyewear

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 effects

Because 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

APITypePurpose
OnPlayerClothingChangedEventFires when a player equips or unequips a clothing item (fires before the equip)
player.Player.clothing.hatPropertyCurrently equipped hat item (or null)
player.Player.clothing.shirtPropertyCurrently equipped shirt item (or null)
player.Player.clothing.pantsPropertyCurrently equipped pants item (or null)
player.Player.clothing.backpackPropertyCurrently equipped backpack item (or null)
player.Player.clothing.vestPropertyCurrently equipped vest item (or null)
player.Player.clothing.maskPropertyCurrently equipped mask item (or null)
player.Player.clothing.glassesPropertyCurrently equipped glasses item (or null)
askWearClothing(slot, id, quality, bool)MethodEquips or unequips an item in the specified slot

Cross-references

Document history

VersionDateAuthorNotes
1.02025-06-1857 StudiosInitial publication. Wearable slot system, OnPlayerClothingChanged event, uniform enforcement plugin, cosmetic loadout system, clothing analytics, event timing, common pitfalls.