Browse Source

[csharp] See #1346: Port bone/constraint association with skins. Also contains second (and final) partial port of commit ff5b854. Adapted spine-unity for skin changes. Fixed a bug in Skin setter property.

Harald Csaszar 6 năm trước cách đây
mục cha
commit
c63bc7b88f

+ 1 - 1
spine-csharp/src/Animation.cs

@@ -52,7 +52,7 @@ namespace Spine {
 		/// <summary>The duration of the animation in seconds, which is the highest time of all keys in the timeline.</summary>
 		public float Duration { get { return duration; } set { duration = value; } }
 
-		/// <summary>The animation's name, which is unique within the skeleton.</summary>
+		/// <summary>The animation's name, which is unique across all animations in the skeleton.</summary>
 		public string Name { get { return name; } }
 
 		/// <summary>Applies all the animation's timelines to the specified skeleton.</summary>

+ 2 - 2
spine-csharp/src/Attachments/RegionAttachment.cs

@@ -195,8 +195,8 @@ namespace Spine {
 			copy.rotation = rotation;
 			copy.width = width;
 			copy.height = height;
-			Array.Copy(uvs, 0, copy.uvs, 0, uvs.Length);
-			Array.Copy(offset, 0, copy.offset, 0, offset.Length);
+			Array.Copy(uvs, 0, copy.uvs, 0, 8);
+			Array.Copy(offset, 0, copy.offset, 0, 8);
 			return copy;
 		}
 	}

+ 1 - 2
spine-csharp/src/Attachments/VertexAttachment.cs

@@ -133,8 +133,7 @@ namespace Spine {
 			return this == sourceAttachment;
 		}
 
