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:
| Instance | Source Transform | Purpose |
|---|---|---|
firstClothes | player.first/Camera/Viewmodel | First-person viewmodel arms and held clothing |
thirdClothes | player.third | Third-person full body rendering for other players |
characterClothes | player.character | Character 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, glassesStateAsset-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:
- Updates
thirdClothes.slotGuid. - Updates quality and state fields.
- Calls
thirdClothes.apply()andfirstClothes?.apply()orcharacterClothes?.apply()if present. - Calls
UpdateStatModifiers(). - Fires per-slot delegate and global event.
- Requests client asset integrity verification (
ClientAssetIntegrity.QueueRequest). - 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)- If the equipped slot selection matches the item's page (e.g.,
PlayerInventory.SHIRT), the current equipment is dequipped first (if busy, returns). - If
page == 255: removes current clothing — callsaskWearSlot(0, 0, emptyState, true). - Otherwise: a. Gets inventory index at (page, x, y). b. Validates the item exists and its
EItemTypematches (SHIRT, PANTS, etc.). c. Removes the item from inventory. d. CallsaskWearSlot(jar.item.id, jar.item.quality, jar.item.state, true).
Unequip Flow
askWearSlot(asset, quality, state, playEffect):
- Saves current item ID, quality, state.
- Broadcasts the new item via
SendWearSlot.InvokeAndLoopbackto all clients (GUID, quality, state, playEffect). - 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:
- Checks
Lose_Clothes_PvPorLose_Clothes_PvEconfig. - If true, drops each equipped item whose asset has
shouldDropOnDeath == trueviaItemManager.dropItem. - Clears all seven slot GUIDs, qualities, and states to empty.
- Broadcasts the cleared state via
SendClothingStateloopback.
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[] glassesStateSave Conditions
- Only writes if
wasLoadCalledis true. - If the player is dead and
loseClothesis true, the save file is deleted (forcing fresh spawn). thirdClothesmust not be null.
Load
- Reads from
/Player/Clothing.datonly if the file exists and level type is SURVIVAL. - Version 7+ uses GUIDs for asset references. Earlier versions used
ushortIDs (migrated). - Version 3+ added
isVisual. Version 6+ addedisSkinnedandisMythic. 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:
| Type | Controlled By | Effect |
|---|---|---|
COSMETIC | thirdClothes.isVisual / firstClothes.isVisual / characterClothes.isVisual | Toggles cosmetic item visibility on all three clothing layers |
SKIN | isSkinned (per-player bool) | Applies/removes skin materials and meshes on equipped items via PlayerEquipment.applySkinVisual() |
MYTHIC | thirdClothes.isMythic / firstClothes.isMythic / characterClothes.isMythic | Enables/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 eventPlugin 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 slotsResult 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, andcharacterClothes.face. - Capital/maxilla/orbit indexes affect the
HumanClothesface 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.hasBackpackis set tofalseif 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);
}
#endifThe 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
shirtGuidupdates (other slots are not applied to first-person). - Receives
isVisualandisMythictoggle flags. - Receives
channel.owner.skincolor 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:
| Property | Source | Description |
|---|---|---|
face | channel.owner.face (byte index) | Face geometry selection |
hair | channel.owner.hair (byte index) | Hair geometry selection |
beard | channel.owner.beard (byte index) | Beard geometry selection |
skin | channel.owner.skin (Color) | Skin color |
color | channel.owner.color (Color) | Hair color (no longer refers to shirt, despite the name) |
BeardColor | channel.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:
| Slot | State Array | Meaning |
|---|---|---|
| Glasses | 1 byte | state[0] = 0 (off) or 1 (on) for NVG/headlamp toggle |
| Mask | 1 byte (common) | Stores mask configuration (filter status, etc.) |
| Other slots | Variable | Mod-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 (
SendToggleVisionloopback). - The
glassesState[0]toggle only appears whenglassesState.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 PlayerEquipmentThe 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:
item: New Item with ID, amount=1, quality, state.position: Player's current position.wasDroppedByPlayer: false (death drop, not manual).isBlindDrop: true (spawn on ground near position).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 slotsA 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.0A 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 slotsA 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
| Delegate | Parameters |
|---|---|
onShirtUpdated | (ushort id, byte quality, byte[] state) |
onPantsUpdated | same |
onHatUpdated | same |
onBackpackUpdated | same |
onVestUpdated | same |
onMaskUpdated | same |
onGlassesUpdated | same |
VisualToggleChanged | (PlayerClothing sender) |
Global Static Events
| Event | Signature |
|---|---|
OnShirtChanged_Global | Action<PlayerClothing> |
OnPantsChanged_Global | Action<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:
| Slot | Inventory Page | EItemType |
|---|---|---|
| Shirt | PlayerInventory.SHIRT (2) | EItemType.SHIRT |
| Pants | PlayerInventory.PANTS (3) | EItemType.PANTS |
| Hat | PlayerInventory.HAT (4) | EItemType.HAT |
| Backpack | PlayerInventory.BACKPACK (5) | EItemType.BACKPACK |
| Vest | PlayerInventory.VEST (6) | EItemType.VEST |
| Mask | PlayerInventory.MASK (7) | EItemType.MASK |
| Glasses | PlayerInventory.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:
- When a backpack is worn (
ReceiveWearBackpack), theUpdateStatModifierscall updates the movement speed multiplier. - The backpack asset's capacity is read by
PlayerInventoryto determine the number of additional storage slots available. - 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] isMythicTotal 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] playEffectTotal 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] toggleSent reliably via loopback.
Face Change Packet (SendFaceState)
[uint8] index (1 byte)Sent reliably via loopback.
Save/Load Version History
| Version | Changes |
|---|---|
| 1 | Initial format (ushort IDs for all slots, no state arrays) |
| 2 | Added isVisual boolean |
| 3–4 | Unchanged from v2 |
| 5 | Added per-slot byte[] state arrays |
| 6 | Added isSkinned and isMythic booleans |
| 7 | Migrated 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.
