Skip to content

Player Clothing and Visual Equipment

Overview

The clothing system manages seven wearable slots—shirt, pants, hat, backpack, vest, mask, glasses—each with its own item asset type, quality tracking, and state array. Clothing provides both cosmetic appearance and stat modifiers (movement speed, fall damage, broken bone prevention). Three visual toggle layers (Cosmetic, Skin, Mythic) control rendering independently.

The primary class is PlayerClothing (2079 lines) at Unturned/Player/PlayerClothing.cs. Visual toggle states are defined by EVisualToggleType (COSMETIC, SKIN, MYTHIC). Clothing state is rendered through three separate HumanClothes instances (first-person, third-person, character preview).


Architecture: Three-Clothing-Layer Model

Every player maintains three distinct HumanClothes renderers, each serving a different camera viewpoint:

InstanceSource TransformPurpose
firstClothesplayer.first/Camera/ViewmodelFirst-person viewmodel arms and held clothing
thirdClothesplayer.thirdThird-person full body rendering for other players
characterClothesplayer.characterCharacter selection/inventory preview

Clothing state is written to all three during network replication (ReceiveClothingState). Each instance independently applies GUIDs, visual flags, and customizer settings (face, hair, beard, skin, color).

The isVisual, isSkinned, and isMythic booleans control rendering on all three instances simultaneously.


Seven Clothing Slots

Each slot has an ushort ID, byte quality, and byte[] state array. All slots follow identical patterns:

ItemShirtAsset    → shirt,  shirtQuality,  shirtState
ItemPantsAsset    → pants,  pantsQuality,  pantsState
ItemHatAsset      → hat,    hatQuality,    hatState
ItemBackpackAsset → backpack, backpackQuality, backpackState
ItemVestAsset     → vest,   vestQuality,   vestState
ItemMaskAsset     → mask,   maskQuality,   maskState
ItemGlassesAsset  → glasses, glassesQuality, glassesState

Asset-type properties are read from thirdClothes (e.g., shirtAsset => thirdClothes.shirtAsset).


Network Replication

Full State Snapshots

SendClothingState / ReceiveClothingState transmit the entire wardrobe as a single reliable RPC. The packet layout:

(per slot) GUID (16 bytes) + quality (1 byte) + stateArray (length-prefixed)
isVisual (1 bit)
isSkinned (1 bit)
isMythic (1 bit)

Sent during:

  • Initial player sync (SendInitialPlayerState)
  • Full update after any slot changes (updateClothes)

Per-Slot Updates

Each slot has its own SendWear* / ReceiveWear* pair for targeted changes:

  • SendWearShirt(Guid, byte quality, byte[] state, bool playEffect)
  • SendWearPants(Guid, byte quality, byte[] state, bool playEffect)
  • ...repeated for Hat, Backpack, Vest, Mask, Glasses

These are ClientInstanceMethod with ONLY_FROM_SERVER validation. Each receiver:

  1. Updates thirdClothes.slotGuid.
  2. Updates quality and state fields.
  3. Calls thirdClothes.apply() and firstClothes?.apply() or characterClothes?.apply() if present.
  4. Calls UpdateStatModifiers().
  5. Fires per-slot delegate and global event.
  6. Requests client asset integrity verification (ClientAssetIntegrity.QueueRequest).
  7. Plays wear audio from slotAsset.wearAudio (non-dedicated only).

Quality-Only Updates

Separate RPCs for quality changes without re-sending GUID/state:

  • SendShirtQuality(byte)
  • SendPantsQuality(byte) — etc., for all 7 slots.

These only fire the per-slot update delegate without re-applying the visual model.


Equip/Unequip Flow (Server Authoritative)

All slot changes originate from ReceiveSwap*Request RPCs (rate-limited 2 Hz, ONLY_FROM_OWNER).

Equip Flow (e.g., Shirt)

sendSwapShirt(page, x, y)
  → SendSwapShirtRequest(page, x, y)
  → ReceiveSwapShirtRequest(page, x, y)
  1. If the equipped slot selection matches the item's page (e.g., PlayerInventory.SHIRT), the current equipment is dequipped first (if busy, returns).
  2. If page == 255: removes current clothing — calls askWearSlot(0, 0, emptyState, true).
  3. Otherwise: a. Gets inventory index at (page, x, y). b. Validates the item exists and its EItemType matches (SHIRT, PANTS, etc.). c. Removes the item from inventory. d. Calls askWearSlot(jar.item.id, jar.item.quality, jar.item.state, true).

Unequip Flow

