]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Markings overhaul (#35938)
authorEd <96445749+TheShuEd@users.noreply.github.com>
Sun, 20 Apr 2025 21:01:50 +0000 (00:01 +0300)
committerGitHub <noreply@github.com>
Sun, 20 Apr 2025 21:01:50 +0000 (17:01 -0400)
* markings displacement setup

* ok i got it!

* fix map updating

* remove trackingLayers

* markings clean up and modernizize

* marking disabling displacements

* markings restriction

* dehihienize

* dehihiniezize 2

* aa

* nice

14 files changed:
Content.Client/Clothing/ClientClothingSystem.cs
Content.Client/DisplacementMap/DisplacementMapSystem.cs
Content.Client/Hands/Systems/HandsSystem.cs
Content.Client/Humanoid/HumanoidAppearanceSystem.cs
Content.Shared/Humanoid/HumanoidAppearanceComponent.cs
Content.Shared/Humanoid/Markings/MarkingManager.cs
Content.Shared/Humanoid/Markings/MarkingPoints.cs
Content.Shared/Humanoid/Markings/MarkingPrototype.cs
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_facial_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_hair.yml
Resources/Prototypes/Entities/Mobs/Species/vox.yml
Resources/Prototypes/Species/vox.yml
Resources/Textures/Mobs/Species/Vox/displacement.rsi/hair.png [new file with mode: 0644]
Resources/Textures/Mobs/Species/Vox/displacement.rsi/meta.json

index 5db7209d2228e60054dad6958078d46662b196a0..f01a170850e1dcd30dc331529a15f54aee038c02 100644 (file)
@@ -334,8 +334,11 @@ public sealed class ClientClothingSystem : ClothingSystem
                 if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId))
                     continue;
 
-                if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers))
+                if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, out var displacementKey))
+                {
+                    revealedLayers.Add(displacementKey);
                     index++;
+                }
             }
         }
 
