UseableClothing — Dressing and Undressing
UseableClothing is the smallest Useable subclass in the SDK at 121 lines, but it sits at the intersection of the inventory system, the visual clothing pipeline, and the proof-of-ownership system. It handles the server-authoritative transition of an equippable clothing item from the player's hands to their equipped clothing slot, applying the visual model override and consuming the item.
Source code location: Unturned/Useable/UseableClothing.cs
The Wear Pipeline
The entire useable follows a simple state machine: equip → primary input → animation → simulate → slot assignment.
Equip
On equip(), the class reads the "Use" animation length from the player's animator controller:
csharp
public override void equip()
{
player.animator.play("Equip", true);
useTime = player.animator.GetAnimationLength("Use");
}This establishes the timing baseline used to determine when the wear action completes.
startPrimary — Initiating the Wear
Primary input is blocked if the player is already busy (mid-animation):
csharp
public override bool startPrimary()
{
if (player.equipment.isBusy)
return false;
player.equipment.isBusy = true;
startedUse = Time.realtimeSinceStartup;
isUsing = true;
if (Provider.isServer)
{
SendPlayWear.Invoke(GetNetId(), ENetReliability.Unreliable,
channel.GatherRemoteClientConnectionsExcludingOwner());
}
wear();
return true;
}The SendPlayWear RPC is multicast to all remote clients (excluding the owner) so they see the wear animation on the local player's third-person model. It uses ONLY_FROM_SERVER validation:
csharp
[SteamCall(ESteamCallValidation.ONLY_FROM_SERVER, legacyName = nameof(askWear))]
public void ReceivePlayWear()
{
if (player.equipment.IsEquipAnimationFinished)
wear();
}The IsEquipAnimationFinished guard prevents a client from playing the wear animation before the equip animation completes in cases of network latency.
The wear() Method
The wear method itself is minimal — it triggers the animation and alerts nearby zombies:
csharp
private void wear()
{
player.animator.play("Use", false);
if (Provider.isServer)
AlertTool.alert(transform.position, 8);
}The AlertTool.alert at radius 8 informs nearby zombies of the player's position, making clothing changes audible in the game world.
Simulate — Slot Resolution
The core logic lives in simulate(), which executes once the use animation timer expires:
csharp
public override void simulate(uint simulation, bool inputSteady)
{
if (isUsing && isUseable)
{
player.equipment.isBusy = false;
isUsing = false;
if (Provider.isServer)
{
ItemAsset asset = player.equipment.asset;
EItemType type = asset.type;
byte quality = player.equipment.quality;
byte[] state = player.equipment.state;
player.equipment.use(); // Consume the item
// Type dispatch to clothing slots
if (type == EItemType.HAT)
player.clothing.askWearHat(asset as ItemHatAsset, quality, state, true);
else if (type == EItemType.SHIRT)
player.clothing.askWearShirt(asset as ItemShirtAsset, quality, state, true);
// ... PANTS, BACKPACK, VEST, MASK, GLASSES
}
}
}The isUseable computed property gates execution:
csharp
private bool isUseable => Time.realtimeSinceStartup - startedUse > useTime;Clothing Slot Mapping
Each EItemType maps to a specific slot on the player.clothing object:
| EItemType | Clothing Method | Visual Slot |
|---|---|---|
HAT | askWearHat(ItemHatAsset, quality, state, true) | visualHat |
SHIRT | askWearShirt(ItemShirtAsset, quality, state, true) | visualShirt |
PANTS | askWearPants(ItemPantsAsset, quality, state, true) | visualPants |
BACKPACK | askWearBackpack(ItemBackpackAsset, quality, state, true) | visualBackpack |
VEST | askWearVest(ItemVestAsset, quality, state, true) | visualVest |
MASK | askWearMask(ItemMaskAsset, quality, state, true) | visualMask |
GLASSES | askWearGlasses(ItemGlassesAsset, quality, state, true) | visualGlasses |
The boolean true parameter indicates that the clothing change should trigger a visual state update and network broadcast. Internally, the askWear* methods:
- Store the current worn item back into the inventory (swap)
- Assign the new item data (asset reference, quality, item state) to the clothing slot
- Update the visual model (
HumanClothes.apply()) - Broadcast the change to all clients via
SendClothing*RPCs
Proof System and Visual Override
Unturned clothing uses an economy-backed proof system. Each clothing item may have a skin applied from the Steam inventory. The "proof" is a cryptographic signature from the economy service that certifies the player legitimately owns the skin.
The askWear* methods on the server validate this proof before applying the visual override. The proof is carried in the item state data, which is forwarded from player.equipment.state through simulate() into the clothing slot.
The visual override system (HumanClothes) reads from skin/mythic data:
visualShirt = economyService.getInventorySkinID(itemDefId)The mythic effect ID is resolved separately:
displayMythic = provider.economyService.getInventoryMythicID(item)These visual overrides are rendered on the character model independently of the base item ID, allowing cosmetic skins to alter appearance without changing functionality.
Item Consumption
The critical player.equipment.use() call in simulate() consumes the item from the player's hotbar. This happens server-side only, so a hacked client that skips the wear animation cannot duplicate clothing items. The item is destroyed from the inventory before the clothing slot assignment, ensuring the "one item, one slot" invariant.
If the player's clothing slot already contains an item, the askWear* method swaps it back into the inventory before assigning the new item. This swap is handled inside the clothing manager, not in UseableClothing itself.
Animation State Machine
The wear animation follows a strict sequence:
Equip → [player.input] → Use → SimulateThe equip animation must finish before the use animation can play (IsEquipAnimationFinished check in ReceivePlayWear). The use animation must finish before simulate() assigns the clothing slot (isUseable timer). This two-phase gate prevents animation blending issues and ensures all network observers see the full wear animation before the visual clothing changes.
Audio Feedback
The wear() method triggers AlertTool.alert at distance 8. This is identical to the alert radius used by weapon fire and footstep noise, meaning zombie AI can hear the player changing clothes and investigate the sound source.
Detailed Method Analysis
simulate() — Complete Code Path
csharp
public override void simulate(uint simulation, bool inputSteady)
{
if (isUsing && isUseable)
{
player.equipment.isBusy = false;
isUsing = false;
if (Provider.isServer)
{
ItemAsset asset = player.equipment.asset;
EItemType type = asset.type;
byte quality = player.equipment.quality;
byte[] state = player.equipment.state;
player.equipment.use(); // Consume item from inventory
if (type == EItemType.HAT)
player.clothing.askWearHat(asset as ItemHatAsset, quality, state, true);
else if (type == EItemType.SHIRT)
player.clothing.askWearShirt(asset as ItemShirtAsset, quality, state, true);
else if (type == EItemType.PANTS)
player.clothing.askWearPants(asset as ItemPantsAsset, quality, state, true);
else if (type == EItemType.BACKPACK)
player.clothing.askWearBackpack(asset as ItemBackpackAsset, quality, state, true);
else if (type == EItemType.VEST)
player.clothing.askWearVest(asset as ItemVestAsset, quality, state, true);
else if (type == EItemType.MASK)
player.clothing.askWearMask(asset as ItemMaskAsset, quality, state, true);
else if (type == EItemType.GLASSES)
player.clothing.askWearGlasses(asset as ItemGlassesAsset, quality, state, true);
}
}
}Note the explicit type checking: there is no else fallback. If an item has an unexpected EItemType, the item is consumed but nothing is worn. This is by design — only the seven clothing types trigger slot assignment.
The isUseable Gate
csharp
private bool isUseable => Time.realtimeSinceStartup - startedUse > useTime;Uses unscaled realtime, not game time, so pause menus or server lag cannot extend the wear animation and delay slot assignment. The useTime is set once during equip():
csharp
useTime = player.animator.GetAnimationLength("Use");This means the animation length is baked at equip time. If the player switches clothing items rapidly, each equip sets its own useTime independent of the previous item.
Wear Method Internals
The wear() method is deliberately minimal:
csharp
private void wear()
{
player.animator.play("Use", false);
if (Provider.isServer)
AlertTool.alert(transform.position, 8);
}The animation is played with crossFade = false, meaning it replaces the current animation immediately. The alert radius of 8 meters is significant — it's the same radius used by:
UseableGunon unsuppressed weapon fireUseableMeleeon swingUseableThrowableon throwUseableStructureon construction
This uniform alert radius means any audible player action alerts zombies within the same range, preventing clothing changes from being uniquely stealthy or noisy.
The Clothing Manager API
Each askWear* method on PlayerClothing follows the same pattern. Using hats as an example:
askWearHat Protocol
csharp
public void askWearHat(ItemHatAsset hatAsset, byte quality, byte[] state, bool playEffect)The method:
- Reads the currently worn hat (if any) from
_hatand stores it temporarily - Assigns the new
ItemHatAsset, quality, and state to the hat slot fields - Calls
tryAddItem(oldHatItem, true)to return the previous hat to inventory (or drop it if inventory is full) - Calls
applyVisualHat()to update theHumanClothescomponent with the new visual override data - If
playEffectis true, callsSendClothingHat_InventoryandSendClothingHat_VisualRPCs to broadcast the change
The visual update path:
csharp
private void applyVisualHat()
{
if (_hatAsset != null)
{
int visualHat;
if (channel.owner.getItemSkinItemDefID(_hatAsset.GUID, out int itemDefId))
visualHat = Provider.provider.economyService.getInventorySkinID(itemDefId);
else
visualHat = 0;
ushort mythic = 0;
if (channel.owner.getParticleEffectForItemDef(itemDefId) != 0)
mythic = channel.owner.getParticleEffectForItemDef(itemDefId);
_clothes.visualHat = visualHat;
_clothes.hat = _hatAsset.id;
}
_clothes.apply();
}The apply() call on HumanClothes triggers a full model rebuild: creating or destroying hat/glasses/mask/backpack/vest/shirt/pants child meshes and applying materials, colors, and particle effects.
Item State Propagation
The clothing item's state byte array (typically 0–16 bytes depending on the item) is forwarded verbatim from the player's equipment slot to the clothing manager. This state can contain:
- Dye colors — RGB color data for dyed clothing items
- Economy proof — Cryptographic proof of skin ownership from the Steam economy
- Custom data — Plugin-inserted metadata
The state array is stored permanently on the clothing slot and re-applied whenever the clothing model is rebuilt (e.g., on player respawn or region transition).
Quality Propagation
Clothing quality is a byte (0–100). Unlike weapons, clothing quality does not affect gameplay stats — it is purely cosmetic and visual. The quality value is forwarded to the clothing manager slot, where it may be read by cosmetic systems or economy skin rendering.
Animation Clip Timing
The "Equip" and "Use" clips have specific timing requirements:
csharp
public void ReceivePlayWear()
{
if (player.equipment.IsEquipAnimationFinished)
{
wear();
}
}IsEquipAnimationFinished is implemented on PlayerEquipment:
csharp
public bool IsEquipAnimationFinished =>
Time.realtimeSinceStartup - lastEquipTime > equipAnimationLength;The equip animation must fully complete before the wear animation begins. This two-phase approach prevents visual glitches where the clothing item appears to change mid-equip.
The SendPlayWear RPC uses ONLY_FROM_SERVER validation:
csharp
private static readonly ClientInstanceMethod SendPlayWear =
ClientInstanceMethod.Get(typeof(UseableClothing), nameof(ReceivePlayWear));
[SteamCall(ESteamCallValidation.ONLY_FROM_SERVER, legacyName = nameof(askWear))]Only the server can trigger the wear animation on remote clients. The local client triggers its own animation directly in startPrimary() via wear().
Animation Controller Requirements
For a clothing item to function correctly, the player's animation controller must have:
| Clip name | Required | Purpose |
|---|---|---|
Equip | Yes | Drawing the item |
Use | Yes | The wearing motion |
UnEquip | No | Stowing (base class default) |
The Use clip's length determines the timing of the entire wear operation. Missing clips result in immediate completion (GetAnimationLength returns 0).
Inventory Slot Resolution
The simulate() path only executes if the player still has the item equipped when the timer fires. Between startPrimary() and simulate(), the player could:
- Switch items (canceling the equip, returning to idle)
- Die (clearing equipment)
- Be disconnected
If the equipment is no longer valid, player.equipment.asset may be null, and the type dispatch would still execute. The null-safe casts (asset as ItemHatAsset) return null for mismatched types, and askWearHat(null, ...) is expected to handle a null asset gracefully (typically by doing nothing).
What Happens When the Item Is Not a Clothing Type
Although UseableClothing should only be equipped for EItemType values of the seven clothing types, the type dispatch in simulate() has no default case. If an item somehow reaches simulate() with a different type (e.g., a mod error or asset data mismatch):
csharp
if (type == EItemType.HAT) { ... }
else if (type == EItemType.SHIRT) { ... }
// ... no else clauseThe item is consumed by player.equipment.use() but never assigned to a clothing slot. The item is effectively destroyed. This is a safety mechanism — silently dropping the item prevents undefined behavior from spreading.
Network Protocol Summary
| Direction | Message | Timing | Reliability |
|---|---|---|---|
| Server → All | SendPlayWear | After startPrimary | Unreliable |
| Client → Server | (none — item use is server-evaluated in simulate) | — | — |
| Server → All | Clothing slot RPCs | After simulate | Reliable |
The clothing slot RPCs (SendClothingHat_Inventory, SendClothingShirt_Inventory, etc.) use ClientInstanceMethod with ONLY_FROM_SERVER validation. They carry:
csharp
// Per-clothing-slot RPC signature (example: hat)
private static readonly ClientInstanceMethod<ushort, byte, byte[], ushort, ushort>
SendClothingHat_Visual = ...;Parameters: itemID (ushort), quality (byte), state (byte[]), skinID (ushort), mythicID (ushort).
Dequip and Unequip
UseableClothing does not override dequip(). The dequip lifecycle is inherited from the base Useable class:
csharp
public virtual void dequip()
{
// Base implementation clears visual state
}When the player manually unequips a clothing item before the wear animation completes, the item is simply put away without being consumed. The clothing slot is never modified. This provides a natural "cancel" behavior — if the player realizes they equipped the wrong hat, they can switch items during the equip animation and nothing is lost.
Comparison with Other Useables
UseableClothing is structurally distinct from other Useable subclasses:
| Aspect | UseableClothing | UseableGun | UseableConsumeable |
|---|---|---|---|
startPrimary action | Sets isUsing, plays wear animation | Fires projectile | Begins consume animation |
simulate outcome | Calls askWear* for slot assignment | Spawns bullet/raycast | Applies stat effects |
| Animation required | "Use" clip on controller | "Use" + "Equip" on controller | "Use" clip on controller |
| Item consumption | player.equipment.use() in simulate | Ammo per shot | player.equipment.use() in simulate |
| Visual change | Permanent slot assignment | Muzzle flash (transient) | None |
The clothing useable is the only one that produces a permanent visual change on the player model. Weapons and consumables produce transient effects.
Thread Safety Notes
The clothing system operates entirely on the main Unity thread. The simulate() callback is called from the game's tick loop, which runs on the main thread. The askWear* calls modify the HumanClothes component, which calls Instantiate and Destroy — both Unity API calls that require the main thread.
The isUsing and isUseable pattern is safe because:
isUsingis set totrueon the main thread instartPrimary()isUseablereadsTime.realtimeSinceStartup(main-thread-safe)isUsingis set tofalseon the main thread insimulate()- No concurrent access exists because
Useablelifecycle methods are all main-thread
Edge Cases
Double-Wear Prevention
The isUsing boolean prevents simulate() from running the wear logic twice:
csharp
if (isUsing && isUseable)
{
player.equipment.isBusy = false;
isUsing = false;
// ... wear logic (only executes once)
}If simulate() is called multiple times (e.g., due to tick rate fluctuation) before isUseable becomes true, only the first activation after the timer expires executes the wear path.
Busy Lock
csharp
public override bool startPrimary()
{
if (player.equipment.isBusy)
return false;
player.equipment.isBusy = true;
// ...
}The busy lock prevents the player from initiating multiple wears simultaneously. It is released in simulate():
csharp
player.equipment.isBusy = false;Missing Animation Clip
If the "Use" animation clip is missing from the player's controller (e.g., a modded item that forgot to add the clip), GetAnimationLength("Use") returns 0, and isUseable becomes immediately true. The clothing slot assignment happens on the next simulate() tick with no visible animation — the item is consumed and equipped instantly.
Server-Only Execution
On a dedicated server, Dedicator.IsDedicatedServer is true, and channel.IsLocalPlayer is always false. The server:
- Receives
startPrimaryvia the baseUseableprotocol - Plays no animation (no renderer exists)
- Sets
isUsing = trueand starts the realtime timer - In
simulate(), executes theaskWear*call server-side - Broadcasts the clothing slot change to all clients
The animation-free server path saves CPU cycles and avoids audio issues on headless instances.
Clothing Removal (Dequip) Protocol
Clothing removal is not handled by UseableClothing. Instead, it is managed through the PlayerClothingUI which provides a grid-based interface for managing worn items. The removal protocol:
- Player opens inventory
- Player clicks on a clothing slot in the
PlayerClothingUI - Client sends a removal request to the server
- Server calls the appropriate
askWear*method withnullasset andplayEffect = true - The worn item is returned to the player's inventory
- The
HumanClothescomponent updates to show the removed visual - The change is broadcast to all clients
The UseableClothing class is only activated for the equipping half of the wear/dequip lifecycle. The dequipping half bypasses the Useable system entirely. This asymmetry exists because:
- Equipping requires an animation (the "Use" clip, sound effects)
- Dequipping is instantaneous (click the slot, item moves to inventory)
- The inventory UI is already open during dequipping, providing visual feedback
Economy Integration Details
The Steam economy integration flows through askWear* methods:
Skin Resolution
csharp
private int resolveSkinID(ItemAsset asset)
{
if (asset == null) return 0;
if (asset.sharedSkinLookupID != asset.id)
{
// Shared skin: look up by the shared ID
if (channel.owner.getItemSkinItemDefID(asset.sharedSkinLookupID, out int itemDefId))
return Provider.provider.economyService.getInventorySkinID(itemDefId);
}
else
{
if (channel.owner.getItemSkinItemDefID(asset.id, out int itemDefId))
return Provider.provider.economyService.getInventorySkinID(itemDefId);
}
return 0;
}The skin resolution handles both standard skins and shared skins (where multiple items share the same skin model). The sharedSkinLookupID allows items like "Green Baseball Cap" and "Blue Baseball Cap" to share the same skin cosmetic without requiring duplicate economy entries.
Mythic Resolution
csharp
private ushort resolveMythicID(ItemAsset asset, int itemDefId)
{
ushort mythic = Provider.provider.economyService.getInventoryMythicID(itemDefId);
if (mythic == 0)
mythic = channel.owner.getParticleEffectForItemDef(itemDefId);
return mythic;
}Mythic effects are first resolved from the economy service, then fall back to the player's equipped particle effects. This allows mythics to function as both cosmetic items (from the economy) and achievement rewards (from player data).
Integration with Mannequins
As covered in the mannequin article, UseableClothing items interact with mannequins through the EMannequinUpdateMode.ADD flow:
csharp
case EMannequinUpdateMode.ADD:
if (!player.equipment.HasValidUseable
|| !player.equipment.IsEquipAnimationFinished
|| player.equipment.isBusy
|| player.equipment.asset == null
|| !(player.equipment.useable is UseableClothing))
return;
ItemJar item = player.inventory.getItem(...);
// Transfer item from hotbar to mannequin slot
player.equipment.use();
break;The mannequin checks that the player is holding a UseableClothing before accepting ADD mode, ensuring only actual clothing items are placed on the display.
Key Design Insights
- Server-authoritative slot assignment — All clothing slot mutations happen exclusively on the server in
simulate(); the client never directly modifies its own clothing state - Animation-first validation — Both
startPrimaryandsimulaterequire animation completion before executing their logic, preventing speed-hacks - Single responsibility —
UseableClothingonly handles the transition from inventory to clothing slot; visual rendering is delegated toHumanClothesand the economy proof validation is handled inaskWear* - No dequip path — Clothing items cannot be de-equipped through
UseableClothing; removal is handled by the clothing manager UI (PlayerClothingUI) which bypasses the Useable system entirely - Alert integration — The
AlertTool.alertcall ties into the zombie AI hearing system, making clothing changes a real gameplay consideration in stealth scenarios - State array preservation — The clothing item's state byte array (containing dye colors, economy proof, and plugin metadata) is preserved through the entire wear pipeline and stored on the clothing slot permanently
- Unscaled realtime timers —
Time.realtimeSinceStartup(not game time) is used for all animation timing, preventing pause-menu abuse to extend or skip the wear animation - Cross-fade disabled — The "Use" animation plays with crossFade = false, replacing the current animation immediately for responsive feel