askWearSlot(asset, quality, state, playEffect):

  1. Saves current item ID, quality, state.
  2. Broadcasts the new item via SendWearSlot.InvokeAndLoopback to all clients (GUID, quality, state, playEffect).
  3. If the old item was non-zero, returns it to inventory via forceAddItem.

This unconditional return prevents item loss on server-crashed mid-swap.


Death Handling (onLifeUpdated)

When the player dies server-side:

  1. Checks Lose_Clothes_PvP or Lose_Clothes_PvE config.
  2. If true, drops each equipped item whose asset has shouldDropOnDeath == true via ItemManager.dropItem.
  3. Clears all seven slot GUIDs, qualities, and states to empty.
  4. Broadcasts the cleared state via SendClothingState loopback.

Save/Load System

Save File: /Player/Clothing.dat, version 7

byte   SAVEDATA_VERSION (7)
guid   shirtGuid
byte   shirtQuality
guid   pantsGuid
byte   pantsQuality
guid   hatGuid
byte   hatQuality
guid   backpackGuid
byte   backpackQuality
guid   vestGuid
byte   vestQuality
guid   maskGuid
byte   maskQuality
guid   glassesGuid
byte   glassesQuality
bool   isVisual
bool   isSkinned
bool   isMythic
byte[] shirtState (length-prefixed)
byte[] pantsState
byte[] hatState
byte[] backpackState
byte[] vestState
byte[] maskState
byte[] glassesState

Save Conditions

  • Only writes if wasLoadCalled is true.
  • If the player is dead and loseClothes is true, the save file is deleted (forcing fresh spawn).
  • thirdClothes must not be null.

Load

  • Reads from /Player/Clothing.dat only if the file exists and level type is SURVIVAL.
  • Version 7+ uses GUIDs for asset references. Earlier versions used ushort IDs (migrated).
  • Version 3+ added isVisual. Version 6+ added isSkinned and isMythic. Version 5+ added per-slot state arrays.
  • Fallback: all slots cleared to null qualities, empty state arrays.

Legacy State Handling

For version ≤4 saves, per-slot state arrays default to empty. A special case checks if glasses == 334 (night vision goggles) and initializes glassesState to new byte[1], allowing NVG toggle state to persist.


Visual Toggle System

EVisualToggleType defines three independently controlled visibility layers:

TypeControlled ByEffect
COSMETICthirdClothes.isVisual / firstClothes.isVisual / characterClothes.isVisualToggles cosmetic item visibility on all three clothing layers
SKINisSkinned (per-player bool)Applies/removes skin materials and meshes on equipped items via PlayerEquipment.applySkinVisual()
MYTHICthirdClothes.isMythic / firstClothes.isMythic / characterClothes.isMythicEnables/disables mythical particle effects on clothing and equipment

Toggle Request Flow

sendVisualToggle(EVisualToggleType)
  → SendVisualToggleRequest.Invoke(type)
  → ReceiveVisualToggleRequest(type)
    → determines new state (!current)
    → SendVisualToggleState.InvokeAndLoopback(type, newState)
    → ReceiveVisualToggleState(type, toggle)
      → COSMETIC: sets isVisual = toggle on all three HumanClothes instances; apply()
      → SKIN: sets isSkinned = toggle; calls player.equipment.applySkinVisual(), applyMythicVisual()
      → MYTHIC: sets isMythic = toggle on all three HumanClothes; apply(); calls player.equipment.applyMythicVisual()
      → fires VisualToggleChanged event

Plugin Interface

ServerSetVisualToggleState(EVisualToggleType type, bool isVisible) allows servers to override toggle state directly via RPC.


Stat Modifiers

UpdateStatModifiers() recalculates three aggregated player stats whenever clothing changes:

Movement Speed Multiplier

csharp
movementSpeedMultiplier = 1.0f;
movementSpeedMultiplier *= shirtAsset?.movementSpeedMultiplier ?? 1.0f;
// ... multiplied across all 7 worn slots

Result clamped implicitly by multiplication chain. Base is 1.0; tactical vests may reduce it; lightweight gear may keep it high.

Fall Damage Multiplier

Same multiplicative accumulation across all 7 slots. Values < 1.0 reduce fall damage; some assets set this to 0 for complete fall damage immunity.

Broken Bone Prevention

csharp
preventsFallingBrokenBones |= slotAsset?.preventsFallingBrokenBones ?? false;

Boolean OR across all slots. If any worn item prevents broken legs from falling, the player is immune.


Face, Hair, Beard Customization