index 6db164a09f092cf8fde31a2e3ea28c0ae9f0f436..9e9ad6135bd1420ed91c7e2a21954b0362ccd9d8 100644 (file)
@@ -9,22 +9,36 @@ public sealed class DisplacementMapSystem : EntitySystem
 {
     [Dependency] private readonly ISerializationManager _serialization = default!;
 
-    public bool TryAddDisplacement(DisplacementData data, SpriteComponent sprite, int index, string key, HashSet<string> revealedLayers)
+    /// <summary>
+    /// Attempting to apply a displacement map to a specific layer of SpriteComponent
+    /// </summary>
+    /// <param name="data">Information package for applying the displacement map</param>
+    /// <param name="sprite">SpriteComponent</param>
+    /// <param name="index">Index of the layer where the new map layer will be added</param>
+    /// <param name="key">Unique layer key, which will determine which layer to apply displacement map to</param>
+    /// <param name="displacementKey">The key of the new displacement map layer added by this function.</param>
+    /// <returns></returns>
+    public bool TryAddDisplacement(DisplacementData data,
+        SpriteComponent sprite,
+        int index,
+        object key,
+        out string displacementKey)
     {
+        displacementKey = $"{key}-displacement";
+
+        if (key.ToString() is null)
+            return false;
+
         if (data.ShaderOverride != null)
             sprite.LayerSetShader(index, data.ShaderOverride);
 
-        var displacementKey = $"{key}-displacement";
-        if (!revealedLayers.Add(displacementKey))
-        {
-            Log.Warning($"Duplicate key for DISPLACEMENT: {displacementKey}.");
-            return false;
-        }
+        if (sprite.LayerMapTryGet(displacementKey, out var oldIndex))
+            sprite.RemoveLayer(oldIndex);
 
         //allows you not to write it every time in the YML
         foreach (var pair in data.SizeMaps)
         {
-            pair.Value.CopyToShaderParameters??= new()
+            pair.Value.CopyToShaderParameters ??= new()
             {
                 LayerKey = "dummy",
                 ParameterTexture = "displacementMap",
@@ -45,21 +59,22 @@ public sealed class DisplacementMapSystem : EntitySystem
         if (actualRSI is not null)
         {
             if (actualRSI.Size.X != actualRSI.Size.Y)
-                Log.Warning($"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked");
+            {
+                Log.Warning(
+                    $"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked");
+            }
 
             var layerSize = actualRSI.Size.X;
-            if (data.SizeMaps.ContainsKey(layerSize))
-                displacementDataLayer = data.SizeMaps[layerSize];
+            if (data.SizeMaps.TryGetValue(layerSize, out var map))
+                displacementDataLayer = map;
         }
 
         var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
-        displacementLayer.CopyToShaderParameters!.LayerKey = key;
+        displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString() ?? "this is impossible";
 
         sprite.AddLayer(displacementLayer, index);
         sprite.LayerMapSet(displacementKey, index);
 
-        revealedLayers.Add(displacementKey);
-
         return true;
     }
 }
index f879e06a20ddb4e6cbf5fea2183ed78c06047149..7e4484fde6c7a65ed8c06f7db833609636490e6c 100644 (file)
@@ -348,14 +348,16 @@ namespace Content.Client.Hands.Systems
 
                 sprite.LayerSetData(index, layerData);
 
-                //Add displacement maps
-                if (hand.Location == HandLocation.Left && handComp.LeftHandDisplacement is not null)
-                    _displacement.TryAddDisplacement(handComp.LeftHandDisplacement, sprite, index, key, revealedLayers);
-                else if (hand.Location == HandLocation.Right && handComp.RightHandDisplacement is not null)
-                    _displacement.TryAddDisplacement(handComp.RightHandDisplacement, sprite, index, key, revealedLayers);
-                //Fallback to default displacement map
-                else if (handComp.HandDisplacement is not null)
-                    _displacement.TryAddDisplacement(handComp.HandDisplacement, sprite, index, key, revealedLayers);
+                // Add displacement maps
+                var displacement = hand.Location switch
+                {
+                    HandLocation.Left => handComp.LeftHandDisplacement,
+                    HandLocation.Right => handComp.RightHandDisplacement,
+                    _ => handComp.HandDisplacement
+                };
+
+                if (displacement is not null && _displacement.TryAddDisplacement(displacement, sprite, index, key, out var displacementKey))
+                    revealedLayers.Add(displacementKey);
             }
 
             RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
index aa9c4ccce768e305233a74d1ac94ade9612cd62c..c63112243e327a0ce1f6c9b7ec45375c259bff93 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Client.DisplacementMap;
 using Content.Shared.CCVar;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
@@ -16,6 +17,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly MarkingManager _markingManager = default!;
     [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+    [Dependency] private readonly DisplacementMapSystem _displacement = default!;
 
     public override void Initialize()
     {
@@ -369,6 +371,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
             {
                 sprite.LayerSetColor(layerId, Color.White);
             }
+
+            if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
+            {
+                _displacement.TryAddDisplacement(displacementData, sprite, targetLayer + j + 1, layerId, out _);
+            }
         }
     }
 
index 4a741074c9e82e8abd02ae8e967655a3daa2add9..aeb8d6716f386dace5797e1d233f2252aef9dc4e 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.DisplacementMap;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Inventory;
@@ -99,6 +100,12 @@ public sealed partial class HumanoidAppearanceComponent : Component
 
     [DataField]
     public ProtoId<MarkingPrototype>? UndergarmentBottom = new ProtoId<MarkingPrototype>("UndergarmentBottomBoxers");
+
+    /// <summary>
+    ///     The displacement maps that will be applied to specific layers of the humanoid.
+    /// </summary>
+    [DataField]
+    public Dictionary<HumanoidVisualLayers, DisplacementData> MarkingsDisplacement = new();
 }
 
 [DataDefinition]