-		///<summary>Internal method used by VertexAttachment subclasses to copy basic data. Does not copy id (generated) and name (set on
-		/// construction).</summary>
+		///<summary>Does not copy id (generated) or name (set on construction).</summary>
 		internal void CopyTo (VertexAttachment attachment) {
 			if (bones != null) {
 				attachment.bones = new int[bones.Length];

+ 1 - 1
spine-csharp/src/Bone.cs

@@ -52,7 +52,7 @@ namespace Spine {
 		internal float a, b, worldX;
 		internal float c, d, worldY;
 
-		internal bool sorted;
+		internal bool sorted, update;
 
 		public BoneData Data { get { return data; } }
 		public Skeleton Skeleton { get { return skeleton; } }

+ 7 - 1
spine-csharp/src/BoneData.cs

@@ -37,11 +37,12 @@ namespace Spine {
 		internal float length;
 		internal float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
 		internal TransformMode transformMode = TransformMode.Normal;
+		internal bool skinRequired;
 
 		/// <summary>The index of the bone in Skeleton.Bones</summary>
 		public int Index { get { return index; } }
 
-		/// <summary>The name of the bone, which is unique within the skeleton.</summary>
+		/// <summary>The name of the bone, which is unique across all bones in the skeleton.</summary>
 		public string Name { get { return name; } }
 
 		/// <summary>May be null.</summary>
@@ -73,6 +74,11 @@ namespace Spine {
 		/// <summary>The transform mode for how parent world transforms affect this bone.</summary>
 		public TransformMode TransformMode { get { return transformMode; } set { transformMode = value; } }
 
+		///<summary>When true, <see cref="Skeleton.UpdateWorldTransform()"/> only updates this bone if the <see cref="Skeleton.Skin"/> contains this
+		/// bone.</summary>
+		/// <seealso cref="Skin.Bones"/>
+		public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } }
+
 		/// <param name="parent">May be null.</param>
 		public BoneData (int index, string name, BoneData parent) {
 			if (index < 0) throw new ArgumentException("index must be >= 0", "index");

+ 30 - 7
spine-csharp/src/IConstraint.cs → spine-csharp/src/ConstraintData.cs

@@ -27,13 +27,36 @@
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-namespace Spine {
-	
-	/// <summary>The interface for all constraints.</summary>
-	public interface IConstraint : IUpdatable {
-		/// <summary>The ordinal for the order a skeleton's constraints will be applied.</summary>
-		int Order { get; }
+using System;
+using System.Collections.Generic;
 
-	}
+namespace Spine
+{
+	/// <summary>The base class for all constraint datas.</summary>
+	public abstract class ConstraintData {
+		internal readonly string name;
+		internal int order;
+		internal bool skinRequired;
+
+		public ConstraintData (string name) {
+			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
+			this.name = name;
+		}
+
+		/// <summary> The constraint's name, which is unique across all constraints in the skeleton of the same type.</summary>
+		public string Name { get { return name; } }
 
+		///<summary>The ordinal of this constraint for the order a skeleton's constraints will be applied by
+		/// <see cref="Skeleton.UpdateWorldTransform()"/>.</summary>
+		public int Order { get { return order; } set { order = value; } }
+
+		///<summary>When true, <see cref="Skeleton.UpdateWorldTransform()"/> only updates this constraint if the <see cref="Skeleton.Skin"/> contains
+		/// this constraint.</summary>
+		///<seealso cref="Skin.Constraints"/>
+		public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } }
+
+		override public string ToString () {
+			return name;
+		}
+	}
 }

+ 1 - 1
spine-csharp/src/EventData.cs

@@ -34,7 +34,7 @@ namespace Spine {
 	public class EventData {
 		internal string name;
 
-		/// <summary>The name of the event, which is unique within the skeleton.</summary>
+		/// <summary>The name of the event, which is unique across all events in the skeleton.</summary>
 		public string Name { get { return name; } }
 		public int Int { get; set; }
 		public float Float { get; set; }

+ 1 - 6
spine-csharp/src/IkConstraint.cs

@@ -37,7 +37,7 @@ namespace Spine {
 	/// <para>
 	/// See <a href="http://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide.</para>
 	/// </summary>
-	public class IkConstraint : IConstraint {
+	public class IkConstraint : IUpdatable {
 		internal IkConstraintData data;
 		internal ExposedList<Bone> bones = new ExposedList<Bone>();
 		internal Bone target;
@@ -93,11 +93,6 @@ namespace Spine {
 			}
 		}
 
-
-		public int Order {
-			get { return data.order; }
-		}
-
 		/// <summary>The bones that will be modified by this IK constraint.</summary>
 		public ExposedList<Bone> Bones {
 			get { return bones; }

+ 2 - 20
spine-csharp/src/IkConstraintData.cs

@@ -32,23 +32,14 @@ using System.Collections.Generic;
 
 namespace Spine {
 	/// <summary>Stores the setup pose for an IkConstraint.</summary>
-	public class IkConstraintData {
-		internal string name;
-		internal int order;
+	public class IkConstraintData : ConstraintData {
 		internal List<BoneData> bones = new List<BoneData>();
 		internal BoneData target;
 		internal int bendDirection = 1;
 		internal bool compress, stretch, uniform;
 		internal float mix = 1;
 
-		/// <summary>The IK constraint's name, which is unique within the skeleton.</summary>
-		public string Name {
-			get { return name; }
-		}
-
-		public int Order {
-			get { return order; }
-			set { order = value; }
+		public IkConstraintData (string name) : base(name) {
 		}
 
 		/// <summary>The bones that are constrained by this IK Constraint.</summary>
@@ -98,14 +89,5 @@ namespace Spine {
 			get { return uniform; }
 			set { uniform = value; }
 		}
-
-		public IkConstraintData (string name) {
-			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
-			this.name = name;
-		}
-
-		override public string ToString () {
-			return name;
-		}
 	}
 }

+ 1 - 6
spine-csharp/src/PathConstraint.cs

@@ -38,7 +38,7 @@ namespace Spine {
 	/// <para>
 	/// See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide.</para>
 	/// </summary>
-	public class PathConstraint : IConstraint {
+	public class PathConstraint : IUpdatable {
 		const int NONE = -1, BEFORE = -2, AFTER = -3;
 		const float Epsilon = 0.00001f;
 
@@ -446,7 +446,6 @@ namespace Spine {
 			}
 		}
 		
-		public int Order { get { return data.order; } }
 		/// <summary>The position along the path.</summary>
 		public float Position { get { return position; } set { position = value; } }
 		/// <summary>The spacing between bones.</summary>
@@ -461,9 +460,5 @@ namespace Spine {
 		public Slot Target { get { return target; } set { target = value; } }
 		/// <summary>The path constraint's setup pose data.</summary>
 		public PathConstraintData Data { get { return data; } }
-
-		override public string ToString () {
-			return data.name;
-		}
 	}
 }

+ 4 - 14
spine-csharp/src/PathConstraintData.cs

@@ -30,9 +30,7 @@
 using System;
 
 namespace Spine {
-	public class PathConstraintData {
-		internal string name;
-		internal int order;
+	public class PathConstraintData : ConstraintData {
 		internal ExposedList<BoneData> bones = new ExposedList<BoneData>();
 		internal SlotData target;
 		internal PositionMode positionMode;
@@ -41,8 +39,9 @@ namespace Spine {
 		internal float offsetRotation;
 		internal float position, spacing, rotateMix, translateMix;
 
-		public string Name { get { return name; } }
-		public int Order { get { return order; } set { order = value; } }
+		public PathConstraintData (string name) : base(name) {
+		}
+
 		public ExposedList<BoneData> Bones { get { return bones; } }
 		public SlotData Target { get { return target; } set { target = value; } }			
 		public PositionMode PositionMode { get { return positionMode; } set { positionMode = value; } }
@@ -53,15 +52,6 @@ namespace Spine {
 		public float Spacing { get { return spacing; } set { spacing = value; } }
 		public float RotateMix { get { return rotateMix; } set { rotateMix = value; } }
 		public float TranslateMix { get { return translateMix; } set { translateMix = value; } }
-
-		public PathConstraintData (String name) {
-			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
-			this.name = name;
-		}
-
-		public override string ToString () {
-			return name;
-		}
 	}
 	
 	public enum PositionMode {

+ 32 - 17
spine-csharp/src/Skeleton.cs

@@ -55,7 +55,7 @@ namespace Spine {
 		public ExposedList<IkConstraint> IkConstraints { get { return ikConstraints; } }
 		public ExposedList<PathConstraint> PathConstraints { get { return pathConstraints; } }
 		public ExposedList<TransformConstraint> TransformConstraints { get { return transformConstraints; } }
-		public Skin Skin { get { return skin; } set { skin = value; } }
+		public Skin Skin { get { return skin; } set { SetSkin(value); } }
 		public float R { get { return r; } set { r = value; } }
 		public float G { get { return g; } set { g = value; } }
 		public float B { get { return b; } set { b = value; } }
@@ -118,21 +118,25 @@ namespace Spine {
 			UpdateWorldTransform();
 		}
 
-		/// <summary>Caches information about bones and constraints. Must be called if bones, constraints or weighted path attachments are added
-		/// or removed.</summary>
+		/// <summary>Caches information about bones and constraints. Must be called if the skin is modified or if bones, constraints, or
+		/// weighted path attachments are added or removed.</summary>
 		public void UpdateCache () {
 			var updateCache = this.updateCache;
 			updateCache.Clear();
 			this.updateCacheReset.Clear();
 
+			int boneCount = this.bones.Items.Length;
 			var bones = this.bones;
-			for (int i = 0, n = bones.Count; i < n; i++)
-				bones.Items[i].sorted = false;
+			for (int i = 0; i < boneCount; i++) {
+				Bone bone = bones.Items[i];
+				bone.update = !bone.data.skinRequired || (skin != null && skin.bones.Contains(bone.data));
+				bone.sorted = !bone.update;
+			}
 
+			int ikCount = this.ikConstraints.Count, transformCount = this.transformConstraints.Count, pathCount = this.pathConstraints.Count;
 			var ikConstraints = this.ikConstraints;
 			var transformConstraints = this.transformConstraints;
 			var pathConstraints = this.pathConstraints;
-			int ikCount = ikConstraints.Count, transformCount = transformConstraints.Count, pathCount = pathConstraints.Count;
 			int constraintCount = ikCount + transformCount + pathCount;
 			//outer:
 			for (int i = 0; i < constraintCount; i++) {
@@ -160,11 +164,13 @@ namespace Spine {
 				continue_outer: {}
 			}
 
-			for (int i = 0, n = bones.Count; i < n; i++)
+			for (int i = 0; i < boneCount; i++)
 				SortBone(bones.Items[i]);
 		}
 
 		private void SortIkConstraint (IkConstraint constraint) {
+			if (constraint.data.skinRequired && (skin == null || !skin.constraints.Contains(constraint.data))) return;
+
 			Bone target = constraint.target;
 			SortBone(target);
 
@@ -185,15 +191,15 @@ namespace Spine {
 		}
 
 		private void SortPathConstraint (PathConstraint constraint) {
+			if (constraint.data.skinRequired && (skin == null || !skin.constraints.Contains(constraint.data))) return;
+
 			Slot slot = constraint.target;
 			int slotIndex = slot.data.index;
 			Bone slotBone = slot.bone;
 			if (skin != null) SortPathConstraintAttachment(skin, slotIndex, slotBone);
 			if (data.defaultSkin != null && data.defaultSkin != skin)
 				SortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone);
-			for (int ii = 0, nn = data.skins.Count; ii < nn; ii++)
-				SortPathConstraintAttachment(data.skins.Items[ii], slotIndex, slotBone);
-
+			
 			Attachment attachment = slot.attachment;
 			if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone);
 
@@ -211,6 +217,8 @@ namespace Spine {
 		}
 
 		private void SortTransformConstraint (TransformConstraint constraint) {
+			if (constraint.data.skinRequired && (skin == null || !skin.constraints.Contains(constraint.data))) return;
+
 			SortBone(constraint.target);
 
 			var constrained = constraint.bones;
@@ -269,6 +277,7 @@ namespace Spine {
 			var bonesItems = bones.Items;
 			for (int i = 0, n = bones.Count; i < n; i++) {
 				Bone bone = bonesItems[i];
+				if (!bone.update) continue;
 				if (bone.sorted) SortReset(bone.children);
 				bone.sorted = false;
 			}
@@ -295,7 +304,8 @@ namespace Spine {
 		}
 
 		/// <summary>
-		/// Updates the world transform for each bone and applies all constraints. The root bone will be temporarily parented to the specified bone.
+		/// Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
+		/// all constraints.
 	 	/// </summary>
 		public void UpdateWorldTransform (Bone parent) {
 			// This partial update avoids computing the world transform for constrained bones when 1) the bone is not updated
@@ -315,8 +325,7 @@ namespace Spine {
 				bone.appliedValid = true;
 			}
 
-			// Apply the parent bone transform to the root bone. The root bone
-			// always inherits scale, rotation and reflection.
+			// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
 			Bone rootBone = this.RootBone;
 			float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
 			rootBone.worldX = pa * x + pb * y + parent.worldX;
@@ -447,8 +456,12 @@ namespace Spine {
 		}
 
 		/// <summary>
-		/// <para>Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. 
-		/// If there was no old skin, each slot's setup mode attachment is attached from the new skin.</para>
+		/// <para>Sets the skin used to look up attachments before looking in the <see cref="SkeletonData.DefaultSkin"/>. If the
+		/// skin is changed, <see cref="UpdateCache()"/> is called.
+		/// </para>
+	 	/// <para>Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. 
+		/// If there was no old skin, each slot's setup mode attachment is attached from the new skin.
+		/// </para>
 		/// <para>After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling 
 		/// <see cref="Skeleton.SetSlotsToSetupPose()"/>. 
 		/// Also, often <see cref="AnimationState.Apply(Skeleton)"/> is called before the next time the 
@@ -456,6 +469,7 @@ namespace Spine {
 		/// </summary>
 		/// <param name="newSkin">May be null.</param>
 		public void SetSkin (Skin newSkin) {
+			if (newSkin == skin) return;
 			if (newSkin != null) {
 				if (skin != null)
 					newSkin.AttachAll(this, skin);
@@ -472,6 +486,7 @@ namespace Spine {
 				}
 			}
 			skin = newSkin;
+			UpdateCache();
 		}
 
 		/// <summary>Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment name.</summary>
@@ -528,7 +543,7 @@ namespace Spine {
 			ExposedList<TransformConstraint> transformConstraints = this.transformConstraints;
 			for (int i = 0, n = transformConstraints.Count; i < n; i++) {
 				TransformConstraint transformConstraint = transformConstraints.Items[i];
-				if (transformConstraint.data.name == constraintName) return transformConstraint;
+				if (transformConstraint.data.Name == constraintName) return transformConstraint;
 			}
 			return null;
 		}
@@ -539,7 +554,7 @@ namespace Spine {
 			ExposedList<PathConstraint> pathConstraints = this.pathConstraints;
 			for (int i = 0, n = pathConstraints.Count; i < n; i++) {
 				PathConstraint constraint = pathConstraints.Items[i];
-				if (constraint.data.name.Equals(constraintName)) return constraint;
+				if (constraint.data.Name.Equals(constraintName)) return constraint;
 			}
 			return null;
 		}

+ 19 - 7
spine-csharp/src/SkeletonBinary.cs

@@ -174,6 +174,7 @@ namespace Spine {
 				data.shearY = ReadFloat(input);
 				data.length = ReadFloat(input) * scale;
 				data.transformMode = TransformModeValues[ReadVarint(input, true)];
+				data.skinRequired = ReadBoolean(input);
 				if (nonessential) ReadInt(input); // Skip bone color.
 				skeletonData.bones.Add(data);
 			}
@@ -206,6 +207,7 @@ namespace Spine {
 			for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
 				IkConstraintData data = new IkConstraintData(ReadString(input));
 				data.order = ReadVarint(input, true);
+				data.skinRequired = ReadBoolean(input);
 				for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++)
 					data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]);
 				data.target = skeletonData.bones.Items[ReadVarint(input, true)];
@@ -221,6 +223,7 @@ namespace Spine {
 			for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
 				TransformConstraintData data = new TransformConstraintData(ReadString(input));
 				data.order = ReadVarint(input, true);
+				data.skinRequired = ReadBoolean(input);
 				for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++)
 				    data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]);
 				data.target = skeletonData.bones.Items[ReadVarint(input, true)];
@@ -243,6 +246,7 @@ namespace Spine {
 			for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
 				PathConstraintData data = new PathConstraintData(ReadString(input));
 				data.order = ReadVarint(input, true);
+				data.skinRequired = ReadBoolean(input);
 				for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++)
 					data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]);
 				data.target = skeletonData.slots.Items[ReadVarint(input, true)];
@@ -260,7 +264,7 @@ namespace Spine {
 			}
 
 			// Default skin.
-			Skin defaultSkin = ReadSkin(input, skeletonData, "default", nonessential);
+			Skin defaultSkin = ReadSkin(input, skeletonData, true, nonessential);
 			if (defaultSkin != null) {
 				skeletonData.defaultSkin = defaultSkin;
 				skeletonData.skins.Add(defaultSkin);
@@ -268,7 +272,7 @@ namespace Spine {
 
 			// Skins.
 			for (int i = 0, n = ReadVarint(input, true); i < n; i++)
-				skeletonData.skins.Add(ReadSkin(input, skeletonData, ReadString(input), nonessential));
+				skeletonData.skins.Add(ReadSkin(input, skeletonData, false, nonessential));
 
 			// Linked meshes.
 			for (int i = 0, n = linkedMeshes.Count; i < n; i++) {
@@ -312,11 +316,19 @@ namespace Spine {
 
 
 		/// <returns>May be null.</returns>
-		private Skin ReadSkin (Stream input, SkeletonData skeletonData, String skinName, bool nonessential) {
-			int slotCount = ReadVarint(input, true);
-			if (slotCount == 0) return null;
-			Skin skin = new Skin(skinName);
-			for (int i = 0; i < slotCount; i++) {
+		private Skin ReadSkin (Stream input, SkeletonData skeletonData, bool defaultSkin, bool nonessential) {
+			Skin skin = new Skin(defaultSkin ? "default" : ReadString(input));
+			if (!defaultSkin) {
+				for (int i = 0, n = ReadVarint(input, true); i < n; i++)
+					skin.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]);
+				for (int i = 0, n = ReadVarint(input, true); i < n; i++)
+					skin.constraints.Add(skeletonData.ikConstraints.Items[ReadVarint(input, true)]);
+				for (int i = 0, n = ReadVarint(input, true); i < n; i++)
+					skin.constraints.Add(skeletonData.transformConstraints.Items[ReadVarint(input, true)]);
+				for (int i = 0, n = ReadVarint(input, true); i < n; i++)
+					skin.constraints.Add(skeletonData.pathConstraints.Items[ReadVarint(input, true)]);
+			}
+			for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
 				int slotIndex = ReadVarint(input, true);
 				for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) {
 					String name = ReadString(input);

+ 111 - 67
spine-csharp/src/SkeletonJson.cs

@@ -109,27 +109,30 @@ namespace Spine {
 			}
 
 			// Bones.
-			foreach (Dictionary<string, Object> boneMap in (List<Object>)root["bones"]) {
-				BoneData parent = null;
-				if (boneMap.ContainsKey("parent")) {
-					parent = skeletonData.FindBone((string)boneMap["parent"]);
-					if (parent == null)
-						throw new Exception("Parent bone not found: " + boneMap["parent"]);
+			if (root.ContainsKey("bones")) {
+				foreach (Dictionary<string, Object> boneMap in (List<Object>)root["bones"]) {
+					BoneData parent = null;
+					if (boneMap.ContainsKey("parent")) {
+						parent = skeletonData.FindBone((string)boneMap["parent"]);
+						if (parent == null)
+							throw new Exception("Parent bone not found: " + boneMap["parent"]);
+					}
+					var data = new BoneData(skeletonData.Bones.Count, (string)boneMap["name"], parent);
+					data.length = GetFloat(boneMap, "length", 0) * scale;
+					data.x = GetFloat(boneMap, "x", 0) * scale;
+					data.y = GetFloat(boneMap, "y", 0) * scale;
+					data.rotation = GetFloat(boneMap, "rotation", 0);
+					data.scaleX = GetFloat(boneMap, "scaleX", 1);
+					data.scaleY = GetFloat(boneMap, "scaleY", 1);
+					data.shearX = GetFloat(boneMap, "shearX", 0);
+					data.shearY = GetFloat(boneMap, "shearY", 0);
+
+					string tm = GetString(boneMap, "transform", TransformMode.Normal.ToString());
+					data.transformMode = (TransformMode)Enum.Parse(typeof(TransformMode), tm, true);
+					data.skinRequired = GetBoolean(boneMap, "skin", false);
+
+					skeletonData.bones.Add(data);
 				}
-				var data = new BoneData(skeletonData.Bones.Count, (string)boneMap["name"], parent);
-				data.length = GetFloat(boneMap, "length", 0) * scale;
-				data.x = GetFloat(boneMap, "x", 0) * scale;
-				data.y = GetFloat(boneMap, "y", 0) * scale;
-				data.rotation = GetFloat(boneMap, "rotation", 0);
-				data.scaleX = GetFloat(boneMap, "scaleX", 1);
-				data.scaleY = GetFloat(boneMap, "scaleY", 1);
-				data.shearX = GetFloat(boneMap, "shearX", 0);
-				data.shearY = GetFloat(boneMap, "shearY", 0);
-
-				string tm = GetString(boneMap, "transform", TransformMode.Normal.ToString());
-				data.transformMode = (TransformMode)Enum.Parse(typeof(TransformMode), tm, true);
-
-				skeletonData.bones.Add(data);
 			}
 
 			// Slots.
@@ -171,16 +174,19 @@ namespace Spine {
 				foreach (Dictionary<string, Object> constraintMap in (List<Object>)root["ik"]) {
 					IkConstraintData data = new IkConstraintData((string)constraintMap["name"]);
 					data.order = GetInt(constraintMap, "order", 0);
+					data.skinRequired = GetBoolean(constraintMap,"skin", false);
 
-					foreach (string boneName in (List<Object>)constraintMap["bones"]) {
-						BoneData bone = skeletonData.FindBone(boneName);
-						if (bone == null) throw new Exception("IK constraint bone not found: " + boneName);
-						data.bones.Add(bone);
+					if (constraintMap.ContainsKey("bones")) {
+						foreach (string boneName in (List<Object>)constraintMap["bones"]) {
+							BoneData bone = skeletonData.FindBone(boneName);
+							if (bone == null) throw new Exception("IK bone not found: " + boneName);
+							data.bones.Add(bone);
+						}
 					}
 
 					string targetName = (string)constraintMap["target"];
 					data.target = skeletonData.FindBone(targetName);
-					if (data.target == null) throw new Exception("Target bone not found: " + targetName);
+					if (data.target == null) throw new Exception("IK target bone not found: " + targetName);
 					data.mix = GetFloat(constraintMap, "mix", 1);
 					data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1;
 					data.compress = GetBoolean(constraintMap, "compress", false);
@@ -196,16 +202,19 @@ namespace Spine {
 				foreach (Dictionary<string, Object> constraintMap in (List<Object>)root["transform"]) {
 					TransformConstraintData data = new TransformConstraintData((string)constraintMap["name"]);
 					data.order = GetInt(constraintMap, "order", 0);
+					data.skinRequired = GetBoolean(constraintMap,"skin", false);
 
-					foreach (string boneName in (List<Object>)constraintMap["bones"]) {
-						BoneData bone = skeletonData.FindBone(boneName);
-						if (bone == null) throw new Exception("Transform constraint bone not found: " + boneName);
-						data.bones.Add(bone);
+					if (constraintMap.ContainsKey("bones")) {
+						foreach (string boneName in (List<Object>)constraintMap["bones"]) {
+							BoneData bone = skeletonData.FindBone(boneName);
+							if (bone == null) throw new Exception("Transform constraint bone not found: " + boneName);
+							data.bones.Add(bone);
+						}
 					}
 
 					string targetName = (string)constraintMap["target"];
 					data.target = skeletonData.FindBone(targetName);
-					if (data.target == null) throw new Exception("Target bone not found: " + targetName);
+					if (data.target == null) throw new Exception("Transform constraint target bone not found: " + targetName);
 
 					data.local = GetBoolean(constraintMap, "local", false);
 					data.relative = GetBoolean(constraintMap, "relative", false);
@@ -231,16 +240,19 @@ namespace Spine {
 				foreach (Dictionary<string, Object> constraintMap in (List<Object>)root["path"]) {
 					PathConstraintData data = new PathConstraintData((string)constraintMap["name"]);
 					data.order = GetInt(constraintMap, "order", 0);
+					data.skinRequired = GetBoolean(constraintMap,"skin", false);
 
-					foreach (string boneName in (List<Object>)constraintMap["bones"]) {
-						BoneData bone = skeletonData.FindBone(boneName);
-						if (bone == null) throw new Exception("Path bone not found: " + boneName);
-						data.bones.Add(bone);
+					if (constraintMap.ContainsKey("bones")) {
+						foreach (string boneName in (List<Object>)constraintMap["bones"]) {
+							BoneData bone = skeletonData.FindBone(boneName);
+							if (bone == null) throw new Exception("Path bone not found: " + boneName);
+							data.bones.Add(bone);
+						}
 					}
 
 					string targetName = (string)constraintMap["target"];
 					data.target = skeletonData.FindSlot(targetName);
-					if (data.target == null) throw new Exception("Target slot not found: " + targetName);
+					if (data.target == null) throw new Exception("Path target slot not found: " + targetName);
 
 					data.positionMode = (PositionMode)Enum.Parse(typeof(PositionMode), GetString(constraintMap, "positionMode", "percent"), true);
 					data.spacingMode = (SpacingMode)Enum.Parse(typeof(SpacingMode), GetString(constraintMap, "spacingMode", "length"), true);
@@ -259,18 +271,48 @@ namespace Spine {
 
 			// Skins.
 			if (root.ContainsKey("skins")) {
-					foreach (KeyValuePair<string, Object> skinMap in (Dictionary<string, Object>)root["skins"]) {
-					var skin = new Skin(skinMap.Key);
-					foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap.Value) {
-						int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
-						foreach (KeyValuePair<string, Object> entry in ((Dictionary<string, Object>)slotEntry.Value)) {
-							try {
-								Attachment attachment = ReadAttachment((Dictionary<string, Object>)entry.Value, skin, slotIndex, entry.Key, skeletonData);
-								if (attachment != null) skin.SetAttachment(slotIndex, entry.Key, attachment);
-							} catch (Exception e) {
-								throw new Exception("Error reading attachment: " + entry.Key + ", skin: " + skin, e);
-							}
-						} 
+				foreach (Dictionary<string, object> skinMap in (List<object>)root["skins"]) { 
+					Skin skin = new Skin((string)skinMap["name"]);
+					if (skinMap.ContainsKey("bones")) {
+						foreach (string entryName in (List<Object>)skinMap["bones"]) {
+							BoneData bone = skeletonData.FindBone(entryName);
+							if (bone == null) throw new Exception("Skin bone not found: " + entryName);
+							skin.bones.Add(bone);
+						}
+					}
+					if (skinMap.ContainsKey("ik")) {
+						foreach (string entryName in (List<Object>)skinMap["ik"]) {
+							IkConstraintData constraint = skeletonData.FindIkConstraint(entryName);
+							if (constraint == null) throw new Exception("Skin IK constraint not found: " + entryName);
+							skin.constraints.Add(constraint);
+						}
+					}
+					if (skinMap.ContainsKey("transform")) {
+						foreach (string entryName in (List<Object>)skinMap["transform"]) {
+							TransformConstraintData constraint = skeletonData.FindTransformConstraint(entryName);
+							if (constraint == null) throw new Exception("Skin transform constraint not found: " + entryName);
+							skin.constraints.Add(constraint);
+						}
+					}
+					if (skinMap.ContainsKey("path")) {
+						foreach (string entryName in (List<Object>)skinMap["path"]) {
+							PathConstraintData constraint = skeletonData.FindPathConstraint(entryName);
+							if (constraint == null) throw new Exception("Skin path constraint not found: " + entryName);
+							skin.constraints.Add(constraint);
+						}
+					}
+					if (skinMap.ContainsKey("attachments")) {
+						foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap["attachments"]) {
+							int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
+							foreach (KeyValuePair<string, Object> entry in ((Dictionary<string, Object>)slotEntry.Value)) {
+								try {
+									Attachment attachment = ReadAttachment((Dictionary<string, Object>)entry.Value, skin, slotIndex, entry.Key, skeletonData);
+									if (attachment != null) skin.SetAttachment(slotIndex, entry.Key, attachment);
+								} catch (Exception e) {
+									throw new Exception("Error reading attachment: " + entry.Key + ", skin: " + skin, e);
+								}
+							} 
+						}
 					}
 					skeletonData.skins.Add(skin);
 					if (skin.name == "default") skeletonData.defaultSkin = skin;
@@ -494,7 +536,7 @@ namespace Spine {
 
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								float time = (float)valueMap["time"];
+								float time = GetFloat(valueMap, "time", 0);
 								timeline.SetFrame(frameIndex++, time, (string)valueMap["name"]);
 							}
 							timelines.Add(timeline);
@@ -506,7 +548,7 @@ namespace Spine {
 
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								float time = (float)valueMap["time"];
+								float time = GetFloat(valueMap, "time", 0);
 								string c = (string)valueMap["color"];
 								timeline.SetFrame(frameIndex, time, ToColor(c, 0), ToColor(c, 1), ToColor(c, 2), ToColor(c, 3));
 								ReadCurve(valueMap, timeline, frameIndex);
@@ -521,7 +563,7 @@ namespace Spine {
 
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								float time = (float)valueMap["time"];
+								float time = GetFloat(valueMap, "time", 0);
 								string light = (string)valueMap["light"];
 								string dark = (string)valueMap["dark"];
 								timeline.SetFrame(frameIndex, time, ToColor(light, 0), ToColor(light, 1), ToColor(light, 2), ToColor(light, 3),
@@ -554,7 +596,7 @@ namespace Spine {
 
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, (float)valueMap["time"], (float)valueMap["angle"]);
+								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "angle", 0));
 								ReadCurve(valueMap, timeline, frameIndex);
 								frameIndex++;
 							}
@@ -563,9 +605,11 @@ namespace Spine {
 
 						} else if (timelineName == "translate" || timelineName == "scale" || timelineName == "shear") {
 							TranslateTimeline timeline;
-							float timelineScale = 1;
-							if (timelineName == "scale")
+							float timelineScale = 1, defaultValue = 0;
+							if (timelineName == "scale") {
 								timeline = new ScaleTimeline(values.Count);
+								defaultValue = 1;
+							}
 							else if (timelineName == "shear")
 								timeline = new ShearTimeline(values.Count);
 							else {
@@ -576,9 +620,9 @@ namespace Spine {
 
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								float time = (float)valueMap["time"];
-								float x = GetFloat(valueMap, "x", 0);
-								float y = GetFloat(valueMap, "y", 0);
+								float time = GetFloat(valueMap, "time", 0);
+								float x = GetFloat(valueMap, "x", defaultValue);
+								float y = GetFloat(valueMap, "y", defaultValue);
 								timeline.SetFrame(frameIndex, time, x * timelineScale, y * timelineScale);
 								ReadCurve(valueMap, timeline, frameIndex);
 								frameIndex++;
@@ -603,7 +647,7 @@ namespace Spine {
 					foreach (Dictionary<string, Object> valueMap in values) {
 						timeline.SetFrame(
 							frameIndex,
-							(float)valueMap["time"],
+							GetFloat(valueMap, "time", 0),
 							GetFloat(valueMap, "mix", 1),
 							GetBoolean(valueMap, "bendPositive", true) ? 1 : -1,
 							GetBoolean(valueMap, "compress", true),
@@ -626,7 +670,7 @@ namespace Spine {
 					timeline.transformConstraintIndex = skeletonData.transformConstraints.IndexOf(constraint);
 					int frameIndex = 0;
 					foreach (Dictionary<string, Object> valueMap in values) {
-						float time = (float)valueMap["time"];
+						float time = GetFloat(valueMap, "time", 0);
 						float rotateMix = GetFloat(valueMap, "rotateMix", 1);
 						float translateMix = GetFloat(valueMap, "translateMix", 1);
 						float scaleMix = GetFloat(valueMap, "scaleMix", 1);
@@ -641,8 +685,8 @@ namespace Spine {
 			}
 
 			// Path constraint timelines.
-			if (map.ContainsKey("paths")) {
-				foreach (KeyValuePair<string, Object> constraintMap in (Dictionary<string, Object>)map["paths"]) {
+			if (map.ContainsKey("path")) {
+				foreach (KeyValuePair<string, Object> constraintMap in (Dictionary<string, Object>)map["path"]) {
 					int index = skeletonData.FindPathConstraintIndex(constraintMap.Key);
 					if (index == -1) throw new Exception("Path constraint not found: " + constraintMap.Key);
 					PathConstraintData data = skeletonData.pathConstraints.Items[index];
@@ -664,7 +708,7 @@ namespace Spine {
 							timeline.pathConstraintIndex = index;
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, (float)valueMap["time"], GetFloat(valueMap, timelineName, 0) * timelineScale);
+								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, timelineName, 0) * timelineScale);
 								ReadCurve(valueMap, timeline, frameIndex);
 								frameIndex++;
 							}
@@ -676,7 +720,7 @@ namespace Spine {
 							timeline.pathConstraintIndex = index;
 							int frameIndex = 0;
 							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, (float)valueMap["time"], GetFloat(valueMap, "rotateMix", 1), GetFloat(valueMap, "translateMix", 1));
+								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "rotateMix", 1), GetFloat(valueMap, "translateMix", 1));
 								ReadCurve(valueMap, timeline, frameIndex);
 								frameIndex++;
 							}
@@ -727,7 +771,7 @@ namespace Spine {
 									}
 								}
 
-								timeline.SetFrame(frameIndex, (float)valueMap["time"], deform);
+								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), deform);
 								ReadCurve(valueMap, timeline, frameIndex);
 								frameIndex++;
 							}
@@ -770,7 +814,7 @@ namespace Spine {
 						for (int i = slotCount - 1; i >= 0; i--)
 							if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
 					}
-					timeline.SetFrame(frameIndex++, (float)drawOrderMap["time"], drawOrder);
+					timeline.SetFrame(frameIndex++, GetFloat(drawOrderMap, "time", 0), drawOrder);
 				}
 				timelines.Add(timeline);
 				duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
@@ -784,7 +828,7 @@ namespace Spine {
 				foreach (Dictionary<string, Object> eventMap in eventsMap) {
 					EventData eventData = skeletonData.FindEvent((string)eventMap["name"]);
 					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
-					var e = new Event((float)eventMap["time"], eventData) {
+					var e = new Event(GetFloat(eventMap, "time", 0), eventData) {
 						intValue = GetInt(eventMap, "int", eventData.Int),
 						floatValue = GetFloat(eventMap, "float", eventData.Float),
 						stringValue = GetString(eventMap, "string", eventData.String)
@@ -807,12 +851,12 @@ namespace Spine {
 			if (!valueMap.ContainsKey("curve"))
 				return;
 			Object curveObject = valueMap["curve"];
-			if (curveObject.Equals("stepped"))
+			if (curveObject is string)
 				timeline.SetStepped(frameIndex);
 			else {
 				var curve = curveObject as List<Object>;
 				if (curve != null)
-					timeline.SetCurve(frameIndex, (float)curve[0], (float)curve[1], (float)curve[2], (float)curve[3]);
+					timeline.SetCurve(frameIndex, (float)curveObject, GetFloat(valueMap, "c2", 0), GetFloat(valueMap, "c3", 1), GetFloat(valueMap, "c4", 1));
 			}
 		}
 

+ 25 - 3
spine-csharp/src/Skin.cs

@@ -40,9 +40,13 @@ namespace Spine {
 	public class Skin {
 		internal string name;
 		private OrderedDictionary attachments = new OrderedDictionary(SkinEntryComparer.Instance); // contains <SkinEntry, Attachment>
-		
+		internal readonly ExposedList<BoneData> bones = new ExposedList<BoneData>();
+		internal readonly ExposedList<ConstraintData> constraints = new ExposedList<ConstraintData>();
+
 		public string Name { get { return name; } }
 		public OrderedDictionary Attachments { get { return attachments; } }
+		public ExposedList<BoneData> Bones { get { return bones; } }
+		public ExposedList<ConstraintData> Constraints { get { return constraints; } }
 		
 		public Skin (string name) {
 			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
@@ -59,13 +63,23 @@ namespace Spine {
 
 		///<summary>Adds all attachments, bones, and constraints from the specified skin to this skin.</summary>
 		public void AddSkin (Skin skin) {
+			foreach (BoneData data in skin.bones)
+				if (!bones.Contains(data)) bones.Add(data);
+
+			foreach (ConstraintData data in skin.constraints)
+				if (!constraints.Contains(data)) constraints.Add(data);
+
 			foreach (SkinEntry entry in skin.attachments.Keys)
 				SetAttachment(entry.SlotIndex, entry.Name, entry.Attachment);
 		}
 
 		///<summary>Adds all attachments from the specified skin to this skin. Attachments are deep copied.</summary>
 		public void CopySkin (Skin skin) {
-			// note: bones and constraints are added in a separate commit.
+			foreach (BoneData data in skin.bones)
+				if (!bones.Contains(data)) bones.Add(data);
+
+			foreach (ConstraintData data in skin.constraints)
+				if (!constraints.Contains(data)) constraints.Add(data);
 
 			foreach (SkinEntry entry in skin.attachments.Keys) {
 				Attachment attachment = entry.Attachment.Copy();
@@ -98,13 +112,20 @@ namespace Spine {
 			return entries;
 		}
 
-		/// <summary>Returns all attachments for the given slot in this skin.</summary>
+		/// <summary>Returns all attachments in this skin for the specified slot index.</summary>
 		/// <param name="slotIndex">The target slotIndex. To find the slot index, use <see cref="Spine.Skeleton.FindSlotIndex"/> or <see cref="Spine.SkeletonData.FindSlotIndex"/>
 		public void GetAttachments (int slotIndex, List<SkinEntry> attachments) {
 			foreach (SkinEntry entry in this.attachments.Keys)
 				if (entry.SlotIndex == slotIndex) attachments.Add(entry);
 		}
 
+		///<summary>Clears all attachments, bones, and constraints.</summary>
+		public void Clear () {
+			attachments.Clear();
+			bones.Clear();
+			constraints.Clear();
+		}
+
 		override public string ToString () {
 			return name;
 		}
@@ -141,6 +162,7 @@ namespace Spine {
 				}
 			}
 
+			/// <summary>The name the attachment is associated with, equivalent to the skin placeholder name in the Spine editor.</summary>
 			public String Name {
 				get {
 					return name;

+ 5 - 1
spine-csharp/src/SlotData.cs

@@ -40,8 +40,11 @@ namespace Spine {
 		internal string attachmentName;
 		internal BlendMode blendMode;
 
+		/// <summary>The index of the slot in <see cref="Skeleton.Slots"/>.</summary>
 		public int Index { get { return index; } }
+		/// <summary>The name of the slot, which is unique across all slots in the skeleton.</summary>
 		public string Name { get { return name; } }
+		/// <summary>The bone this slot belongs to.</summary>
 		public BoneData BoneData { get { return boneData; } }
 		public float R { get { return r; } set { r = value; } }
 		public float G { get { return g; } set { g = value; } }
@@ -53,8 +56,9 @@ namespace Spine {
 		public float B2 { get { return b2; } set { b2 = value; } }
 		public bool HasSecondColor { get { return hasSecondColor; } set { hasSecondColor = value; } }
 
-		/// <summary>May be null.</summary>
+		/// <summary>The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible.</summary>
 		public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } }
+		/// <summary>The blend mode for drawing the slot's attachment.</summary>
 		public BlendMode BlendMode { get { return blendMode; } set { blendMode = value; } }
 
 		public SlotData (int index, String name, BoneData boneData) {

+ 1 - 2
spine-csharp/src/TransformConstraint.cs

@@ -37,7 +37,7 @@ namespace Spine {
 	/// <para>
 	/// See <a href="http://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide.</para>
 	/// </summary>
-	public class TransformConstraint : IConstraint {
+	public class TransformConstraint : IUpdatable {
 		internal TransformConstraintData data;
 		internal ExposedList<Bone> bones;
 		internal Bone target;
@@ -288,7 +288,6 @@ namespace Spine {
 			}
 		}
 
-		public int Order { get { return data.order; } }
 		/// <summary>The bones that will be modified by this transform constraint.</summary>
 		public ExposedList<Bone> Bones { get { return bones; } }
 		/// <summary>The target bone whose world transform will be copied to the constrained bones.</summary>

+ 2 - 12
spine-csharp/src/TransformConstraintData.cs

@@ -30,17 +30,13 @@
 using System;
 
 namespace Spine {
-	public class TransformConstraintData {
-		internal string name;
-		internal int order;
+	public class TransformConstraintData : ConstraintData {
 		internal ExposedList<BoneData> bones = new ExposedList<BoneData>();
 		internal BoneData target;
 		internal float rotateMix, translateMix, scaleMix, shearMix;
 		internal float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
 		internal bool relative, local;
 
-		public string Name { get { return name; } }
-		public int Order { get { return order; } set { order = value; } }
 		public ExposedList<BoneData> Bones { get { return bones; } }
 		public BoneData Target { get { return target; } set { target = value; } }
 		public float RotateMix { get { return rotateMix; } set { rotateMix = value; } }
@@ -58,13 +54,7 @@ namespace Spine {
 		public bool Relative { get { return relative; } set { relative = value; } }
 		public bool Local { get { return local; } set { local = value; } }
 
-		public TransformConstraintData (string name) {
-			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
-			this.name = name;
-		}
-
-		override public string ToString () {
-			return name;
+		public TransformConstraintData (string name) : base(name) {
 		}
 	}
 }

+ 10 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonRendererInspector.cs

@@ -504,8 +504,17 @@ namespace Spine.Unity.Editor {
 
 			if (!fieldMatchesSkin) {
 				Skin skinToSet = string.IsNullOrEmpty(componentSkinName) ? null : skeletonRenderer.Skeleton.Data.FindSkin(componentSkinName);
-				skeletonRenderer.Skeleton.Skin = skinToSet;
+				skeletonRenderer.Skeleton.SetSkin(skinToSet);
 				skeletonRenderer.Skeleton.SetSlotsToSetupPose();
+
+				// Note: the UpdateIfSkinMismatch concept shall be replaced with e.g. an OnValidate based
+				// solution or in a separate commit. The current solution does not repaint the Game view because
+				// it is first applying values and in the next editor pass is calling this skin-changing method.
+				if (skeletonRenderer is SkeletonAnimation)
+					((SkeletonAnimation) skeletonRenderer).Update(0f);
+				else if (skeletonRenderer is SkeletonMecanim)
+					((SkeletonMecanim) skeletonRenderer).Update();
+
 				skeletonRenderer.LateUpdate();
 				return true;
 			}

+ 4 - 3
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineEditorUtilities.cs

@@ -707,9 +707,10 @@ namespace Spine.Unity.Editor {
 				if (root == null || !root.ContainsKey("skins"))
 					return requiredPaths;
 
-				foreach (var skin in (Dictionary<string, object>)root["skins"]) {
-					foreach (var slot in (Dictionary<string, object>)skin.Value) {
-
+				foreach (Dictionary<string, object> skinMap in (List<object>)root["skins"]) {
+					if (!skinMap.ContainsKey("attachments"))
+						continue;
+					foreach (var slot in (Dictionary<string, object>)skinMap["attachments"]) {
 						foreach (var attachment in ((Dictionary<string, object>)slot.Value)) {
 							var data = ((Dictionary<string, object>)attachment.Value);