Face index (0 to FACES_FREE + FACES_PRO) is managed through SendFaceState / ReceiveFaceState:

  • Server validates index bounds and pro status (channel.owner.isPro).
  • Applies to channel.owner.face, thirdClothes.face, and characterClothes.face.
  • Capital/maxilla/orbit indexes affect the HumanClothes face mesh selection.

ServerSetFace(byte index) is the plugin-safe server-side setter.


Stance Integration

onStanceUpdated (subscribed during client-side initialization) handles vehicle passenger seat visibility:

  • When in a vehicle, thirdClothes.hasBackpack is set to false if the seat's obj is non-null (seat has a visible player model that conceals the backpack).
  • Otherwise hasBackpack = true.

Visual Economy Items

During initialization (InitializePlayer), visual economy item IDs from Steam economy (channel.owner.shirtItem, pantsItem, etc.) are written to all three HumanClothes.visual* properties. These control which cosmetic appearance override is applied for items with skin support, independent of the underlying Guid.


Wear Audio System

Each clothing asset type has a wearAudio field (AudioReference). When a clothing item is equipped:

csharp
#if !DEDICATED_SERVER
if (playEffect && thirdClothes.shirtAsset != null)
{
    player.PlayAudioReference(thirdClothes.shirtAsset.wearAudio);
}
#endif

The playEffect parameter is false for initial load and true for in-game swaps. Audio only plays on non-dedicated servers. Each slot type (shirt, pants, hat, backpack, vest, mask, glasses) has identical audio logic in its ReceiveWear* handler.


Skeletal Attachment Points

HumanClothes transforms are attached at specific points on the player skeleton:

First-Person Viewmodel

firstClothes is attached at player.first/Camera/Viewmodel. Only the shirt and arms are visible in first-person. The firstClothes instance:

  • Only receives shirtGuid updates (other slots are not applied to first-person).
  • Receives isVisual and isMythic toggle flags.
  • Receives channel.owner.skin color for skin tone matching.

Third-Person Full Body

thirdClothes is attached at player.third. This instance receives all 7 slot GUIDs plus full customization (face, hair, beard, skin, color, beard color).

Character Preview

characterClothes is attached at player.character. This instance receives all 7 slot GUIDs, full customization, and additionally updates Characters.active.* properties to persist cosmetic choices back to the character metadata.


Character Customizer Properties

The HumanClothes component applies these cosmetic properties on each apply() call:

PropertySourceDescription
facechannel.owner.face (byte index)Face geometry selection
hairchannel.owner.hair (byte index)Hair geometry selection
beardchannel.owner.beard (byte index)Beard geometry selection
skinchannel.owner.skin (Color)Skin color
colorchannel.owner.color (Color)Hair color (no longer refers to shirt, despite the name)
BeardColorchannel.owner.BeardColor (Color)Separate beard tint

Pro Check

csharp
firstClothes.ShouldHairOverridesUseFallbackColor = !player.channel.owner.isPro;
thirdClothes.ShouldHairOverridesUseFallbackColor = !player.channel.owner.isPro;
characterClothes.ShouldHairOverridesUseFallbackColor = !player.channel.owner.isPro;

Non-pro accounts use fallback hair colors instead of premium override colors.


Clothing State Array Semantics

Each slot has a byte[] state that stores per-slot binary data. The semantics depend on the asset type:

SlotState ArrayMeaning
Glasses1 bytestate[0] = 0 (off) or 1 (on) for NVG/headlamp toggle
Mask1 byte (common)Stores mask configuration (filter status, etc.)
Other slotsVariableMod-defined per-asset state (e.g., dye color, charge level)

State arrays are serialized in the save file and replicated through the network. The ReceiveClothingState method reads each state array via reader.ReadStateArray() which is a length-prefixed byte array.


Vision Equipment Integration

Headlamp and Night Vision

Glasses with vision effects (headlamp, civilian NV, military NV) use their state byte as a toggle:

csharp
// In PlayerEquipment.ReceiveToggleVision():
player.clothing.glassesState[0] = (byte)(player.clothing.glassesState[0] == 0 ? 1 : 0);
updateVision();

When toggled:

  • Headlamp: Triggers a firemode effect at the player's position (light flash).
  • Civilian/Military NV: Plays a beep effect sound.
  • Toggle state is replicated to all clients by the server (SendToggleVision loopback).
  • The glassesState[0] toggle only appears when glassesState.Length == 1; items with zero-length or multi-length state arrays cannot be toggled.

Vision Lighting Effects

PlayerEquipment.updateVision() applies the glasses asset's vision mode to the level lighting:

csharp
if (player.clothing.glassesAsset != null)
{
    LevelLighting.vision = player.clothing.glassesAsset.vision;
    LevelLighting.nightvisionColor = player.clothing.glassesAsset.nightvisionColor;
    LevelLighting.nightvisionFogIntensity = player.clothing.glassesAsset.nightvisionFogIntensity;
    LevelLighting.updateLighting();
}

This is the same lighting pipeline used by weapon scopes; if both scope and glasses are active, the scope's vision takes priority via ApplyScopeVisionToLighting.


Mythical Effect System

Mythical effects are particle systems applied to cosmetic items:

csharp
// In ReceiveWear* and ReceiveClothingState:
// Not directly in PlayerClothing — managed by PlayerEquipment

The isMythic boolean on HumanClothes controls whether mythical effect rendering is active. When ServerSetVisualToggleState(MYTHIC, false) is called, isMythic is set to false on all three clothing instances and player.equipment.applyMythicVisual() disables the mythical effect particle systems on the equipped item.


Item Drop on Death — Detailed Logic

Per-Slot Drop Gate

Each clothing asset has a shouldDropOnDeath boolean. During onLifeUpdated(isDead=true):

csharp
if (shirtAsset != null && shirtAsset.shouldDropOnDeath)
    ItemManager.dropItem(new Item(shirt, 1, shirtQuality, shirtState), transform.position, false, true, true);

The dropItem parameters:

  1. item: New Item with ID, amount=1, quality, state.
  2. position: Player's current position.
  3. wasDroppedByPlayer: false (death drop, not manual).
  4. isBlindDrop: true (spawn on ground near position).
  5. isDroppedByDeath: true (special flag for death drops, prevents some pick-up rules).

Clear After Drop

After dropping items marked shouldDropOnDeath, all 7 slot GUIDs, qualities, and state arrays are cleared to empty/default values:

csharp
shirtState = new byte[0];
// ... for all 7 slots

A full SendClothingState loopback is sent to update all clients with the cleared state.


Movement Speed Modifier — Cumulative Formula

The movement speed modifier is calculated multiplicatively across all 7 worn slots:

movementSpeedMultiplier = 1.0
movementSpeedMultiplier *= shirtAsset?.movementSpeedMultiplier    // e.g., 1.0 for regular, 0.9 for tactical
movementSpeedMultiplier *= pantsAsset?.movementSpeedMultiplier    // e.g., 1.0 or 0.95
movementSpeedMultiplier *= hatAsset?.movementSpeedMultiplier      // typically 1.0
movementSpeedMultiplier *= backpackAsset?.movementSpeedMultiplier // e.g., 0.8 for large pack
movementSpeedMultiplier *= vestAsset?.movementSpeedMultiplier     // e.g., 0.85 for heavy vest
movementSpeedMultiplier *= maskAsset?.movementSpeedMultiplier     // typically 1.0
movementSpeedMultiplier *= glassesAsset?.movementSpeedMultiplier  // typically 1.0

A player wearing heavy vest (0.85), large backpack (0.8), and tactical pants (0.95): 1.0 * 0.85 * 0.8 * 0.95 = 0.646 — roughly 65% normal move speed.

This multiplier is consumed by PlayerMovement when calculating the player's velocity and is separate from the weapon-aiming movement speed multiplier.


Fall Damage Modifier — Cumulative Formula

Same multiplicative pattern:

fallingDamageMultiplier = 1.0
fallingDamageMultiplier *= shirtAsset?.fallingDamageMultiplier
// ... through all 7 slots

A player wearing parachute pants (0.0 fall damage multiplier) takes zero fall damage regardless of other slots. The preventsFallingBrokenBones is a boolean OR: if any single worn item has the flag, the player's legs cannot break from falling.


Event System

Per-Player Delegates

DelegateParameters
onShirtUpdated(ushort id, byte quality, byte[] state)
onPantsUpdatedsame
onHatUpdatedsame
onBackpackUpdatedsame
onVestUpdatedsame
onMaskUpdatedsame
onGlassesUpdatedsame
VisualToggleChanged(PlayerClothing sender)

Global Static Events

EventSignature
OnShirtChanged_GlobalAction<PlayerClothing>
OnPantsChanged_GlobalAction<PlayerClothing>
... (7 per-slot events)...
No global toggle event — use VisualToggleChanged on specific PlayerClothing instance.

Hands and Left-Handed Support

Both firstClothes and thirdClothes receive channel.owner.IsLeftHanded via the hand property during InitializePlayer. This flips the model X-scale in HumanClothes to mirror the equipment for left-handed characters.