index f45e6cf0600accf2b9d6a0b1d0925ae9c4eb6099..e844dc2280f5b66abb4391bcd200b49e0e793a00 100644 (file)
@@ -62,12 +62,12 @@ namespace Content.Shared.Humanoid.Markings
             string species)
         {
             var speciesProto = _prototypeManager.Index<SpeciesPrototype>(species);
-            var onlyWhitelisted = _prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted;
+            var markingPoints = _prototypeManager.Index(speciesProto.MarkingPoints);
             var res = new Dictionary<string, MarkingPrototype>();
 
             foreach (var (key, marking) in MarkingsByCategory(category))
             {
-                if (onlyWhitelisted && marking.SpeciesRestrictions == null)
+                if ((markingPoints.OnlyWhitelisted || markingPoints.Points[category].OnlyWhitelisted) && marking.SpeciesRestrictions == null)
                 {
                     continue;
                 }
index a5bcca9706b4d1ec42514102a99d2b6417c31a63..6b1696849fc3fcf29e858d1b4716eec33d85f247 100644 (file)
@@ -1,6 +1,5 @@
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
 namespace Content.Shared.Humanoid.Markings;
 
@@ -8,13 +7,23 @@ namespace Content.Shared.Humanoid.Markings;
 [Serializable, NetSerializable]
 public sealed partial class MarkingPoints
 {
-    [DataField("points", required: true)]
+    [DataField(required: true)]
     public int Points = 0;
-    [DataField("required", required: true)]
-    public bool Required = false;
+
+    [DataField(required: true)]
+    public bool Required;
+
+    /// <summary>
+    ///     If the user of this marking point set is only allowed to
+    ///     use whitelisted markings, and not globally usable markings.
+    ///     Only used for validation and profile construction. Ignored anywhere else.
+    /// </summary>
+    [DataField]
+    public bool OnlyWhitelisted;
+
     // Default markings for this layer.
-    [DataField("defaultMarkings", customTypeSerializer:typeof(PrototypeIdListSerializer<MarkingPrototype>))]
-    public List<string> DefaultMarkings = new();
+    [DataField]
+    public List<ProtoId<MarkingPrototype>> DefaultMarkings = new();
 
     public static Dictionary<MarkingCategories, MarkingPoints> CloneMarkingPointDictionary(Dictionary<MarkingCategories, MarkingPoints> self)
     {
@@ -26,6 +35,7 @@ public sealed partial class MarkingPoints
             {
                 Points = points.Points,
                 Required = points.Required,
+                OnlyWhitelisted = points.OnlyWhitelisted,
                 DefaultMarkings = points.DefaultMarkings
             };
         }
@@ -44,8 +54,9 @@ public sealed partial class MarkingPointsPrototype : IPrototype
     ///     use whitelisted markings, and not globally usable markings.
     ///     Only used for validation and profile construction. Ignored anywhere else.
     /// </summary>
-    [DataField("onlyWhitelisted")] public bool OnlyWhitelisted;
+    [DataField]
+    public bool OnlyWhitelisted;
 
-    [DataField("points", required: true)]
+    [DataField(required: true)]
     public Dictionary<MarkingCategories, MarkingPoints> Points { get; private set; } = default!;
 }
index ead061dd3952414d4ac0dfedaf07712a18044283..a6c578015ab71befe331cac701929e576dba95bd 100644 (file)
@@ -32,6 +32,13 @@ namespace Content.Shared.Humanoid.Markings
         [DataField("coloring")]
         public MarkingColors Coloring { get; private set; } = new();
 
+        /// <summary>
+        /// Do we need to apply any displacement maps to this marking? Set to false if your marking is incompatible
+        /// with a standard human doll, and is used for some special races with unusual shapes
+        /// </summary>
+        [DataField]
+        public bool CanBeDisplaced { get; private set; } = true;
+
         [DataField("sprites", required: true)]
         public List<SpriteSpecifier> Sprites { get; private set; } = default!;
 
index 066d19722361f0c1d908ade7197ccbd0271a1e70..67e686999a4787d3b536ed5c56bb94e0dedc8daa 100644 (file)
@@ -2,6 +2,7 @@
   id: VoxFacialHairBeard
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -11,6 +12,7 @@
   id: VoxFacialHairColonel
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -20,6 +22,7 @@
   id: VoxFacialHairFu
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -29,6 +32,7 @@
   id: VoxFacialHairMane
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -38,6 +42,7 @@
   id: VoxFacialHairManeSmall
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -47,6 +52,7 @@
   id: VoxFacialHairNeck
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
@@ -56,6 +62,7 @@
   id: VoxFacialHairTufts
   bodyPart: FacialHair
   markingCategory: FacialHair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
index 8748b3a093b48a1f198cd40a28cdef304ec09c50..7fc3fedaa24b11065a4973d235c50cbd9bed2560 100644 (file)
@@ -2,6 +2,7 @@
   id: VoxHairAfro
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -11,6 +12,7 @@
   id: VoxHairBraids
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -20,6 +22,7 @@
   id: VoxHairCrestedQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -29,6 +32,7 @@
   id: VoxHairEmperorQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -38,6 +42,7 @@
   id: VoxHairFlowing
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -47,6 +52,7 @@
   id: VoxHairHawk
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -56,6 +62,7 @@
   id: VoxHairHorns
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -65,6 +72,7 @@
   id: VoxHairKeelQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -74,6 +82,7 @@
   id: VoxHairKeetQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
@@ -83,6 +92,7 @@
   id: VoxHairKingly
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairLongBraid
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairMange
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairMohawk
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairNights
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairPony
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairRazorClipped
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairRazor
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairSortBraid
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairShortQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairSpotty
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairSurf
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairTielQuills
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairWiseBraid
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
   id: VoxHairYasu
   bodyPart: Hair
   markingCategory: Hair
+  canBeDisplaced: false
   speciesRestriction: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
index 4703bfd06b06643ff70873c8d5724c0f0a8a5686..94b5cebf26498a5d5697a0180d8899fe8b4965a8 100644 (file)
   - type: Body
     prototype: Vox
     requiredLegs: 2
-  - type: HumanoidAppearance
-    species: Vox
-    undergarmentTop: UndergarmentTopTanktopVox
-    undergarmentBottom: UndergarmentBottomBoxersVox
     #- type: VoxAccent # Not yet coded
   - type: Speech
     speechVerb: Vox
       sprite: "Effects/creampie.rsi"
       state: "creampie_vox" # Not default
       visible: false
+  - type: HumanoidAppearance
+    species: Vox
+    undergarmentTop: UndergarmentTopTanktopVox
+    undergarmentBottom: UndergarmentBottomBoxersVox
+    markingsDisplacement:
+      Hair:
+        sizeMaps:
+          32:
+            sprite: Mobs/Species/Vox/displacement.rsi
+            state: hair
   - type: Inventory
     speciesId: vox
     displacements:
     species: Vox
     undergarmentTop: UndergarmentTopTanktopVox
     undergarmentBottom: UndergarmentBottomBoxersVox
+    markingsDisplacement:
+      Hair:
+        sizeMaps:
+          32:
+            sprite: Mobs/Species/Vox/displacement.rsi
+            state: hair
   - type: Body
     prototype: Vox
   - type: Inventory
index 8501784b89d46c1681d3d7d09b3782443d8e4597..fd458b40f8b71f0f105806be54b51de5e7bcb990 100644 (file)
@@ -37,7 +37,6 @@
 
 - type: markingPoints
   id: MobVoxMarkingLimits
-  onlyWhitelisted: true
   points:
     Hair:
       points: 1
     FacialHair:
       points: 1
       required: false
+      onlyWhitelisted: true
     Head:
       points: 1
       required: true
+      onlyWhitelisted: true
     Snout:
       points: 1
       required: true
       defaultMarkings: [ VoxBeak ]
+      onlyWhitelisted: true
     Arms:
       points: 4
       required: true
       defaultMarkings: [ VoxLArmScales, VoxRArmScales, VoxRHandScales, VoxLHandScales ]
+      onlyWhitelisted: true
     Legs:
       points: 4
       required: true
       defaultMarkings: [ VoxLLegScales, VoxRLegScales, VoxRFootScales, VoxLFootScales ]
+      onlyWhitelisted: true
     UndergarmentTop:
       points: 1
       required: false
+      onlyWhitelisted: true
     UndergarmentBottom:
       points: 1
       required: false
+      onlyWhitelisted: true
     Chest:
       points: 1
       required: false
+      onlyWhitelisted: true
     Tail:
       points: 1
       required: true
       defaultMarkings: [ VoxTail ]
+      onlyWhitelisted: true
 
 - type: humanoidBaseSprite
   id: MobVoxEyes
diff --git a/Resources/Textures/Mobs/Species/Vox/displacement.rsi/hair.png b/Resources/Textures/Mobs/Species/Vox/displacement.rsi/hair.png
new file mode 100644 (file)
index 0000000..d52d859
Binary files /dev/null and b/Resources/Textures/Mobs/Species/Vox/displacement.rsi/hair.png differ
index 228f934114d00b424a1ac99006143eb854ae4311..0c910f85e5646dd868960e9cfacc7b060d4fb372 100644 (file)
@@ -1,7 +1,7 @@
 {
     "version": 1,
     "license": "CC-BY-SA-3.0",
-    "copyright": "jumpsuit state made by PJB3005. back, hand, head, and eyes states made by Flareguy, ears, hand_l, hand_r and shoes made by TheShuEd",
+    "copyright": "jumpsuit state made by PJB3005. back, hand, head, and eyes states made by Flareguy, ears, hand_l, hand_r, hair and shoes made by TheShuEd",
     "size": {
         "x": 32,
         "y": 32
             "name": "shoes",
             "directions": 4
         },
+        {
+            "name": "hair",
+            "directions": 4
+        },
         {
             "name": "hand_l",
             "directions": 4