Clothing Slot Mapping — PlayerEquipment Integration

Each slot corresponds to an inventory page constant in PlayerInventory:

SlotInventory PageEItemType
ShirtPlayerInventory.SHIRT (2)EItemType.SHIRT
PantsPlayerInventory.PANTS (3)EItemType.PANTS
HatPlayerInventory.HAT (4)EItemType.HAT
BackpackPlayerInventory.BACKPACK (5)EItemType.BACKPACK
VestPlayerInventory.VEST (6)EItemType.VEST
MaskPlayerInventory.MASK (7)EItemType.MASK
GlassesPlayerInventory.GLASSES (8)EItemType.GLASSES

When equipping a clothing item, PlayerEquipment.ReceiveSwapShirtRequest checks player.equipment.checkSelection(PlayerInventory.SHIRT). If the player currently has a weapon equipped in that slot, it is dequipped first to free the slot.

Dequip-on-Swap Pattern

All ReceiveSwap*Request methods for clothing follow:

csharp
if (player.equipment.checkSelection(PlayerInventory.SHIRT))
{
    if (player.equipment.isBusy) return;
    player.equipment.dequip();
}

This ensures the player cannot equip a shirt while holding a weapon in the same inventory page slot.


Backpack Capacity Integration

Backpacks affect inventory capacity through PlayerInventory:

  1. When a backpack is worn (ReceiveWearBackpack), the UpdateStatModifiers call updates the movement speed multiplier.
  2. The backpack asset's capacity is read by PlayerInventory to determine the number of additional storage slots available.
  3. When unequipping (wearing ID 0), capacity reverts to the base amount.

This is not handled in PlayerClothing directly but through PlayerInventory reading player.clothing.backpack to determine the currently worn backpack ID, which it maps to an ItemBackpackAsset for the slot count.


Network Protocol — Packet Breakdown

Full State Packet (SendClothingState)

Byte layout for the ReceiveClothingState RPC:

[Guid] newShirt     (16 bytes)
[uint8] quality     (1 byte)
[byte[]] state      (length-prefixed)
[Guid] newPants     (16)
[uint8] quality
[byte[]] state
[Guid] newHat       (16)
[uint8] quality
[byte[]] state
[Guid] newBackpack  (16)
[uint8] quality
[byte[]] state
[Guid] newVest      (16)
[uint8] quality
[byte[]] state
[Guid] newMask      (16)
[uint8] quality
[byte[]] state
[Guid] newGlasses   (16)
[uint8] quality
[byte[]] state
[bit] isVisual
[bit] isSkinned
[bit] isMythic

Total minimum size: 7 × (16 + 1 + 2 for empty state) + 3 bits = ~133 bytes per packet. This is sent reliably as part of the initial player state.

Per-Slot Wear Packet (SendWearShirt etc.)

[Guid] id       (16 bytes)
[uint8] quality (1 byte)
[byte[]] state  (length-prefixed)
[bit] playEffect

Total minimum: ~20 bytes. Sent reliably via loopback.

Quality-Only Update (SendShirtQuality etc.)

[uint8] quality (1 byte)

Very small; sent reliably. Only triggers the per-slot event delegate without visual model re-application.

Visual Toggle Packet (SendVisualToggleState)

[EVisualToggleType] type (enum, 1 byte)
[bit] toggle

Sent reliably via loopback.

Face Change Packet (SendFaceState)

[uint8] index (1 byte)

Sent reliably via loopback.


Save/Load Version History

VersionChanges
1Initial format (ushort IDs for all slots, no state arrays)
2Added isVisual boolean
3–4Unchanged from v2
5Added per-slot byte[] state arrays
6Added isSkinned and isMythic booleans
7Migrated from ushort IDs to GUIDs for asset references

Migration from ID to GUID

In version 6 and earlier, clothing assets were stored as ushort item IDs (e.g., thirdClothes.shirt = block.readUInt16()). Starting at version 7:

csharp
if (version > 6)
    thirdClothes.shirtGuid = block.readGUID();
else
    thirdClothes.shirt = block.readUInt16();

After reading all GUIDs or IDs, thirdClothes.apply() resolves the GUID/ID to the actual asset reference via Assets.find().


Debugging

  • updateMaskQuality(): Triggers a mask update delegate re-fire without network traffic — used when mask state changes through non-standard paths.
  • Clothing state arrays are opaque byte sequences defined per asset type (e.g., glasses state[0] toggles NVG on/off).
  • Client-side asset integrity checks queue requests for each worn asset GUID after receiving clothing state, preventing asset desync exploits.