Browse Source

[csharp] Porting of major 4.0-beta runtime changes. Remaining Unity assets will be updated in separate commit. See #1796.

Harald Csaszar 4 years ago
parent
commit
d2529d410b
30 changed files with 1522 additions and 1457 deletions
  1. 449 337
      spine-csharp/src/Animation.cs
  2. 123 87
      spine-csharp/src/AnimationState.cs
  3. 1 1
      spine-csharp/src/Attachments/AtlasAttachmentLoader.cs
  4. 1 1
      spine-csharp/src/Attachments/VertexAttachment.cs
  5. 4 3
      spine-csharp/src/Bone.cs
  6. 18 26
      spine-csharp/src/IkConstraint.cs
  7. 1 6
      spine-csharp/src/PathConstraint.cs
  8. 99 108
      spine-csharp/src/Skeleton.cs
  9. 298 234
      spine-csharp/src/SkeletonBinary.cs
  10. 2 2
      spine-csharp/src/SkeletonBounds.cs
  11. 29 27
      spine-csharp/src/SkeletonData.cs
  12. 333 195
      spine-csharp/src/SkeletonJson.cs
  13. 92 0
      spine-csharp/src/SkeletonLoader.cs
  14. 9 3
      spine-csharp/src/Skin.cs
  15. 1 1
      spine-csharp/src/Slot.cs
  16. 13 27
      spine-csharp/src/TransformConstraint.cs
  17. 26 33
      spine-unity/Assets/Spine Examples/Scripts/MecanimAnimationMatchModifier/AnimationMatchModifierAsset.cs
  18. 0 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs
  19. 0 9
      spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes.meta
  20. 0 9
      spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor.meta
  21. 0 47
      spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor/SlotBlendModesEditor.cs
  22. 0 12
      spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor/SlotBlendModesEditor.cs.meta
  23. 2 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs
  24. 0 1
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
  25. 0 9
      spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated.meta
  26. 0 9
      spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes.meta
  27. 0 230
      spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes/SlotBlendModes.cs
  28. 0 16
      spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes/SlotBlendModes.cs.meta
  29. 20 20
      spine-unity/Assets/Spine/Runtime/spine-unity/Utility/TimelineExtensions.cs
  30. 1 1
      spine-unity/Assets/Spine/version.txt

File diff suppressed because it is too large
+ 449 - 337
spine-csharp/src/Animation.cs


+ 123 - 87
spine-csharp/src/AnimationState.cs

@@ -112,7 +112,7 @@ namespace Spine {
 
 
 		// end of difference
 		// end of difference
 		private readonly EventQueue queue; // Initialized by constructor.
 		private readonly EventQueue queue; // Initialized by constructor.
-		private readonly HashSet<int> propertyIDs = new HashSet<int>();
+		private readonly HashSet<string> propertyIds = new HashSet<string>();
 		private bool animationsChanged;
 		private bool animationsChanged;
 		private float timeScale = 1;
 		private float timeScale = 1;
 		private int unkeyedState;
 		private int unkeyedState;
@@ -244,7 +244,13 @@ namespace Spine {
 					mix = 0; // Set to setup pose the last time the entry will be applied.
 					mix = 0; // Set to setup pose the last time the entry will be applied.
 
 
 				// Apply current entry.
 				// Apply current entry.
-				float animationLast = current.animationLast, animationTime = current.AnimationTime;
+				float animationLast = current.animationLast, animationTime = current.AnimationTime, applyTime = animationTime;
+				ExposedList<Event> applyEvents = events;
+				if (current.reverse) {
+					applyTime = current.animation.duration - applyTime;
+					applyEvents = null;
+				}
+
 				int timelineCount = current.animation.timelines.Count;
 				int timelineCount = current.animation.timelines.Count;
 				var timelines = current.animation.timelines;
 				var timelines = current.animation.timelines;
 				var timelinesItems = timelines.Items;
 				var timelinesItems = timelines.Items;
@@ -252,9 +258,9 @@ namespace Spine {
 					for (int ii = 0; ii < timelineCount; ii++) {
 					for (int ii = 0; ii < timelineCount; ii++) {
 						var timeline = timelinesItems[ii];
 						var timeline = timelinesItems[ii];
 						if (timeline is AttachmentTimeline)
 						if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
 						else
 						else
-							timeline.Apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.In);
+							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.In);
 					}
 					}
 				} else {
 				} else {
 					var timelineMode = current.timelineMode.Items;
 					var timelineMode = current.timelineMode.Items;
@@ -268,12 +274,12 @@ namespace Spine {
 						MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup;
 						MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup;
 						var rotateTimeline = timeline as RotateTimeline;
 						var rotateTimeline = timeline as RotateTimeline;
 						if (rotateTimeline != null)
 						if (rotateTimeline != null)
-							ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation,
+							ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation,
 												ii << 1, firstFrame);
 												ii << 1, firstFrame);
 						else if (timeline is AttachmentTimeline)
 						else if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
 						else
 						else
-							timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.In);
+							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.In);
 					}
 					}
 				}
 				}
 				QueueEvents(current, animationTime);
 				QueueEvents(current, animationTime);
@@ -314,17 +320,23 @@ namespace Spine {
 				if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend.
 				if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend.
 			}
 			}
 
 
-			var eventBuffer = mix < from.eventThreshold ? this.events : null;
 			bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
 			bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
-			float animationLast = from.animationLast, animationTime = from.AnimationTime;
 			var timelines = from.animation.timelines;
 			var timelines = from.animation.timelines;
 			int timelineCount = timelines.Count;
 			int timelineCount = timelines.Count;
 			var timelinesItems = timelines.Items;
 			var timelinesItems = timelines.Items;
 			float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
 			float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
+			float animationLast = from.animationLast, animationTime = from.AnimationTime, applyTime = animationTime;
+			ExposedList<Event> events = null;
+			if (from.reverse)
+				applyTime = from.animation.duration - applyTime;
+			else {
+				if (mix < from.eventThreshold) events = this.events;
+			}
+
 
 
 			if (blend == MixBlend.Add) {
 			if (blend == MixBlend.Add) {
 				for (int i = 0; i < timelineCount; i++)
 				for (int i = 0; i < timelineCount; i++)
-					timelinesItems[i].Apply(skeleton, animationLast, animationTime, eventBuffer, alphaMix, blend, MixDirection.Out);
+					timelinesItems[i].Apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.Out);
 			} else {
 			} else {
 				var timelineMode = from.timelineMode.Items;
 				var timelineMode = from.timelineMode.Items;
 				var timelineHoldMix = from.timelineHoldMix.Items;
 				var timelineHoldMix = from.timelineHoldMix.Items;
@@ -366,14 +378,14 @@ namespace Spine {
 					from.totalAlpha += alpha;
 					from.totalAlpha += alpha;
 					var rotateTimeline = timeline as RotateTimeline;
 					var rotateTimeline = timeline as RotateTimeline;
 					if (rotateTimeline != null) {
 					if (rotateTimeline != null) {
-						ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation,
-											i << 1, firstFrame);
+						ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1,
+							firstFrame);
 					} else if (timeline is AttachmentTimeline) {
 					} else if (timeline is AttachmentTimeline) {
-						ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, timelineBlend, attachments);
+						ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments);
 					} else {
 					} else {
 						if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup)
 						if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup)
 							direction = MixDirection.In;
 							direction = MixDirection.In;
-						timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, timelineBlend, direction);
+						timeline.Apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
 					}
 					}
 				}
 				}
 			}
 			}
@@ -393,7 +405,7 @@ namespace Spine {
 		private void ApplyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend,
 		private void ApplyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend,
 			bool attachments) {
 			bool attachments) {
 
 
-			Slot slot = skeleton.slots.Items[timeline.slotIndex];
+			Slot slot = skeleton.slots.Items[timeline.SlotIndex];
 			if (!slot.bone.active) return;
 			if (!slot.bone.active) return;
 
 
 			float[] frames = timeline.frames;
 			float[] frames = timeline.frames;
@@ -402,12 +414,7 @@ namespace Spine {
 					SetAttachment(skeleton, slot, slot.data.attachmentName, attachments);
 					SetAttachment(skeleton, slot, slot.data.attachmentName, attachments);
 			}
 			}
 			else {
 			else {
-				int frameIndex;
-				if (time >= frames[frames.Length - 1]) // Time is after last frame.
-					frameIndex = frames.Length - 1;
-				else
-					frameIndex = Animation.BinarySearch(frames, time) - 1;
-				SetAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments);
+				SetAttachment(skeleton, slot, timeline.AttachmentNames[Animation.Search(frames, time)], attachments);
 			}
 			}
 
 
 			// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
 			// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
@@ -432,7 +439,7 @@ namespace Spine {
 				return;
 				return;
 			}
 			}
 
 
-			Bone bone = skeleton.bones.Items[timeline.boneIndex];
+			Bone bone = skeleton.bones.Items[timeline.BoneIndex];
 			if (!bone.active) return;
 			if (!bone.active) return;
 
 
 			float[] frames = timeline.frames;
 			float[] frames = timeline.frames;
@@ -441,7 +448,7 @@ namespace Spine {
 				switch (blend) {
 				switch (blend) {
 					case MixBlend.Setup:
 					case MixBlend.Setup:
 						bone.rotation = bone.data.rotation;
 						bone.rotation = bone.data.rotation;
-						return;
+						goto default; // Fall through.
 					default:
 					default:
 						return;
 						return;
 					case MixBlend.First:
 					case MixBlend.First:
@@ -451,21 +458,7 @@ namespace Spine {
 				}
 				}
 			} else {
 			} else {
 				r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation;
 				r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation;
-				if (time >= frames[frames.Length - RotateTimeline.ENTRIES]) // Time is after last frame.
-					r2 = bone.data.rotation + frames[frames.Length + RotateTimeline.PREV_ROTATION];
-				else {
-					// Interpolate between the previous frame and the current frame.
-					int frame = Animation.BinarySearch(frames, time, RotateTimeline.ENTRIES);
-					float prevRotation = frames[frame + RotateTimeline.PREV_ROTATION];
-					float frameTime = frames[frame];
-					float percent = timeline.GetCurvePercent((frame >> 1) - 1,
-						1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime));
-
-					r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation;
-					r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
-					r2 = prevRotation + r2 * percent + bone.data.rotation;
-					r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
-				}
+				r2 = bone.data.rotation + timeline.GetCurveValue(time);
 			}
 			}
 
 
 			// Mix between rotations using the direction of the shortest route on the first frame.
 			// Mix between rotations using the direction of the shortest route on the first frame.
@@ -494,8 +487,7 @@ namespace Spine {
 				timelinesRotation[i] = total;
 				timelinesRotation[i] = total;
 			}
 			}
 			timelinesRotation[i + 1] = diff;
 			timelinesRotation[i + 1] = diff;
-			r1 += total * alpha;
-			bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360;
+			bone.rotation = r1 + total * alpha;
 		}
 		}
 
 
 		private void QueueEvents (TrackEntry entry, float animationTime) {
 		private void QueueEvents (TrackEntry entry, float animationTime) {
@@ -577,10 +569,17 @@ namespace Spine {
 			queue.Drain();
 			queue.Drain();
 		}
 		}
 
 
+		/// <summary>
+		/// Removes the <see cref="TrackEntry.Next">next entry</see> and all entries after it for the specified entry.</summary>
+		public void ClearNext (TrackEntry entry) {
+			DisposeNext(entry.next);
+		}
+
 		/// <summary>Sets the active TrackEntry for a given track number.</summary>
 		/// <summary>Sets the active TrackEntry for a given track number.</summary>
 		private void SetCurrent (int index, TrackEntry current, bool interrupt) {
 		private void SetCurrent (int index, TrackEntry current, bool interrupt) {
 			TrackEntry from = ExpandToIndex(index);
 			TrackEntry from = ExpandToIndex(index);
 			tracks.Items[index] = current;
 			tracks.Items[index] = current;
+			current.previous = null;
 
 
 			if (from != null) {
 			if (from != null) {
 				if (interrupt) queue.Interrupt(from);
 				if (interrupt) queue.Interrupt(from);
@@ -647,7 +646,7 @@ namespace Spine {
 		/// equivalent to calling <see cref="SetAnimation(int, Animation, bool)"/>.</summary>
 		/// equivalent to calling <see cref="SetAnimation(int, Animation, bool)"/>.</summary>
 		/// <param name="delay">
 		/// <param name="delay">
 		/// If &gt; 0, sets <see cref="TrackEntry.Delay"/>. If &lt;= 0, the delay set is the duration of the previous track entry
 		/// If &gt; 0, sets <see cref="TrackEntry.Delay"/>. If &lt;= 0, the delay set is the duration of the previous track entry
-		/// minus any mix duration (from the {@link AnimationStateData}) plus the specified <code>Delay</code> (ie the mix
+		/// minus any mix duration (from the <see cref="AnimationStateData"/> plus the specified <code>Delay</code> (ie the mix
 		/// ends at (<code>Delay</code> = 0) or before (<code>Delay</code> &lt; 0) the previous track entry duration). If the
 		/// ends at (<code>Delay</code> = 0) or before (<code>Delay</code> &lt; 0) the previous track entry duration). If the
 		/// previous entry is looping, its next loop completion is used instead of its duration.
 		/// previous entry is looping, its next loop completion is used instead of its duration.
 		/// </param>
 		/// </param>
@@ -669,18 +668,8 @@ namespace Spine {
 				queue.Drain();
 				queue.Drain();
 			} else {
 			} else {
 				last.next = entry;
 				last.next = entry;
-				if (delay <= 0) {
-					float duration = last.animationEnd - last.animationStart;
-					if (duration != 0) {
-						if (last.loop) {
-							delay += duration * (1 + (int)(last.trackTime / duration)); // Completion of next loop.
-						} else {
-							delay += Math.Max(duration, last.trackTime); // After duration, else next update.
-						}
-						delay -= data.GetMix(last.animation, animation);
-					} else
-						delay = last.trackTime; // Next update.
-				}
+				entry.previous = last;
+				if (delay <= 0) delay += last.TrackComplete - entry.mixDuration;
 			}
 			}
 
 
 			entry.delay = delay;
 			entry.delay = delay;
@@ -698,11 +687,11 @@ namespace Spine {
 		/// 0 still mixes out over one frame.</para>
 		/// 0 still mixes out over one frame.</para>
 		/// <para>
 		/// <para>
 		/// Mixing in is done by first setting an empty animation, then adding an animation using
 		/// Mixing in is done by first setting an empty animation, then adding an animation using
-		/// <see cref="AnimationState.AddAnimation(int, Animation, boolean, float)"/> and on the returned track entry, set the
-		/// <see cref="TrackEntry.SetMixDuration(float)"/>. Mixing from an empty animation causes the new animation to be applied more and
-		/// more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the
-		/// setup pose value if no lower tracks key the property to the value keyed in the new animation.</para>
-		/// </summary>
+		/// <see cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with the desired delay (an empty animation has a duration of 0) and on
+		/// the returned track entry, set the <see cref="TrackEntry.SetMixDuration(float)"/>. Mixing from an empty animation causes the new
+		/// animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value
+		/// from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new
+		/// animation.</para></summary>
 		public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) {
 		public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) {
 			TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false);
 			TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false);
 			entry.mixDuration = mixDuration;
 			entry.mixDuration = mixDuration;
@@ -725,10 +714,10 @@ namespace Spine {
 		/// after the <see cref="AnimationState.Dispose"/> event occurs.
 		/// after the <see cref="AnimationState.Dispose"/> event occurs.
 		/// </returns>
 		/// </returns>
 		public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) {
 		public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) {
-			if (delay <= 0) delay -= mixDuration;
-			TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay);
+			TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay <= 0 ? 1 : delay);
 			entry.mixDuration = mixDuration;
 			entry.mixDuration = mixDuration;
 			entry.trackEnd = mixDuration;
 			entry.trackEnd = mixDuration;
+			if (delay <= 0 && entry.previous != null) entry.delay = entry.previous.TrackComplete - entry.mixDuration;
 			return entry;
 			return entry;
 		}
 		}
 
 
@@ -738,8 +727,9 @@ namespace Spine {
 		public void SetEmptyAnimations (float mixDuration) {
 		public void SetEmptyAnimations (float mixDuration) {
 			bool oldDrainDisabled = queue.drainDisabled;
 			bool oldDrainDisabled = queue.drainDisabled;
 			queue.drainDisabled = true;
 			queue.drainDisabled = true;
+			var tracksItems = tracks.Items;
 			for (int i = 0, n = tracks.Count; i < n; i++) {
 			for (int i = 0, n = tracks.Count; i < n; i++) {
-				TrackEntry current = tracks.Items[i];
+				TrackEntry current = tracksItems[i];
 				if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration);
 				if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration);
 			}
 			}
 			queue.drainDisabled = oldDrainDisabled;
 			queue.drainDisabled = oldDrainDisabled;
@@ -798,10 +788,10 @@ namespace Spine {
 			animationsChanged = false;
 			animationsChanged = false;
 
 
 			// Process in the order that animations are applied.
 			// Process in the order that animations are applied.
-			propertyIDs.Clear();
-
+			propertyIds.Clear();
+			int n = tracks.Count;
 			var tracksItems = tracks.Items;
 			var tracksItems = tracks.Items;
-			for (int i = 0, n = tracks.Count; i < n; i++) {
+			for (int i = 0; i < n; i++) {
 				TrackEntry entry = tracksItems[i];
 				TrackEntry entry = tracksItems[i];
 				if (entry == null) continue;
 				if (entry == null) continue;
 				while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
 				while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
@@ -814,6 +804,8 @@ namespace Spine {
 			}
 			}
 		}
 		}
 
 
+
+
 		private void ComputeHold (TrackEntry entry) {
 		private void ComputeHold (TrackEntry entry) {
 			TrackEntry to = entry.mixingTo;
 			TrackEntry to = entry.mixingTo;
 			var timelines = entry.animation.timelines.Items;
 			var timelines = entry.animation.timelines.Items;
@@ -821,11 +813,11 @@ namespace Spine {
 			var timelineMode = entry.timelineMode.Resize(timelinesCount).Items; //timelineMode.setSize(timelinesCount);
 			var timelineMode = entry.timelineMode.Resize(timelinesCount).Items; //timelineMode.setSize(timelinesCount);
 			entry.timelineHoldMix.Clear();
 			entry.timelineHoldMix.Clear();
 			var timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; //timelineHoldMix.setSize(timelinesCount);
 			var timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; //timelineHoldMix.setSize(timelinesCount);
-			var propertyIDs = this.propertyIDs;
+			var propertyIds = this.propertyIds;
 
 
 			if (to != null && to.holdPrevious) {
 			if (to != null && to.holdPrevious) {
 				for (int i = 0; i < timelinesCount; i++)
 				for (int i = 0; i < timelinesCount; i++)
-					timelineMode[i] = propertyIDs.Add(timelines[i].PropertyId) ? AnimationState.HoldFirst : AnimationState.HoldSubsequent;
+					timelineMode[i] = propertyIds.AddAll(timelines[i].PropertyIds) ? AnimationState.HoldFirst : AnimationState.HoldSubsequent;
 
 
 				return;
 				return;
 			}
 			}
@@ -833,15 +825,15 @@ namespace Spine {
 			// outer:
 			// outer:
 			for (int i = 0; i < timelinesCount; i++) {
 			for (int i = 0; i < timelinesCount; i++) {
 				Timeline timeline = timelines[i];
 				Timeline timeline = timelines[i];
-				int id = timeline.PropertyId;
-				if (!propertyIDs.Add(id))
+				String[] ids = timeline.PropertyIds;
+				if (!propertyIds.AddAll(ids))
 					timelineMode[i] = AnimationState.Subsequent;
 					timelineMode[i] = AnimationState.Subsequent;
 				else if (to == null || timeline is AttachmentTimeline || timeline is DrawOrderTimeline
 				else if (to == null || timeline is AttachmentTimeline || timeline is DrawOrderTimeline
-						|| timeline is EventTimeline || !to.animation.HasTimeline(id)) {
+						|| timeline is EventTimeline || !to.animation.HasTimeline(ids)) {
 					timelineMode[i] = AnimationState.First;
 					timelineMode[i] = AnimationState.First;
 				} else {
 				} else {
 					for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
 					for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
-						if (next.animation.HasTimeline(id)) continue;
+						if (next.animation.HasTimeline(ids)) continue;
 						if (next.mixDuration > 0) {
 						if (next.mixDuration > 0) {
 							timelineMode[i] = AnimationState.HoldMix;
 							timelineMode[i] = AnimationState.HoldMix;
 							timelineHoldMix[i] = next;
 							timelineHoldMix[i] = next;
@@ -892,8 +884,9 @@ namespace Spine {
 
 
 		override public string ToString () {
 		override public string ToString () {
 			var buffer = new System.Text.StringBuilder();
 			var buffer = new System.Text.StringBuilder();
+			var tracksItems = tracks.Items;
 			for (int i = 0, n = tracks.Count; i < n; i++) {
 			for (int i = 0, n = tracks.Count; i < n; i++) {
-				TrackEntry entry = tracks.Items[i];
+				TrackEntry entry = tracksItems[i];
 				if (entry == null) continue;
 				if (entry == null) continue;
 				if (buffer.Length > 0) buffer.Append(", ");
 				if (buffer.Length > 0) buffer.Append(", ");
 				buffer.Append(entry.ToString());
 				buffer.Append(entry.ToString());
@@ -912,7 +905,7 @@ namespace Spine {
 	public class TrackEntry : Pool<TrackEntry>.IPoolable {
 	public class TrackEntry : Pool<TrackEntry>.IPoolable {
 		internal Animation animation;
 		internal Animation animation;
 
 
-		internal TrackEntry next, mixingFrom, mixingTo;
+		internal TrackEntry previous, next, mixingFrom, mixingTo;
 		// difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'.
 		// difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'.
 		public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
 		public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
 		public event AnimationState.TrackEntryEventDelegate Event;
 		public event AnimationState.TrackEntryEventDelegate Event;
@@ -925,7 +918,7 @@ namespace Spine {
 
 
 		internal int trackIndex;
 		internal int trackIndex;
 
 
-		internal bool loop, holdPrevious;
+		internal bool loop, holdPrevious, reverse;
 		internal float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		internal float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		internal float animationStart, animationEnd, animationLast, nextAnimationLast;
 		internal float animationStart, animationEnd, animationLast, nextAnimationLast;
 		internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f;
 		internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f;
@@ -937,6 +930,7 @@ namespace Spine {
 
 
 		// IPoolable.Reset()
 		// IPoolable.Reset()
 		public void Reset () {
 		public void Reset () {
+			previous = null;
 			next = null;
 			next = null;
 			mixingFrom = null;
 			mixingFrom = null;
 			mixingTo = null;
 			mixingTo = null;
@@ -973,7 +967,10 @@ namespace Spine {
 		/// track entry <see cref="TrackEntry.TrackTime"/> &gt;= this track entry's <code>Delay</code>).</para>
 		/// track entry <see cref="TrackEntry.TrackTime"/> &gt;= this track entry's <code>Delay</code>).</para>
 		/// <para>
 		/// <para>
 		/// <see cref="TrackEntry.TimeScale"/> affects the delay.</para>
 		/// <see cref="TrackEntry.TimeScale"/> affects the delay.</para>
-		/// </summary>
+		/// <para>
+		/// When using <see cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with a <code>delay</code> <= 0, the delay
+		/// is set using the mix duration from the <see cref="AnimationStateData"/>. If <see cref="mixDuration"/> is set afterward, the delay
+		/// may need to be adjusted.</summary>
 		public float Delay { get { return delay; } set { delay = value; } }
 		public float Delay { get { return delay; } set { delay = value; } }
 
 
 		/// <summary>
 		/// <summary>
@@ -994,6 +991,21 @@ namespace Spine {
 		/// </summary>
 		/// </summary>
 		public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } }
 		public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } }
 
 
+		/// <summary>
+		/// If this track entry is non-looping, the track time in seconds when <see cref="AnimationEnd"/> is reached, or the current
+		/// <see cref="TrackTime"/> if it has already been reached. If this track entry is looping, the track time when this
+		/// animation will reach its next <see cref="AnimationEnd"/> (the next loop completion).</summary>
+		public float TrackComplete {
+			get {
+				float duration = animationEnd - animationStart;
+				if (duration != 0) {
+					if (loop) return duration * (1 + (int)(trackTime / duration)); // Completion of next loop.
+					if (trackTime < duration) return duration; // Before duration.
+				}
+				return trackTime; // Next update.
+			}
+		}
+
 		/// <summary>
 		/// <summary>
 		/// <para>
 		/// <para>
 		/// Seconds when this animation starts, both initially and after looping. Defaults to 0.</para>
 		/// Seconds when this animation starts, both initially and after looping. Defaults to 0.</para>
@@ -1043,11 +1055,13 @@ namespace Spine {
 		/// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or
 		/// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or
 		/// faster. Defaults to 1.</para>
 		/// faster. Defaults to 1.</para>
 		/// <para>
 		/// <para>
+		/// Values < 0 are not supported. To play an animation in reverse, use <see cref="Reverse"/>.
+		/// <para>
 		/// <see cref="TrackEntry.MixTime"/> is not affected by track entry time scale, so <see cref="TrackEntry.MixDuration"/> may need to be adjusted to
 		/// <see cref="TrackEntry.MixTime"/> is not affected by track entry time scale, so <see cref="TrackEntry.MixDuration"/> may need to be adjusted to
 		/// match the animation speed.</para>
 		/// match the animation speed.</para>
 		/// <para>
 		/// <para>
-		/// When using <see cref="AnimationState.AddAnimation(int, Animation, boolean, float)"> with a <code>Delay</code> <= 0, note the
-		/// {<see cref="TrackEntry.Delay"/> is set using the mix duration from the <see cref="AnimationStateData"/>, assuming time scale to be 1. If
+		/// When using <see cref="AnimationState.AddAnimation(int, Animation, bool, float)"> with a <code>Delay</code> <= 0, the
+		/// <see cref="TrackEntry.Delay"/> is set using the mix duration from the <see cref="AnimationStateData"/>, assuming time scale to be 1. If
 		/// the time scale is not 1, the delay may need to be adjusted.</para>
 		/// the time scale is not 1, the delay may need to be adjusted.</para>
 		/// <para>
 		/// <para>
 		/// See AnimationState <see cref="AnimationState.TimeScale"/> for affecting all animations.</para>
 		/// See AnimationState <see cref="AnimationState.TimeScale"/> for affecting all animations.</para>
@@ -1086,9 +1100,16 @@ namespace Spine {
 		public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } }
 		public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } }
 
 
 		/// <summary>
 		/// <summary>
-		/// The animation queued to start after this animation, or null. <code>Next</code> makes up a linked list. </summary>
+		/// The animation queued to start after this animation, or null if there is none. <code>next</code> makes up a doubly linked
+		/// list.
+		/// <para>
+		/// See <see cref="AnimationState.ClearNext(TrackEntry)"/> to truncate the list.</para></summary>
 		public TrackEntry Next { get { return next; } }
 		public TrackEntry Next { get { return next; } }
 
 
+		/// <summary>
+		/// The animation queued to play before this animation, or null. <code>previous</code> makes up a doubly linked list.</summary>
+		public TrackEntry Previous { get { return previous; } }
+
 		/// <summary>
 		/// <summary>
 		/// Returns true if at least one loop has been completed.</summary>
 		/// Returns true if at least one loop has been completed.</summary>
 		/// <seealso cref="TrackEntry.Complete"/>
 		/// <seealso cref="TrackEntry.Complete"/>
@@ -1108,20 +1129,21 @@ namespace Spine {
 		/// <para>
 		/// <para>
 		/// The <code>MixDuration</code> can be set manually rather than use the value from
 		/// The <code>MixDuration</code> can be set manually rather than use the value from
 		/// <see cref="AnimationStateData.GetMix(Animation, Animation)"/>. In that case, the <code>MixDuration</code> can be set for a new
 		/// <see cref="AnimationStateData.GetMix(Animation, Animation)"/>. In that case, the <code>MixDuration</code> can be set for a new
-		///  track entry only before <see cref="AnimationState.Update(float)"/> is first called.</para>
-		///  <para>
-		///  When using <seealso cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with a <code>Delay</code> &lt;= 0, note the
-		///  <see cref="TrackEntry.Delay"/> is set using the mix duration from the <see cref=" AnimationStateData"/>, not a mix duration set
-		///  afterward.</para>
-		/// </summary>
+		/// track entry only before <see cref="AnimationState.Update(float)"/> is first called.</para>
+		/// <para>
+		/// When using <seealso cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with a <code>Delay</code> &lt;= 0, the
+		/// <see cref="TrackEntry.Delay"/> is set using the mix duration from the <see cref=" AnimationStateData"/>. If <code>mixDuration</code> is set
+		/// afterward, the delay may need to be adjusted. For example:
+		/// <code>entry.Delay = entry.previous.TrackComplete - entry.MixDuration;</code>
+		/// </para></summary>
 		public float MixDuration { get { return mixDuration; } set { mixDuration = value; } }
 		public float MixDuration { get { return mixDuration; } set { mixDuration = value; } }
 
 
 		/// <summary>
 		/// <summary>
 		/// <para>
 		/// <para>
-		/// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to <see cref="MixBlend.Replace"/>, which
-		/// replaces the values from the lower tracks with the animation values. <see cref="MixBlend.Add"/> adds the animation values to
-		/// the values from the lower tracks.</para>
-		/// <para>
+		/// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to <see cref="MixBlend.Replace"/>.
+		/// </para><para>
+		/// Track entries on track 0 ignore this setting and always use <see cref="MixBlend.First"/>.
+		/// </para><para>
 		///  The <code>MixBlend</code> can be set for a new track entry only before <see cref="AnimationState.Apply(Skeleton)"/> is first
 		///  The <code>MixBlend</code> can be set for a new track entry only before <see cref="AnimationState.Apply(Skeleton)"/> is first
 		///  called.</para>
 		///  called.</para>
 		/// </summary>
 		/// </summary>
@@ -1153,6 +1175,10 @@ namespace Spine {
 		/// </summary>
 		/// </summary>
 		public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } }
 		public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } }
 
 
+		/// <summary>
+		/// If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse.</summary>
+		public bool Reverse { get { return reverse; } set { reverse = value; } }
+
 		/// <summary>
 		/// <summary>
 		/// <para>
 		/// <para>
 		/// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
 		/// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
@@ -1331,4 +1357,14 @@ namespace Spine {
 		}
 		}
 	}
 	}
 
 
+	public static class HashSetExtensions {
+		public static bool AddAll<T> (this HashSet<T> set, T[] addSet) {
+			bool anyItemAdded = false;
+			for (int i = 0, n = addSet.Length; i < n; ++i) {
+				var item = addSet[i];
+				anyItemAdded |= set.Add(item);
+			}
+			return anyItemAdded;
+		}
+	}
 }
 }

+ 1 - 1
spine-csharp/src/Attachments/AtlasAttachmentLoader.cs

@@ -39,7 +39,7 @@ namespace Spine {
 		private Atlas[] atlasArray;
 		private Atlas[] atlasArray;
 
 
 		public AtlasAttachmentLoader (params Atlas[] atlasArray) {
 		public AtlasAttachmentLoader (params Atlas[] atlasArray) {
-			if (atlasArray == null) throw new ArgumentNullException("atlas array cannot be null.");
+			if (atlasArray == null) throw new ArgumentNullException("atlas", "atlas array cannot be null.");
 			this.atlasArray = atlasArray;
 			this.atlasArray = atlasArray;
 		}
 		}
 
 

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

@@ -56,7 +56,7 @@ namespace Spine {
 
 
 			deformAttachment = this;
 			deformAttachment = this;
 			lock (VertexAttachment.nextIdLock) {
 			lock (VertexAttachment.nextIdLock) {
-				id = (VertexAttachment.nextID++ & 65535) << 11;
+				id = VertexAttachment.nextID++;
 			}
 			}
 		}
 		}
 
 

+ 4 - 3
spine-csharp/src/Bone.cs

@@ -116,6 +116,7 @@ namespace Spine {
 		/// <summary>Returns the magnitide (always positive) of the world scale Y.</summary>
 		/// <summary>Returns the magnitide (always positive) of the world scale Y.</summary>
 		public float WorldScaleY { get { return (float)Math.Sqrt(b * b + d * d); } }
 		public float WorldScaleY { get { return (float)Math.Sqrt(b * b + d * d); } }
 
 
+		/// <summary>Copy constructor. Does not copy the <see cref="Children"/> bones.</summary>
 		/// <param name="parent">May be null.</param>
 		/// <param name="parent">May be null.</param>
 		public Bone (BoneData data, Skeleton skeleton, Bone parent) {
 		public Bone (BoneData data, Skeleton skeleton, Bone parent) {
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
@@ -305,10 +306,10 @@ namespace Spine {
 
 
 		public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) {
 		public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) {
 			float a = this.a, b = this.b, c = this.c, d = this.d;
 			float a = this.a, b = this.b, c = this.c, d = this.d;
-			float invDet = 1 / (a * d - b * c);
+			float det = a * d - b * c;
 			float x = worldX - this.worldX, y = worldY - this.worldY;
 			float x = worldX - this.worldX, y = worldY - this.worldY;
-			localX = (x * d * invDet - y * b * invDet);
-			localY = (y * a * invDet - x * c * invDet);
+			localX = (x * d - y * b) / det;
+			localY = (y * a - x * c) / det;
 		}
 		}
 
 
 		public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) {
 		public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) {

+ 18 - 26
spine-csharp/src/IkConstraint.cs

@@ -79,20 +79,16 @@ namespace Spine {
 			stretch = constraint.stretch;
 			stretch = constraint.stretch;
 		}
 		}
 
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
 		public void Update () {
+			if (mix == 0) return;
 			Bone target = this.target;
 			Bone target = this.target;
-			ExposedList<Bone> bones = this.bones;
-			switch (bones.Count) {
+			var bones = this.bones.Items;
+			switch (this.bones.Count) {
 			case 1:
 			case 1:
-				Apply(bones.Items[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
+				Apply(bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
 				break;
 				break;
 			case 2:
 			case 2:
-				Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, stretch, softness, mix);
+				Apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, stretch, softness, mix);
 				break;
 				break;
 			}
 			}
 		}
 		}
@@ -157,6 +153,7 @@ namespace Spine {
 		/// <summary>Applies 1 bone IK. The target is specified in the world coordinate system.</summary>
 		/// <summary>Applies 1 bone IK. The target is specified in the world coordinate system.</summary>
 		static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform,
 		static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform,
 								float alpha) {
 								float alpha) {
+			if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null.");
 			if (!bone.appliedValid) bone.UpdateAppliedTransform();
 			if (!bone.appliedValid) bone.UpdateAppliedTransform();
 			Bone p = bone.parent;
 			Bone p = bone.parent;
 
 
@@ -175,16 +172,16 @@ namespace Spine {
 					float sc = pc / bone.skeleton.ScaleY;
 					float sc = pc / bone.skeleton.ScaleY;
 					pb = -sc * s * bone.skeleton.ScaleX;
 					pb = -sc * s * bone.skeleton.ScaleX;
 					pd = sa * s * bone.skeleton.ScaleY;
 					pd = sa * s * bone.skeleton.ScaleY;
-					rotationIK += (float)Math.Atan2(pc, pa) * MathUtils.RadDeg;
+					rotationIK += (float)Math.Atan2(sc, sa) * MathUtils.RadDeg;
 					goto default; // Fall through.
 					goto default; // Fall through.
-                }
-                default: {
+				}
+				default: {
 					float x = targetX - p.worldX, y = targetY - p.worldY;
 					float x = targetX - p.worldX, y = targetY - p.worldY;
-                    float d = pa * pd - pb * pc;
-                    tx = (x * pd - y * pb) / d - bone.ax;
-                    ty = (y * pa - x * pc) / d - bone.ay;
-                    break;
-                }
+					float d = pa * pd - pb * pc;
+					tx = (x * pd - y * pb) / d - bone.ax;
+					ty = (y * pa - x * pc) / d - bone.ay;
+					break;
+				}
 			}
 			}
 
 
 			rotationIK += (float)Math.Atan2(ty, tx) * MathUtils.RadDeg;
 			rotationIK += (float)Math.Atan2(ty, tx) * MathUtils.RadDeg;
@@ -198,13 +195,10 @@ namespace Spine {
 			if (compress || stretch) {
 			if (compress || stretch) {
 				switch (bone.data.transformMode) {
 				switch (bone.data.transformMode) {
 					case TransformMode.NoScale:
 					case TransformMode.NoScale:
-                        tx = targetX - bone.worldX;
-                        ty = targetY - bone.worldY;
-                        break;
-                    case TransformMode.NoScaleOrReflection:
+					case TransformMode.NoScaleOrReflection:
 						tx = targetX - bone.worldX;
 						tx = targetX - bone.worldX;
 						ty = targetY - bone.worldY;
 						ty = targetY - bone.worldY;
-                        break;
+						break;
 				}
 				}
 				float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
 				float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
 				if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
 				if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
@@ -220,10 +214,8 @@ namespace Spine {
 		/// <param name="child">A direct descendant of the parent bone.</param>
 		/// <param name="child">A direct descendant of the parent bone.</param>
 		static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float softness,
 		static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float softness,
 			float alpha) {
 			float alpha) {
-			if (alpha == 0) {
-				child.UpdateWorldTransform();
-				return;
-			}
+			if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null.");
+			if (child == null) throw new ArgumentNullException("child", "child cannot be null.");
 			if (!parent.appliedValid) parent.UpdateAppliedTransform();
 			if (!parent.appliedValid) parent.UpdateAppliedTransform();
 			if (!child.appliedValid) child.UpdateAppliedTransform();
 			if (!child.appliedValid) child.UpdateAppliedTransform();
 			float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX;
 			float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX;

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

@@ -34,7 +34,7 @@ namespace Spine {
 	/// <summary>
 	/// <summary>
 	/// <para>
 	/// <para>
 	/// Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
 	/// Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
-	/// constrained bones so they follow a {@link PathAttachment}.</para>
+	/// constrained bones so they follow a <see cref="PathAttachment"/>.</para>
 	/// <para>
 	/// <para>
 	/// See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide.</para>
 	/// See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide.</para>
 	/// </summary>
 	/// </summary>
@@ -82,11 +82,6 @@ namespace Spine {
 			translateMix = constraint.translateMix;
 			translateMix = constraint.translateMix;
 		}
 		}
 
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
 		public void Update () {
 			PathAttachment attachment = target.Attachment as PathAttachment;
 			PathAttachment attachment = target.Attachment as PathAttachment;
 			if (attachment == null) return;
 			if (attachment == null) return;

+ 99 - 108
spine-csharp/src/Skeleton.cs

@@ -40,7 +40,6 @@ namespace Spine {
 		internal ExposedList<TransformConstraint> transformConstraints;
 		internal ExposedList<TransformConstraint> transformConstraints;
 		internal ExposedList<PathConstraint> pathConstraints;
 		internal ExposedList<PathConstraint> pathConstraints;
 		internal ExposedList<IUpdatable> updateCache = new ExposedList<IUpdatable>();
 		internal ExposedList<IUpdatable> updateCache = new ExposedList<IUpdatable>();
-		internal ExposedList<Bone> updateCacheReset = new ExposedList<Bone>();
 		internal Skin skin;
 		internal Skin skin;
 		internal float r = 1, g = 1, b = 1, a = 1;
 		internal float r = 1, g = 1, b = 1, a = 1;
 		internal float time;
 		internal float time;
@@ -55,7 +54,13 @@ namespace Spine {
 		public ExposedList<IkConstraint> IkConstraints { get { return ikConstraints; } }
 		public ExposedList<IkConstraint> IkConstraints { get { return ikConstraints; } }
 		public ExposedList<PathConstraint> PathConstraints { get { return pathConstraints; } }
 		public ExposedList<PathConstraint> PathConstraints { get { return pathConstraints; } }
 		public ExposedList<TransformConstraint> TransformConstraints { get { return transformConstraints; } }
 		public ExposedList<TransformConstraint> TransformConstraints { get { return transformConstraints; } }
-		public Skin Skin { get { return skin; } set { SetSkin(value); } }
+
+		public Skin Skin {
+			/// <summary>The skeleton's current skin. May be null.</summary>
+			get { return skin; }
+			/// <summary>Sets a skin, <see cref="SetSkin(Skin)"/>.</summary>
+			set { SetSkin(value); }
+		}
 		public float R { get { return r; } set { r = value; } }
 		public float R { get { return r; } set { r = value; } }
 		public float G { get { return g; } set { g = value; } }
 		public float G { get { return g; } set { g = value; } }
 		public float B { get { return b; } set { b = value; } }
 		public float B { get { return b; } set { b = value; } }
@@ -72,6 +77,7 @@ namespace Spine {
 		[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
 		[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
 		public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
 		public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
 
 
+		/// <summary>Returns the root bone, or null if the skeleton has no bones.</summary>
 		public Bone RootBone {
 		public Bone RootBone {
 			get { return bones.Count == 0 ? null : bones.Items[0]; }
 			get { return bones.Count == 0 ? null : bones.Items[0]; }
 		}
 		}
@@ -81,22 +87,23 @@ namespace Spine {
 			this.data = data;
 			this.data = data;
 
 
 			bones = new ExposedList<Bone>(data.bones.Count);
 			bones = new ExposedList<Bone>(data.bones.Count);
+			var bonesItems = this.bones.Items;
 			foreach (BoneData boneData in data.bones) {
 			foreach (BoneData boneData in data.bones) {
 				Bone bone;
 				Bone bone;
 				if (boneData.parent == null) {
 				if (boneData.parent == null) {
 					bone = new Bone(boneData, this, null);
 					bone = new Bone(boneData, this, null);
 				} else {
 				} else {
-					Bone parent = bones.Items[boneData.parent.index];
+					Bone parent = bonesItems[boneData.parent.index];
 					bone = new Bone(boneData, this, parent);
 					bone = new Bone(boneData, this, parent);
 					parent.children.Add(bone);
 					parent.children.Add(bone);
 				}
 				}
-				bones.Add(bone);
+				this.bones.Add(bone);
 			}
 			}
 
 
 			slots = new ExposedList<Slot>(data.slots.Count);
 			slots = new ExposedList<Slot>(data.slots.Count);
 			drawOrder = new ExposedList<Slot>(data.slots.Count);
 			drawOrder = new ExposedList<Slot>(data.slots.Count);
 			foreach (SlotData slotData in data.slots) {
 			foreach (SlotData slotData in data.slots) {
-				Bone bone = bones.Items[slotData.boneData.index];
+				Bone bone = bonesItems[slotData.boneData.index];
 				Slot slot = new Slot(slotData, bone);
 				Slot slot = new Slot(slotData, bone);
 				slots.Add(slot);
 				slots.Add(slot);
 				drawOrder.Add(slot);
 				drawOrder.Add(slot);
@@ -115,7 +122,7 @@ namespace Spine {
 				pathConstraints.Add(new PathConstraint(pathConstraintData, this));
 				pathConstraints.Add(new PathConstraint(pathConstraintData, this));
 
 
 			UpdateCache();
 			UpdateCache();
-			UpdateWorldTransform();
+			//UpdateWorldTransform();
 		}
 		}
 
 
 		/// <summary>Caches information about bones and constraints. Must be called if the <see cref="Skin"/> is modified or if bones, constraints, or
 		/// <summary>Caches information about bones and constraints. Must be called if the <see cref="Skin"/> is modified or if bones, constraints, or
@@ -123,7 +130,6 @@ namespace Spine {
 		public void UpdateCache () {
 		public void UpdateCache () {
 			var updateCache = this.updateCache;
 			var updateCache = this.updateCache;
 			updateCache.Clear();
 			updateCache.Clear();
-			this.updateCacheReset.Clear();
 
 
 			int boneCount = this.bones.Items.Length;
 			int boneCount = this.bones.Items.Length;
 			var bones = this.bones;
 			var bones = this.bones;
@@ -191,16 +197,19 @@ namespace Spine {
 			Bone parent = constrained.Items[0];
 			Bone parent = constrained.Items[0];
 			SortBone(parent);
 			SortBone(parent);
 
 
-			if (constrained.Count > 1) {
-				Bone child = constrained.Items[constrained.Count - 1];
-				if (!updateCache.Contains(child))
-					updateCacheReset.Add(child);
+			if (constrained.Count == 1) {
+				updateCache.Add(constraint);
+				SortReset(parent.children);
 			}
 			}
+			else {
+				Bone child = constrained.Items[constrained.Count - 1];
+				SortBone(child);
 
 
-			updateCache.Add(constraint);
+				updateCache.Add(constraint);
 
 
-			SortReset(parent.children);
-			constrained.Items[constrained.Count - 1].sorted = true;
+				SortReset(parent.children);
+				child.sorted = true;
+			}
 		}
 		}
 
 
 		private void SortPathConstraint (PathConstraint constraint) {
 		private void SortPathConstraint (PathConstraint constraint) {
@@ -218,17 +227,17 @@ namespace Spine {
 			Attachment attachment = slot.attachment;
 			Attachment attachment = slot.attachment;
 			if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone);
 			if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone);
 
 
-			var constrained = constraint.bones;
-			int boneCount = constrained.Count;
+			var constrained = constraint.bones.Items;
+			int boneCount = constraint.bones.Count;
 			for (int i = 0; i < boneCount; i++)
 			for (int i = 0; i < boneCount; i++)
-				SortBone(constrained.Items[i]);
+				SortBone(constrained[i]);
 
 
 			updateCache.Add(constraint);
 			updateCache.Add(constraint);
 
 
 			for (int i = 0; i < boneCount; i++)
 			for (int i = 0; i < boneCount; i++)
-				SortReset(constrained.Items[i].children);
+				SortReset(constrained[i].children);
 			for (int i = 0; i < boneCount; i++)
 			for (int i = 0; i < boneCount; i++)
-				constrained.Items[i].sorted = true;
+				constrained[i].sorted = true;
 		}
 		}
 
 
 		private void SortTransformConstraint (TransformConstraint constraint) {
 		private void SortTransformConstraint (TransformConstraint constraint) {
@@ -238,25 +247,25 @@ namespace Spine {
 
 
 			SortBone(constraint.target);
 			SortBone(constraint.target);
 
 
-			var constrained = constraint.bones;
-			int boneCount = constrained.Count;
+			var constrained = constraint.bones.Items;
+			int boneCount = constraint.bones.Count;
 			if (constraint.data.local) {
 			if (constraint.data.local) {
 				for (int i = 0; i < boneCount; i++) {
 				for (int i = 0; i < boneCount; i++) {
-					Bone child = constrained.Items[i];
+					Bone child = constrained[i];
 					SortBone(child.parent);
 					SortBone(child.parent);
-					if (!updateCache.Contains(child)) updateCacheReset.Add(child);
+					SortBone(child);
 				}
 				}
 			} else {
 			} else {
 				for (int i = 0; i < boneCount; i++)
 				for (int i = 0; i < boneCount; i++)
-					SortBone(constrained.Items[i]);
+					SortBone(constrained[i]);
 			}
 			}
 
 
 			updateCache.Add(constraint);
 			updateCache.Add(constraint);
 
 
 			for (int i = 0; i < boneCount; i++)
 			for (int i = 0; i < boneCount; i++)
-				SortReset(constrained.Items[i].children);
+				SortReset(constrained[i].children);
 			for (int i = 0; i < boneCount; i++)
 			for (int i = 0; i < boneCount; i++)
-				constrained.Items[i].sorted = true;
+				constrained[i].sorted = true;
 		}
 		}
 
 
 		private void SortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
 		private void SortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
@@ -271,12 +280,12 @@ namespace Spine {
 			if (pathBones == null)
 			if (pathBones == null)
 				SortBone(slotBone);
 				SortBone(slotBone);
 			else {
 			else {
-				var bones = this.bones;
+				var bones = this.bones.Items;
 				for (int i = 0, n = pathBones.Length; i < n;) {
 				for (int i = 0, n = pathBones.Length; i < n;) {
 					int nn = pathBones[i++];
 					int nn = pathBones[i++];
 					nn += i;
 					nn += i;
 					while (i < nn)
 					while (i < nn)
-						SortBone(bones.Items[pathBones[i++]]);
+						SortBone(bones[pathBones[i++]]);
 				}
 				}
 			}
 			}
 		}
 		}
@@ -299,24 +308,17 @@ namespace Spine {
 			}
 			}
 		}
 		}
 
 
-		/// <summary>Updates the world transform for each bone and applies constraints.</summary>
+
+		/// <summary>
+		/// Updates the world transform for each bone and applies all constraints.
+		/// <para>
+		/// See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+		/// Runtimes Guide.</para>
+		/// </summary>
 		public void UpdateWorldTransform () {
 		public void UpdateWorldTransform () {
-			var updateCacheReset = this.updateCacheReset;
-			var updateCacheResetItems = updateCacheReset.Items;
-			for (int i = 0, n = updateCacheReset.Count; i < n; i++) {
-				Bone bone = updateCacheResetItems[i];
-				bone.ax = bone.x;
-				bone.ay = bone.y;
-				bone.arotation = bone.rotation;
-				bone.ascaleX = bone.scaleX;
-				bone.ascaleY = bone.scaleY;
-				bone.ashearX = bone.shearX;
-				bone.ashearY = bone.shearY;
-				bone.appliedValid = true;
-			}
-			var updateItems = this.updateCache.Items;
-			for (int i = 0, n = updateCache.Count; i < n; i++)
-				updateItems[i].Update();
+			var updateCache = this.updateCache.Items;
+			for (int i = 0, n = this.updateCache.Count; i < n; i++)
+				updateCache[i].Update();
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -324,22 +326,7 @@ namespace Spine {
 		/// all constraints.
 		/// all constraints.
 	 	/// </summary>
 	 	/// </summary>
 		public void UpdateWorldTransform (Bone parent) {
 		public void UpdateWorldTransform (Bone parent) {
-			// This partial update avoids computing the world transform for constrained bones when 1) the bone is not updated
-			// before the constraint, 2) the constraint only needs to access the applied local transform, and 3) the constraint calls
-			// updateWorldTransform.
-			var updateCacheReset = this.updateCacheReset;
-			var updateCacheResetItems = updateCacheReset.Items;
-			for (int i = 0, n = updateCacheReset.Count; i < n; i++) {
-				Bone bone = updateCacheResetItems[i];
-				bone.ax = bone.x;
-				bone.ay = bone.y;
-				bone.arotation = bone.rotation;
-				bone.ascaleX = bone.scaleX;
-				bone.ascaleY = bone.scaleY;
-				bone.ashearX = bone.shearX;
-				bone.ashearY = bone.shearY;
-				bone.appliedValid = true;
-			}
+			if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null.");
 
 
 			// 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;
 			Bone rootBone = this.RootBone;
@@ -358,10 +345,9 @@ namespace Spine {
 			rootBone.d = (pc * lb + pd * ld) * scaleY;
 			rootBone.d = (pc * lb + pd * ld) * scaleY;
 
 
 			// Update everything except root bone.
 			// Update everything except root bone.
-			var updateCache = this.updateCache;
-			var updateCacheItems = updateCache.Items;
-			for (int i = 0, n = updateCache.Count; i < n; i++) {
-				var updatable = updateCacheItems[i];
+			var updateCache = this.updateCache.Items;
+			for (int i = 0, n = this.updateCache.Count; i < n; i++) {
+				var updatable = updateCache[i];
 				if (updatable != rootBone)
 				if (updatable != rootBone)
 					updatable.Update();
 					updatable.Update();
 			}
 			}
@@ -375,13 +361,13 @@ namespace Spine {
 
 
 		/// <summary>Sets the bones and constraints to their setup pose values.</summary>
 		/// <summary>Sets the bones and constraints to their setup pose values.</summary>
 		public void SetBonesToSetupPose () {
 		public void SetBonesToSetupPose () {
-			var bonesItems = this.bones.Items;
-			for (int i = 0, n = bones.Count; i < n; i++)
-				bonesItems[i].SetToSetupPose();
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++)
+				bones[i].SetToSetupPose();
 
 
-			var ikConstraintsItems = this.ikConstraints.Items;
-			for (int i = 0, n = ikConstraints.Count; i < n; i++) {
-				IkConstraint constraint = ikConstraintsItems[i];
+			var ikConstraints = this.ikConstraints.Items;
+			for (int i = 0, n = this.ikConstraints.Count; i < n; i++) {
+				IkConstraint constraint = ikConstraints[i];
 				constraint.mix = constraint.data.mix;
 				constraint.mix = constraint.data.mix;
 				constraint.softness = constraint.data.softness;
 				constraint.softness = constraint.data.softness;
 				constraint.bendDirection = constraint.data.bendDirection;
 				constraint.bendDirection = constraint.data.bendDirection;
@@ -389,9 +375,9 @@ namespace Spine {
 				constraint.stretch = constraint.data.stretch;
 				constraint.stretch = constraint.data.stretch;
 			}
 			}
 
 
-			var transformConstraintsItems = this.transformConstraints.Items;
-			for (int i = 0, n = transformConstraints.Count; i < n; i++) {
-				TransformConstraint constraint = transformConstraintsItems[i];
+			var transformConstraints = this.transformConstraints.Items;
+			for (int i = 0, n = this.transformConstraints.Count; i < n; i++) {
+				TransformConstraint constraint = transformConstraints[i];
 				TransformConstraintData constraintData = constraint.data;
 				TransformConstraintData constraintData = constraint.data;
 				constraint.rotateMix = constraintData.rotateMix;
 				constraint.rotateMix = constraintData.rotateMix;
 				constraint.translateMix = constraintData.translateMix;
 				constraint.translateMix = constraintData.translateMix;
@@ -399,9 +385,9 @@ namespace Spine {
 				constraint.shearMix = constraintData.shearMix;
 				constraint.shearMix = constraintData.shearMix;
 			}
 			}
 
 
-			var pathConstraintItems = this.pathConstraints.Items;
-			for (int i = 0, n = pathConstraints.Count; i < n; i++) {
-				PathConstraint constraint = pathConstraintItems[i];
+			var pathConstraints = this.pathConstraints.Items;
+			for (int i = 0, n = this.pathConstraints.Count; i < n; i++) {
+				PathConstraint constraint = pathConstraints[i];
 				PathConstraintData constraintData = constraint.data;
 				PathConstraintData constraintData = constraint.data;
 				constraint.position = constraintData.position;
 				constraint.position = constraintData.position;
 				constraint.spacing = constraintData.spacing;
 				constraint.spacing = constraintData.spacing;
@@ -411,23 +397,21 @@ namespace Spine {
 		}
 		}
 
 
 		public void SetSlotsToSetupPose () {
 		public void SetSlotsToSetupPose () {
-			var slots = this.slots;
-			var slotsItems = slots.Items;
-			drawOrder.Clear();
-			for (int i = 0, n = slots.Count; i < n; i++)
-				drawOrder.Add(slotsItems[i]);
-
-			for (int i = 0, n = slots.Count; i < n; i++)
-				slotsItems[i].SetToSetupPose();
+			var slots = this.slots.Items;
+			int n = this.slots.Count;
+			Array.Copy(slots, 0, drawOrder.Items, 0, n);
+			for (int i = 0; i < n; i++)
+				slots[i].SetToSetupPose();
 		}
 		}
 
 
+		/// <summary>Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
+		/// repeatedly.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public Bone FindBone (string boneName) {
 		public Bone FindBone (string boneName) {
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
-			var bones = this.bones;
-			var bonesItems = bones.Items;
-			for (int i = 0, n = bones.Count; i < n; i++) {
-				Bone bone = bonesItems[i];
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++) {
+				Bone bone = bones[i];
 				if (bone.data.name == boneName) return bone;
 				if (bone.data.name == boneName) return bone;
 			}
 			}
 			return null;
 			return null;
@@ -443,13 +427,14 @@ namespace Spine {
 			return -1;
 			return -1;
 		}
 		}
 
 
+		/// <summary>Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
+		/// repeatedly.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public Slot FindSlot (string slotName) {
 		public Slot FindSlot (string slotName) {
 			if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
 			if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
-			var slots = this.slots;
-			var slotsItems = slots.Items;
-			for (int i = 0, n = slots.Count; i < n; i++) {
-				Slot slot = slotsItems[i];
+			var slots = this.slots.Items;
+			for (int i = 0, n = this.slots.Count; i < n; i++) {
+				Slot slot = slots[i];
 				if (slot.data.name == slotName) return slot;
 				if (slot.data.name == slotName) return slot;
 			}
 			}
 			return null;
 			return null;
@@ -461,11 +446,11 @@ namespace Spine {
 			var slots = this.slots;
 			var slots = this.slots;
 			var slotsItems = slots.Items;
 			var slotsItems = slots.Items;
 			for (int i = 0, n = slots.Count; i < n; i++)
 			for (int i = 0, n = slots.Count; i < n; i++)
-				if (slotsItems[i].data.name.Equals(slotName)) return i;
+				if (slotsItems[i].data.name == slotName) return i;
 			return -1;
 			return -1;
 		}
 		}
 
 
-		/// <summary>Sets a skin by name (see SetSkin).</summary>
+		/// <summary>Sets a skin by name (<see cref="SetSkin(Skin)"/>).</summary>
 		public void SetSkin (string skinName) {
 		public void SetSkin (string skinName) {
 			Skin foundSkin = data.FindSkin(skinName);
 			Skin foundSkin = data.FindSkin(skinName);
 			if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName");
 			if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName");
@@ -506,7 +491,7 @@ namespace Spine {
 			UpdateCache();
 			UpdateCache();
 		}
 		}
 
 
-		/// <summary>Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment name.</summary>
+		/// <summary>Finds an attachment by looking in the <see cref="Skeleton.Skin"/> and <see cref="SkeletonData.DefaultSkin"/> using the slot name and attachment name.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public Attachment GetAttachment (string slotName, string attachmentName) {
 		public Attachment GetAttachment (string slotName, string attachmentName) {
 			return GetAttachment(data.FindSlotIndex(slotName), attachmentName);
 			return GetAttachment(data.FindSlotIndex(slotName), attachmentName);
@@ -543,34 +528,40 @@ namespace Spine {
 			throw new Exception("Slot not found: " + slotName);
 			throw new Exception("Slot not found: " + slotName);
 		}
 		}
 
 
+		/// <summary>Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
+		/// than to call it repeatedly.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public IkConstraint FindIkConstraint (string constraintName) {
 		public IkConstraint FindIkConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<IkConstraint> ikConstraints = this.ikConstraints;
-			for (int i = 0, n = ikConstraints.Count; i < n; i++) {
-				IkConstraint ikConstraint = ikConstraints.Items[i];
+			var ikConstraints = this.ikConstraints.Items;
+			for (int i = 0, n = this.ikConstraints.Count; i < n; i++) {
+				IkConstraint ikConstraint = ikConstraints[i];
 				if (ikConstraint.data.name == constraintName) return ikConstraint;
 				if (ikConstraint.data.name == constraintName) return ikConstraint;
 			}
 			}
 			return null;
 			return null;
 		}
 		}
 
 
+		/// <summary>Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
+		/// this method than to call it repeatedly.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public TransformConstraint FindTransformConstraint (string constraintName) {
 		public TransformConstraint FindTransformConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<TransformConstraint> transformConstraints = this.transformConstraints;
-			for (int i = 0, n = transformConstraints.Count; i < n; i++) {
-				TransformConstraint transformConstraint = transformConstraints.Items[i];
+			var transformConstraints = this.transformConstraints.Items;
+			for (int i = 0, n = this.transformConstraints.Count; i < n; i++) {
+				TransformConstraint transformConstraint = transformConstraints[i];
 				if (transformConstraint.data.Name == constraintName) return transformConstraint;
 				if (transformConstraint.data.Name == constraintName) return transformConstraint;
 			}
 			}
 			return null;
 			return null;
 		}
 		}
 
 
+		/// <summary>Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
+		/// than to call it repeatedly.</summary>
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public PathConstraint FindPathConstraint (string constraintName) {
 		public PathConstraint FindPathConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<PathConstraint> pathConstraints = this.pathConstraints;
-			for (int i = 0, n = pathConstraints.Count; i < n; i++) {
-				PathConstraint constraint = pathConstraints.Items[i];
+			var pathConstraints = this.pathConstraints.Items;
+			for (int i = 0, n = this.pathConstraints.Count; i < n; i++) {
+				PathConstraint constraint = pathConstraints[i];
 				if (constraint.data.Name.Equals(constraintName)) return constraint;
 				if (constraint.data.Name.Equals(constraintName)) return constraint;
 			}
 			}
 			return null;
 			return null;
@@ -589,10 +580,10 @@ namespace Spine {
 		public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer) {
 		public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer) {
 			float[] temp = vertexBuffer;
 			float[] temp = vertexBuffer;
 			temp = temp ?? new float[8];
 			temp = temp ?? new float[8];
-			var drawOrderItems = this.drawOrder.Items;
+			var drawOrder = this.drawOrder.Items;
 			float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
 			float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
-			for (int i = 0, n = drawOrderItems.Length; i < n; i++) {
-				Slot slot = drawOrderItems[i];
+			for (int i = 0, n = this.drawOrder.Count; i < n; i++) {
+				Slot slot = drawOrder[i];
 				if (!slot.bone.active) continue;
 				if (!slot.bone.active) continue;
 				int verticesLength = 0;
 				int verticesLength = 0;
 				float[] vertices = null;
 				float[] vertices = null;

+ 298 - 234
spine-csharp/src/SkeletonBinary.cs

@@ -34,6 +34,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Runtime.Serialization;
 
 
 #if WINDOWS_STOREAPP
 #if WINDOWS_STOREAPP
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -41,7 +42,7 @@ using Windows.Storage;
 #endif
 #endif
 
 
 namespace Spine {
 namespace Spine {
-	public class SkeletonBinary {
+	public class SkeletonBinary : SkeletonLoader {
 		public const int BONE_ROTATE = 0;
 		public const int BONE_ROTATE = 0;
 		public const int BONE_TRANSLATE = 1;
 		public const int BONE_TRANSLATE = 1;
 		public const int BONE_SCALE = 2;
 		public const int BONE_SCALE = 2;
@@ -59,22 +60,15 @@ namespace Spine {
 		public const int CURVE_STEPPED = 1;
 		public const int CURVE_STEPPED = 1;
 		public const int CURVE_BEZIER = 2;
 		public const int CURVE_BEZIER = 2;
 
 
-		public float Scale { get; set; }
-
-		private AttachmentLoader attachmentLoader;
-		private List<SkeletonJson.LinkedMesh> linkedMeshes = new List<SkeletonJson.LinkedMesh>();
-
-		public SkeletonBinary (params Atlas[] atlasArray)
-			: this(new AtlasAttachmentLoader(atlasArray)) {
+		public SkeletonBinary (AttachmentLoader attachmentLoader)
+			:base(attachmentLoader) {
 		}
 		}
 
 
-		public SkeletonBinary (AttachmentLoader attachmentLoader) {
-			if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader");
-			this.attachmentLoader = attachmentLoader;
-			Scale = 1;
+		public SkeletonBinary (params Atlas[] atlasArray)
+			: base(atlasArray) {
 		}
 		}
 
 
-		#if !ISUNITY && WINDOWS_STOREAPP
+#if !ISUNITY && WINDOWS_STOREAPP
 		private async Task<SkeletonData> ReadFile(string path) {
 		private async Task<SkeletonData> ReadFile(string path) {
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) {
 			using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) {
@@ -84,11 +78,11 @@ namespace Spine {
 			}
 			}
 		}
 		}
 
 
-		public SkeletonData ReadSkeletonData (String path) {
+		public override SkeletonData ReadSkeletonData (string path) {
 			return this.ReadFile(path).Result;
 			return this.ReadFile(path).Result;
 		}
 		}
-		#else
-		public SkeletonData ReadSkeletonData (String path) {
+#else
+		public override SkeletonData ReadSkeletonData (string path) {
 		#if WINDOWS_PHONE
 		#if WINDOWS_PHONE
 			using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 			using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 		#else
 		#else
@@ -119,13 +113,13 @@ namespace Spine {
 
 
 		public SkeletonData ReadSkeletonData (Stream file) {
 		public SkeletonData ReadSkeletonData (Stream file) {
 			if (file == null) throw new ArgumentNullException("file");
 			if (file == null) throw new ArgumentNullException("file");
-			float scale = Scale;
+			float scale = this.scale;
 
 
 			var skeletonData = new SkeletonData();
 			var skeletonData = new SkeletonData();
 			SkeletonInput input = new SkeletonInput(file);
 			SkeletonInput input = new SkeletonInput(file);
 
 
-			skeletonData.hash = input.ReadString();
-			if (skeletonData.hash.Length == 0) skeletonData.hash = null;
+			long hash = input.ReadLong();
+			skeletonData.hash = hash == 0 ? null : hash.ToString();
 			skeletonData.version = input.ReadString();
 			skeletonData.version = input.ReadString();
 			if (skeletonData.version.Length == 0) skeletonData.version = null;
 			if (skeletonData.version.Length == 0) skeletonData.version = null;
 			if ("3.8.75" == skeletonData.version)
 			if ("3.8.75" == skeletonData.version)
@@ -151,16 +145,15 @@ namespace Spine {
 			Object[] o;
 			Object[] o;
 
 
 			// Strings.
 			// Strings.
-			input.strings = new ExposedList<string>(n = input.ReadInt(true));
-			o = input.strings.Resize(n).Items;
+			o = input.strings = new String[n = input.ReadInt(true)];
 			for (int i = 0; i < n; i++)
 			for (int i = 0; i < n; i++)
 				o[i] = input.ReadString();
 				o[i] = input.ReadString();
 
 
 			// Bones.
 			// Bones.
-			o = skeletonData.bones.Resize(n = input.ReadInt(true)).Items;
+			var bones = skeletonData.bones.Resize(n = input.ReadInt(true)).Items;
 			for (int i = 0; i < n; i++) {
 			for (int i = 0; i < n; i++) {
 				String name = input.ReadString();
 				String name = input.ReadString();
-				BoneData parent = i == 0 ? null : skeletonData.bones.Items[input.ReadInt(true)];
+				BoneData parent = i == 0 ? null : bones[input.ReadInt(true)];
 				BoneData data = new BoneData(i, name, parent);
 				BoneData data = new BoneData(i, name, parent);
 				data.rotation = input.ReadFloat();
 				data.rotation = input.ReadFloat();
 				data.x = input.ReadFloat() * scale;
 				data.x = input.ReadFloat() * scale;
@@ -169,18 +162,18 @@ namespace Spine {
 				data.scaleY = input.ReadFloat();
 				data.scaleY = input.ReadFloat();
 				data.shearX = input.ReadFloat();
 				data.shearX = input.ReadFloat();
 				data.shearY = input.ReadFloat();
 				data.shearY = input.ReadFloat();
-				data.length = input.ReadFloat() * scale;
+				data.Length = input.ReadFloat() * scale;
 				data.transformMode = TransformModeValues[input.ReadInt(true)];
 				data.transformMode = TransformModeValues[input.ReadInt(true)];
 				data.skinRequired = input.ReadBoolean();
 				data.skinRequired = input.ReadBoolean();
 				if (nonessential) input.ReadInt(); // Skip bone color.
 				if (nonessential) input.ReadInt(); // Skip bone color.
-				o[i] = data;
+				bones[i] = data;
 			}
 			}
 
 
 			// Slots.
 			// Slots.
-			o = skeletonData.slots.Resize(n = input.ReadInt(true)).Items;
+			var slots = skeletonData.slots.Resize(n = input.ReadInt(true)).Items;
 			for (int i = 0; i < n; i++) {
 			for (int i = 0; i < n; i++) {
 				String slotName = input.ReadString();
 				String slotName = input.ReadString();
-				BoneData boneData = skeletonData.bones.Items[input.ReadInt(true)];
+				BoneData boneData = bones[input.ReadInt(true)];
 				SlotData slotData = new SlotData(i, slotName, boneData);
 				SlotData slotData = new SlotData(i, slotName, boneData);
 				int color = input.ReadInt();
 				int color = input.ReadInt();
 				slotData.r = ((color & 0xff000000) >> 24) / 255f;
 				slotData.r = ((color & 0xff000000) >> 24) / 255f;
@@ -198,7 +191,7 @@ namespace Spine {
 
 
 				slotData.attachmentName = input.ReadStringRef();
 				slotData.attachmentName = input.ReadStringRef();
 				slotData.blendMode = (BlendMode)input.ReadInt(true);
 				slotData.blendMode = (BlendMode)input.ReadInt(true);
-				o[i] = slotData;
+				slots[i] = slotData;
 			}
 			}
 
 
 			// IK constraints.
 			// IK constraints.
@@ -207,10 +200,10 @@ namespace Spine {
 				IkConstraintData data = new IkConstraintData(input.ReadString());
 				IkConstraintData data = new IkConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				data.order = input.ReadInt(true);
 				data.skinRequired = input.ReadBoolean();
 				data.skinRequired = input.ReadBoolean();
-				Object[] bones = data.bones.Resize(nn = input.ReadInt(true)).Items;
+				var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 				for (int ii = 0; ii < nn; ii++)
-					bones[ii] = skeletonData.bones.Items[input.ReadInt(true)];
-				data.target = skeletonData.bones.Items[input.ReadInt(true)];
+					constraintBones[ii] = bones[input.ReadInt(true)];
+				data.target = bones[input.ReadInt(true)];
 				data.mix = input.ReadFloat();
 				data.mix = input.ReadFloat();
 				data.softness = input.ReadFloat() * scale;
 				data.softness = input.ReadFloat() * scale;
 				data.bendDirection = input.ReadSByte();
 				data.bendDirection = input.ReadSByte();
@@ -226,10 +219,10 @@ namespace Spine {
 				TransformConstraintData data = new TransformConstraintData(input.ReadString());
 				TransformConstraintData data = new TransformConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				data.order = input.ReadInt(true);
 				data.skinRequired = input.ReadBoolean();
 				data.skinRequired = input.ReadBoolean();
-				Object[] bones = data.bones.Resize(nn = input.ReadInt(true)).Items;
+				var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 				for (int ii = 0; ii < nn; ii++)
-					bones[ii] = skeletonData.bones.Items[input.ReadInt(true)];
-				data.target = skeletonData.bones.Items[input.ReadInt(true)];
+					constraintBones[ii] = bones[input.ReadInt(true)];
+				data.target = bones[input.ReadInt(true)];
 				data.local = input.ReadBoolean();
 				data.local = input.ReadBoolean();
 				data.relative = input.ReadBoolean();
 				data.relative = input.ReadBoolean();
 				data.offsetRotation = input.ReadFloat();
 				data.offsetRotation = input.ReadFloat();
@@ -251,10 +244,10 @@ namespace Spine {
 				PathConstraintData data = new PathConstraintData(input.ReadString());
 				PathConstraintData data = new PathConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				data.order = input.ReadInt(true);
 				data.skinRequired = input.ReadBoolean();
 				data.skinRequired = input.ReadBoolean();
-				Object[] bones = data.bones.Resize(nn = input.ReadInt(true)).Items;
+				Object[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 				for (int ii = 0; ii < nn; ii++)
-					bones[ii] = skeletonData.bones.Items[input.ReadInt(true)];
-				data.target = skeletonData.slots.Items[input.ReadInt(true)];
+					constraintBones[ii] = bones[input.ReadInt(true)];
+				data.target = slots[input.ReadInt(true)];
 				data.positionMode = (PositionMode)Enum.GetValues(typeof(PositionMode)).GetValue(input.ReadInt(true));
 				data.positionMode = (PositionMode)Enum.GetValues(typeof(PositionMode)).GetValue(input.ReadInt(true));
 				data.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(input.ReadInt(true));
 				data.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(input.ReadInt(true));
 				data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(input.ReadInt(true));
 				data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(input.ReadInt(true));
@@ -286,7 +279,7 @@ namespace Spine {
 			// Linked meshes.
 			// Linked meshes.
 			n = linkedMeshes.Count;
 			n = linkedMeshes.Count;
 			for (int i = 0; i < n; i++) {
 			for (int i = 0; i < n; i++) {
-				SkeletonJson.LinkedMesh linkedMesh = linkedMeshes[i];
+				LinkedMesh linkedMesh = linkedMeshes[i];
 				Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin);
 				Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin);
 				if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin);
 				if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin);
 				Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent);
 				Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent);
@@ -334,16 +327,21 @@ namespace Spine {
 			} else {
 			} else {
 				skin = new Skin(input.ReadStringRef());
 				skin = new Skin(input.ReadStringRef());
 				Object[] bones = skin.bones.Resize(input.ReadInt(true)).Items;
 				Object[] bones = skin.bones.Resize(input.ReadInt(true)).Items;
+				var bonesItems = skeletonData.bones.Items;
 				for (int i = 0, n = skin.bones.Count; i < n; i++)
 				for (int i = 0, n = skin.bones.Count; i < n; i++)
-					bones[i] = skeletonData.bones.Items[input.ReadInt(true)];
+					bones[i] = bonesItems[input.ReadInt(true)];
 
 
+				var ikConstraintsItems = skeletonData.ikConstraints.Items;
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
-					skin.constraints.Add(skeletonData.ikConstraints.Items[input.ReadInt(true)]);
+					skin.constraints.Add(ikConstraintsItems[input.ReadInt(true)]);
+				var transformConstraintsItems = skeletonData.transformConstraints.Items;
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
-					skin.constraints.Add(skeletonData.transformConstraints.Items[input.ReadInt(true)]);
+					skin.constraints.Add(transformConstraintsItems[input.ReadInt(true)]);
+				var pathConstraintsItems = skeletonData.pathConstraints.Items;
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
-					skin.constraints.Add(skeletonData.pathConstraints.Items[input.ReadInt(true)]);
+					skin.constraints.Add(pathConstraintsItems[input.ReadInt(true)]);
 				skin.constraints.TrimExcess();
 				skin.constraints.TrimExcess();
+
 				slotCount = input.ReadInt(true);
 				slotCount = input.ReadInt(true);
 			}
 			}
 			for (int i = 0; i < slotCount; i++) {
 			for (int i = 0; i < slotCount; i++) {
@@ -359,14 +357,12 @@ namespace Spine {
 
 
 		private Attachment ReadAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex,
 		private Attachment ReadAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex,
 			String attachmentName, bool nonessential) {
 			String attachmentName, bool nonessential) {
-
-			float scale = Scale;
+			float scale = this.scale;
 
 
 			String name = input.ReadStringRef();
 			String name = input.ReadStringRef();
 			if (name == null) name = attachmentName;
 			if (name == null) name = attachmentName;
 
 
-			AttachmentType type = (AttachmentType)input.ReadByte();
-			switch (type) {
+			switch ((AttachmentType)input.ReadByte()) {
 			case AttachmentType.Region: {
 			case AttachmentType.Region: {
 					String path = input.ReadStringRef();
 					String path = input.ReadStringRef();
 					float rotation = input.ReadFloat();
 					float rotation = input.ReadFloat();
@@ -529,7 +525,7 @@ namespace Spine {
 		}
 		}
 
 
 		private Vertices ReadVertices (SkeletonInput input, int vertexCount) {
 		private Vertices ReadVertices (SkeletonInput input, int vertexCount) {
-			float scale = Scale;
+			float scale = this.scale;
 			int verticesLength = vertexCount << 1;
 			int verticesLength = vertexCount << 1;
 			Vertices vertices = new Vertices();
 			Vertices vertices = new Vertices();
 			if(!input.ReadBoolean()) {
 			if(!input.ReadBoolean()) {
@@ -574,66 +570,97 @@ namespace Spine {
 			return array;
 			return array;
 		}
 		}
 
 
+		/// <exception cref="SerializationException">SerializationException will be thrown when a Vertex attachment is not found.</exception>
+		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
 		private Animation ReadAnimation (String name, SkeletonInput input, SkeletonData skeletonData) {
 		private Animation ReadAnimation (String name, SkeletonInput input, SkeletonData skeletonData) {
-			var timelines = new ExposedList<Timeline>(32);
-			float scale = Scale;
-			float duration = 0;
+			var timelines = new ExposedList<Timeline>(input.ReadInt(true));
+			float scale = this.scale;
 
 
 			// Slot timelines.
 			// Slot timelines.
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 				int slotIndex = input.ReadInt(true);
 				int slotIndex = input.ReadInt(true);
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
-					int timelineType = input.ReadByte();
-					int frameCount = input.ReadInt(true);
+					int timelineType = input.ReadByte(), frameCount = input.ReadInt(true), frameLast = frameCount - 1;
 					switch (timelineType) {
 					switch (timelineType) {
-					case SLOT_ATTACHMENT: {
-							AttachmentTimeline timeline = new AttachmentTimeline(frameCount);
-							timeline.slotIndex = slotIndex;
-							for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
-								timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadStringRef());
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[frameCount - 1]);
-							break;
-						}
-					case SLOT_COLOR: {
-							ColorTimeline timeline = new ColorTimeline(frameCount);
-							timeline.slotIndex = slotIndex;
-							for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+						case SLOT_ATTACHMENT: {
+								AttachmentTimeline timeline = new AttachmentTimeline(frameCount, slotIndex);
+								for (int frame = 0; frame < frameCount; frame++)
+									timeline.SetFrame(frame, input.ReadFloat(), input.ReadStringRef());
+								timelines.Add(timeline);
+								break;
+							}
+						case SLOT_COLOR: {
+								ColorTimeline timeline = new ColorTimeline(frameCount, input.ReadInt(true), slotIndex);
 								float time = input.ReadFloat();
 								float time = input.ReadFloat();
-								int color = input.ReadInt();
-								float r = ((color & 0xff000000) >> 24) / 255f;
-								float g = ((color & 0x00ff0000) >> 16) / 255f;
-								float b = ((color & 0x0000ff00) >> 8) / 255f;
-								float a = ((color & 0x000000ff)) / 255f;
-								timeline.SetFrame(frameIndex, time, r, g, b, a);
-								if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
+								float r = input.Read() / 255f, g = input.Read() / 255f;
+								float b = input.Read() / 255f, a = input.Read() / 255f;
+								for (int frame = 0, bezier = 0; ; frame++) {
+									timeline.SetFrame(frame, time, r, g, b, a);
+									if (frame == frameLast) break;
+									float time2 = input.ReadFloat();
+									float r2 = input.Read() / 255f, g2 = input.Read() / 255f;
+									float b2 = input.Read() / 255f, a2 = input.Read() / 255f;
+									switch (input.ReadByte()) {
+										case CURVE_STEPPED:
+											timeline.SetStepped(frame);
+											break;
+										case CURVE_BEZIER:
+											SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1);
+											SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1);
+											SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1);
+											SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1);
+											break;
+									}
+									time = time2;
+									r = r2;
+									g = g2;
+									b = b2;
+									a = a2;
+								}
+								timelines.Add(timeline);
+								break;
 							}
 							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(frameCount - 1) * ColorTimeline.ENTRIES]);
-							break;
-						}
-					case SLOT_TWO_COLOR: {
-							TwoColorTimeline timeline = new TwoColorTimeline(frameCount);
-							timeline.slotIndex = slotIndex;
-							for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+						case SLOT_TWO_COLOR: {
+								TwoColorTimeline timeline = new TwoColorTimeline(frameCount, input.ReadInt(true), slotIndex);
 								float time = input.ReadFloat();
 								float time = input.ReadFloat();
-								int color = input.ReadInt();
-								float r = ((color & 0xff000000) >> 24) / 255f;
-								float g = ((color & 0x00ff0000) >> 16) / 255f;
-								float b = ((color & 0x0000ff00) >> 8) / 255f;
-								float a = ((color & 0x000000ff)) / 255f;
-								int color2 = input.ReadInt(); // 0x00rrggbb
-								float r2 = ((color2 & 0x00ff0000) >> 16) / 255f;
-								float g2 = ((color2 & 0x0000ff00) >> 8) / 255f;
-								float b2 = ((color2 & 0x000000ff)) / 255f;
-
-								timeline.SetFrame(frameIndex, time, r, g, b, a, r2, g2, b2);
-								if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
+								float r = input.Read() / 255f, g = input.Read() / 255f;
+								float b = input.Read() / 255f, a = input.Read() / 255f;
+								float r2 = input.Read() / 255f, g2 = input.Read() / 255f;
+								float b2 = input.Read() / 255f;
+								for (int frame = 0, bezier = 0; ; frame++) {
+									timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2);
+									if (frame == frameLast) break;
+									float time2 = input.ReadFloat();
+									float nr = input.Read() / 255f, ng = input.Read() / 255f;
+									float nb = input.Read() / 255f, na = input.Read() / 255f;
+									float nr2 = input.Read() / 255f, ng2 = input.Read() / 255f;
+									float nb2 = input.Read() / 255f;
+									switch (input.ReadByte()) {
+										case CURVE_STEPPED:
+											timeline.SetStepped(frame);
+											break;
+										case CURVE_BEZIER:
+											SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1);
+											SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1);
+											SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1);
+											SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1);
+											SetBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1);
+											SetBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1);
+											SetBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1);
+											break;
+									}
+									time = time2;
+									r = nr;
+									g = ng;
+									b = nb;
+									a = na;
+									r2 = nr2;
+									g2 = ng2;
+									b2 = nb2;
+								}
+								timelines.Add(timeline);
+								break;
 							}
 							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TwoColorTimeline.ENTRIES]);
-							break;
-						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -642,76 +669,78 @@ namespace Spine {
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 				int boneIndex = input.ReadInt(true);
 				int boneIndex = input.ReadInt(true);
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
-					int timelineType = input.ReadByte();
-					int frameCount = input.ReadInt(true);
-					switch (timelineType) {
-					case BONE_ROTATE: {
-							RotateTimeline timeline = new RotateTimeline(frameCount);
-							timeline.boneIndex = boneIndex;
-							for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-								timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat());
-								if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(frameCount - 1) * RotateTimeline.ENTRIES]);
+					switch (input.ReadByte()) {
+						case BONE_ROTATE:
+							timelines.Add(ReadTimeline(input, new RotateTimeline(input.ReadInt(true), input.ReadInt(true), boneIndex), 1));
 							break;
 							break;
-						}
-					case BONE_TRANSLATE:
-					case BONE_SCALE:
-					case BONE_SHEAR: {
-							TranslateTimeline timeline;
-							float timelineScale = 1;
-							if (timelineType == BONE_SCALE)
-								timeline = new ScaleTimeline(frameCount);
-							else if (timelineType == BONE_SHEAR)
-								timeline = new ShearTimeline(frameCount);
-							else {
-								timeline = new TranslateTimeline(frameCount);
-								timelineScale = scale;
-							}
-							timeline.boneIndex = boneIndex;
-							for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-								timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat() * timelineScale,
-									input.ReadFloat() * timelineScale);
-								if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TranslateTimeline.ENTRIES]);
+						case BONE_TRANSLATE:
+							timelines
+								.Add(ReadTimeline(input, new TranslateTimeline(input.ReadInt(true), input.ReadInt(true), boneIndex), scale));
+							break;
+						case BONE_SCALE:
+							timelines.Add(ReadTimeline(input, new ScaleTimeline(input.ReadInt(true), input.ReadInt(true), boneIndex), 1));
+							break;
+						case BONE_SHEAR:
+							timelines.Add(ReadTimeline(input, new ShearTimeline(input.ReadInt(true), input.ReadInt(true), boneIndex), 1));
 							break;
 							break;
-						}
 					}
 					}
 				}
 				}
 			}
 			}
 
 
 			// IK constraint timelines.
 			// IK constraint timelines.
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
-				int index = input.ReadInt(true);
-				int frameCount = input.ReadInt(true);
-				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount) {
-					ikConstraintIndex = index
-				};
-				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat(), input.ReadFloat() * scale, input.ReadSByte(), input.ReadBoolean(),
-						input.ReadBoolean());
-					if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
+				int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1;
+				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.ReadInt(true), index);
+				float time = input.ReadFloat(), mix = input.ReadFloat(), softness = input.ReadFloat() * scale;
+				for (int frame = 0, bezier = 0; ; frame++) {
+					timeline.SetFrame(frame, time, mix, softness, input.ReadSByte(), input.ReadBoolean(), input.ReadBoolean());
+					if (frame == frameLast) break;
+					float time2 = input.ReadFloat(), mix2 = input.ReadFloat(), softness2 = input.ReadFloat() * scale;
+					switch (input.ReadByte()) {
+						case CURVE_STEPPED:
+							timeline.SetStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							SetBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
+							SetBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale);
+							break;
+					}
+					time = time2;
+					mix = mix2;
+					softness = softness2;
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[(frameCount - 1) * IkConstraintTimeline.ENTRIES]);
 			}
 			}
 
 
 			// Transform constraint timelines.
 			// Transform constraint timelines.
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
-				int index = input.ReadInt(true);
-				int frameCount = input.ReadInt(true);
-				TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount);
-				timeline.transformConstraintIndex = index;
-				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat(), input.ReadFloat(), input.ReadFloat(),
-						input.ReadFloat());
-					if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
+				int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1;
+				TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount, input.ReadInt(true), index);
+				float time = input.ReadFloat(), rotateMix = input.ReadFloat(), translateMix = input.ReadFloat(),
+					scaleMix = input.ReadFloat(), shearMix = input.ReadFloat();
+				for (int frame = 0, bezier = 0; ; frame++) {
+					timeline.SetFrame(frame, time, rotateMix, translateMix, scaleMix, shearMix);
+					if (frame == frameLast) break;
+					float time2 = input.ReadFloat(), rotateMix2 = input.ReadFloat(), translateMix2 = input.ReadFloat(),
+						scaleMix2 = input.ReadFloat(), shearMix2 = input.ReadFloat();
+					switch (input.ReadByte()) {
+						case CURVE_STEPPED:
+							timeline.SetStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							SetBezier(input, timeline, bezier++, frame, 0, time, time2, rotateMix, rotateMix2, 1);
+							SetBezier(input, timeline, bezier++, frame, 1, time, time2, translateMix, translateMix2, 1);
+							SetBezier(input, timeline, bezier++, frame, 2, time, time2, scaleMix, scaleMix2, 1);
+							SetBezier(input, timeline, bezier++, frame, 3, time, time2, shearMix, shearMix2, 1);
+							break;
+					}
+					time = time2;
+					rotateMix = rotateMix2;
+					translateMix = translateMix2;
+					scaleMix = scaleMix2;
+					shearMix = shearMix2;
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]);
 			}
 			}
 
 
 			// Path constraint timelines.
 			// Path constraint timelines.
@@ -719,40 +748,21 @@ namespace Spine {
 				int index = input.ReadInt(true);
 				int index = input.ReadInt(true);
 				PathConstraintData data = skeletonData.pathConstraints.Items[index];
 				PathConstraintData data = skeletonData.pathConstraints.Items[index];
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
-					int timelineType = input.ReadSByte();
-					int frameCount = input.ReadInt(true);
-					switch(timelineType) {
+					switch (input.ReadByte()) {
 						case PATH_POSITION:
 						case PATH_POSITION:
-						case PATH_SPACING: {
-								PathConstraintPositionTimeline timeline;
-								float timelineScale = 1;
-								if (timelineType == PATH_SPACING) {
-									timeline = new PathConstraintSpacingTimeline(frameCount);
-									if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) timelineScale = scale;
-								} else {
-									timeline = new PathConstraintPositionTimeline(frameCount);
-									if (data.positionMode == PositionMode.Fixed) timelineScale = scale;
-								}
-								timeline.pathConstraintIndex = index;
-								for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-									timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat() * timelineScale);
-									if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
-								}
-								timelines.Add(timeline);
-								duration = Math.Max(duration, timeline.frames[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]);
-								break;
-							}
-						case PATH_MIX: {
-								PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frameCount);
-								timeline.pathConstraintIndex = index;
-								for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-									timeline.SetFrame(frameIndex, input.ReadFloat(), input.ReadFloat(), input.ReadFloat());
-									if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
-								}
-								timelines.Add(timeline);
-								duration = Math.Max(duration, timeline.frames[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]);
-								break;
-							}
+							timelines
+								.Add(ReadTimeline(input, new PathConstraintPositionTimeline(input.ReadInt(true), input.ReadInt(true), index),
+									data.positionMode == PositionMode.Fixed ? scale : 1));
+							break;
+						case PATH_SPACING:
+							timelines
+								.Add(ReadTimeline(input, new PathConstraintSpacingTimeline(input.ReadInt(true), input.ReadInt(true), index),
+									data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1));
+							break;
+						case PATH_MIX:
+							timelines
+								.Add(ReadTimeline(input, new PathConstraintMixTimeline(input.ReadInt(true), input.ReadInt(true), index), 1));
+							break;
 					}
 					}
 				}
 				}
 			}
 			}
@@ -763,18 +773,18 @@ namespace Spine {
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
 					int slotIndex = input.ReadInt(true);
 					int slotIndex = input.ReadInt(true);
 					for (int iii = 0, nnn = input.ReadInt(true); iii < nnn; iii++) {
 					for (int iii = 0, nnn = input.ReadInt(true); iii < nnn; iii++) {
-						VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, input.ReadStringRef());
-						bool weighted = attachment.bones != null;
-						float[] vertices = attachment.vertices;
-						int deformLength = weighted ? vertices.Length / 3 * 2 : vertices.Length;
-
-						int frameCount = input.ReadInt(true);
-						DeformTimeline timeline = new DeformTimeline(frameCount);
-						timeline.slotIndex = slotIndex;
-						timeline.attachment = attachment;
-
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							float time = input.ReadFloat();
+						String attachmentName = input.ReadStringRef();
+						VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, attachmentName);
+						if (attachment == null) throw new SerializationException("Vertex attachment not found: " + attachmentName);
+						bool weighted = attachment.Bones != null;
+						float[] vertices = attachment.Vertices;
+						int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length;
+
+						int frameCount = input.ReadInt(true), frameLast = frameCount - 1;
+						DeformTimeline timeline = new DeformTimeline(frameCount, input.ReadInt(true), slotIndex, attachment);
+
+						float time = input.ReadFloat();
+						for (int frame = 0, bezier = 0; ; frame++) {
 							float[] deform;
 							float[] deform;
 							int end = input.ReadInt(true);
 							int end = input.ReadInt(true);
 							if (end == 0)
 							if (end == 0)
@@ -786,7 +796,8 @@ namespace Spine {
 								if (scale == 1) {
 								if (scale == 1) {
 									for (int v = start; v < end; v++)
 									for (int v = start; v < end; v++)
 										deform[v] = input.ReadFloat();
 										deform[v] = input.ReadFloat();
-								} else {
+								}
+								else {
 									for (int v = start; v < end; v++)
 									for (int v = start; v < end; v++)
 										deform[v] = input.ReadFloat() * scale;
 										deform[v] = input.ReadFloat() * scale;
 								}
 								}
@@ -795,12 +806,20 @@ namespace Spine {
 										deform[v] += vertices[v];
 										deform[v] += vertices[v];
 								}
 								}
 							}
 							}
-
-							timeline.SetFrame(frameIndex, time, deform);
-							if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
+							timeline.SetFrame(frame, time, deform);
+							if (frame == frameLast) break;
+							float time2 = input.ReadFloat();
+							switch (input.ReadByte()) {
+								case CURVE_STEPPED:
+									timeline.SetStepped(frame);
+									break;
+								case CURVE_BEZIER:
+									SetBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
+									break;
+							}
+							time = time2;
 						}
 						}
 						timelines.Add(timeline);
 						timelines.Add(timeline);
-						duration = Math.Max(duration, timeline.frames[frameCount - 1]);
 					}
 					}
 				}
 				}
 			}
 			}
@@ -835,7 +854,6 @@ namespace Spine {
 					timeline.SetFrame(i, time, drawOrder);
 					timeline.SetFrame(i, time, drawOrder);
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[drawOrderCount - 1]);
 			}
 			}
 
 
 			// Event timeline.
 			// Event timeline.
@@ -845,34 +863,75 @@ namespace Spine {
 				for (int i = 0; i < eventCount; i++) {
 				for (int i = 0; i < eventCount; i++) {
 					float time = input.ReadFloat();
 					float time = input.ReadFloat();
 					EventData eventData = skeletonData.events.Items[input.ReadInt(true)];
 					EventData eventData = skeletonData.events.Items[input.ReadInt(true)];
-					Event e = new Event(time, eventData) {
-						Int = input.ReadInt(false),
-						Float = input.ReadFloat(),
-						String = input.ReadBoolean() ? input.ReadString() : eventData.String
-					};
-					if (e.data.AudioPath != null) {
+					Event e = new Event(time, eventData);
+					e.intValue = input.ReadInt(false);
+					e.floatValue = input.ReadFloat();
+					e.stringValue = input.ReadBoolean() ? input.ReadString() : eventData.String;
+					if (e.Data.AudioPath != null) {
 						e.volume = input.ReadFloat();
 						e.volume = input.ReadFloat();
 						e.balance = input.ReadFloat();
 						e.balance = input.ReadFloat();
 					}
 					}
 					timeline.SetFrame(i, e);
 					timeline.SetFrame(i, e);
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[eventCount - 1]);
 			}
 			}
 
 
-			timelines.TrimExcess();
+			float duration = 0;
+			var items = timelines.Items;
+			for (int i = 0, n = timelines.Count; i < n; i++)
+				duration = Math.Max(duration, items[i].Duration);
 			return new Animation(name, timelines, duration);
 			return new Animation(name, timelines, duration);
 		}
 		}
 
 
-		private void ReadCurve (SkeletonInput input, int frameIndex, CurveTimeline timeline) {
-			switch (input.ReadByte()) {
-			case CURVE_STEPPED:
-				timeline.SetStepped(frameIndex);
-				break;
-			case CURVE_BEZIER:
-				timeline.SetCurve(frameIndex, input.ReadFloat(), input.ReadFloat(), input.ReadFloat(), input.ReadFloat());
-				break;
+		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
+		private Timeline ReadTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) {
+			float time = input.ReadFloat(), value = input.ReadFloat() * scale;
+			for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1;; frame++) {
+				timeline.SetFrame(frame, time, value);
+				if (frame == frameLast) break;
+				float time2 = input.ReadFloat(), value2 = input.ReadFloat() * scale;
+				switch (input.ReadByte()) {
+				case CURVE_STEPPED:
+					timeline.SetStepped(frame);
+					break;
+				case CURVE_BEZIER:
+					SetBezier (input, timeline, bezier++, frame, 0, time, time2, value, value2, 1);
+						break;
+				}
+				time = time2;
+				value = value2;
+			}
+			return timeline;
+		}
+
+		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
+		private Timeline ReadTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) {
+			float time = input.ReadFloat(), value1 = input.ReadFloat() * scale, value2 = input.ReadFloat() * scale;
+			for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1;; frame++) {
+				timeline.SetFrame(frame, time, value1, value2);
+				if (frame == frameLast) break;
+				float time2 = input.ReadFloat(), nvalue1 = input.ReadFloat() * scale, nvalue2 = input.ReadFloat() * scale;
+				switch (input.ReadByte()) {
+					case CURVE_STEPPED:
+						timeline.SetStepped(frame);
+						break;
+					case CURVE_BEZIER:
+						SetBezier(input, timeline, bezier++, frame, 0, time, time2, value1, nvalue1, scale);
+						SetBezier(input, timeline, bezier++, frame, 1, time, time2, value2, nvalue2, scale);
+						break;
+				}
+				time = time2;
+				value1 = nvalue1;
+				value2 = nvalue2;
 			}
 			}
+			return timeline;
+		}
+
+		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
+		void SetBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
+			float value1, float value2, float scale) {
+			timeline.SetBezier(bezier, frame, value, time1, value1, input.ReadFloat(), input.ReadFloat() * scale, input.ReadFloat(),
+					input.ReadFloat() * scale, time2, value2);
 		}
 		}
 
 
 		internal class Vertices
 		internal class Vertices
@@ -883,14 +942,18 @@ namespace Spine {
 
 
 		internal class SkeletonInput {
 		internal class SkeletonInput {
 			private byte[] chars = new byte[32];
 			private byte[] chars = new byte[32];
-			private byte[] bytesBigEndian = new byte[4];
-			internal ExposedList<String> strings;
+			private byte[] bytesBigEndian = new byte[8];
+			internal string[] strings;
 			Stream input;
 			Stream input;
 
 
 			public SkeletonInput (Stream input) {
 			public SkeletonInput (Stream input) {
 				this.input = input;
 				this.input = input;
 			}
 			}
 
 
+			public int Read () {
+				return input.ReadByte();
+			}
+
 			public byte ReadByte () {
 			public byte ReadByte () {
 				return (byte)input.ReadByte();
 				return (byte)input.ReadByte();
 			}
 			}
@@ -922,6 +985,18 @@ namespace Spine {
 					+ bytesBigEndian[3];
 					+ bytesBigEndian[3];
 			}
 			}
 
 
+			public long ReadLong () {
+				input.Read(bytesBigEndian, 0, 8);
+				return ((long)(bytesBigEndian[0]) << 56)
+					+ ((long)(bytesBigEndian[1]) << 48)
+					+ ((long)(bytesBigEndian[2]) << 40)
+					+ ((long)(bytesBigEndian[3]) << 32)
+					+ ((long)(bytesBigEndian[4]) << 24)
+					+ ((long)(bytesBigEndian[5]) << 16)
+					+ ((long)(bytesBigEndian[6]) << 8)
+					+ (long)(bytesBigEndian[7]);
+			}
+
 			public int ReadInt (bool optimizePositive) {
 			public int ReadInt (bool optimizePositive) {
 				int b = input.ReadByte();
 				int b = input.ReadByte();
 				int result = b & 0x7F;
 				int result = b & 0x7F;
@@ -959,7 +1034,7 @@ namespace Spine {
 			///<return>May be null.</return>
 			///<return>May be null.</return>
 			public String ReadStringRef () {
 			public String ReadStringRef () {
 				int index = ReadInt(true);
 				int index = ReadInt(true);
-				return index == 0 ? null : strings.Items[index - 1];
+				return index == 0 ? null : strings[index - 1];
 			}
 			}
 
 
 			public void ReadFully (byte[] buffer, int offset, int length) {
 			public void ReadFully (byte[] buffer, int offset, int length) {
@@ -974,20 +1049,9 @@ namespace Spine {
 			/// <summary>Returns the version string of binary skeleton data.</summary>
 			/// <summary>Returns the version string of binary skeleton data.</summary>
 			public string GetVersionString () {
 			public string GetVersionString () {
 				try {
 				try {
-					// Hash.
-					int byteCount = ReadInt(true);
-					if (byteCount > 1) input.Position += byteCount - 1;
-
-					// Version.
-					byteCount = ReadInt(true);
-					if (byteCount > 1) {
-						byteCount--;
-						var buffer = new byte[byteCount];
-						ReadFully(buffer, 0, byteCount);
-						return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount);
-					}
-
-					throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.", "input");
+					ReadLong(); // long hash
+					string version = ReadString();
+					return version;
 				} catch (Exception e) {
 				} catch (Exception e) {
 					throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.\n" + e, "input");
 					throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.\n" + e, "input");
 				}
 				}

+ 2 - 2
spine-csharp/src/SkeletonBounds.cs

@@ -176,7 +176,7 @@ namespace Spine {
 		}
 		}
 
 
 		/// <summary>Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
 		/// <summary>Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
-		/// efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true.</summary>
+		/// efficient to only call this method if <see cref="AabbContainsPoint(float, float)"/> returns true.</summary>
 		public BoundingBoxAttachment ContainsPoint (float x, float y) {
 		public BoundingBoxAttachment ContainsPoint (float x, float y) {
 			ExposedList<Polygon> polygons = Polygons;
 			ExposedList<Polygon> polygons = Polygons;
 			for (int i = 0, n = polygons.Count; i < n; i++)
 			for (int i = 0, n = polygons.Count; i < n; i++)
@@ -185,7 +185,7 @@ namespace Spine {
 		}
 		}
 
 
 		/// <summary>Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually
 		/// <summary>Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually
-		/// more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns true.</summary>
+		/// more efficient to only call this method if <see cref="aabbIntersectsSegment(float, float, float, float)"/> returns true.</summary>
 		public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) {
 		public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) {
 			ExposedList<Polygon> polygons = Polygons;
 			ExposedList<Polygon> polygons = Polygons;
 			for (int i = 0, n = polygons.Count; i < n; i++)
 			for (int i = 0, n = polygons.Count; i < n; i++)

+ 29 - 27
spine-csharp/src/SkeletonData.cs

@@ -50,6 +50,8 @@ namespace Spine {
 		internal float fps;
 		internal float fps;
 		internal string imagesPath, audioPath;
 		internal string imagesPath, audioPath;
 
 
+		///<summary>The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
+		///set.</summary>
 		public string Name { get { return name; } set { name = value; } }
 		public string Name { get { return name; } set { name = value; } }
 
 
 		/// <summary>The skeleton's bones, sorted parent first. The root bone is always the first bone.</summary>
 		/// <summary>The skeleton's bones, sorted parent first. The root bone is always the first bone.</summary>
@@ -79,16 +81,18 @@ namespace Spine {
 		public float Height { get { return height; } set { height = value; } }
 		public float Height { get { return height; } set { height = value; } }
 		/// <summary>The Spine version used to export this data, or null.</summary>
 		/// <summary>The Spine version used to export this data, or null.</summary>
 		public string Version { get { return version; } set { version = value; } }
 		public string Version { get { return version; } set { version = value; } }
+
+		///<summary>The skeleton data hash. This value will change if any of the skeleton data has changed.
+		///May be null.</summary>
 		public string Hash { get { return hash; } set { hash = value; } }
 		public string Hash { get { return hash; } set { hash = value; } }
 
 
-		/// <summary>The path to the images directory as defined in Spine. Available only when nonessential data was exported. May be null</summary>
 		public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } }
 		public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } }
 
 
-		/// <summary>The path to the audio directory defined in Spine. Available only when nonessential data was exported. May be null.</summary>
+		/// <summary> The path to the audio directory as defined in Spine. Available only when nonessential data was exported.
+		/// May be null.</summary>
 		public string AudioPath { get { return audioPath; } set { audioPath = value; } }
 		public string AudioPath { get { return audioPath; } set { audioPath = value; } }
 
 
-		/// <summary>
-		/// The dopesheet FPS in Spine. Available only when nonessential data was exported.</summary>
+		/// <summary>The dopesheet FPS in Spine, or zero if nonessential data was not exported.</summary>
 		public float Fps { get { return fps; } set { fps = value; } }
 		public float Fps { get { return fps; } set { fps = value; } }
 
 
 		// --- Bones.
 		// --- Bones.
@@ -99,10 +103,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public BoneData FindBone (string boneName) {
 		public BoneData FindBone (string boneName) {
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
-			var bones = this.bones;
-			var bonesItems = bones.Items;
-			for (int i = 0, n = bones.Count; i < n; i++) {
-				BoneData bone = bonesItems[i];
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++) {
+				BoneData bone = bones[i];
 				if (bone.name == boneName) return bone;
 				if (bone.name == boneName) return bone;
 			}
 			}
 			return null;
 			return null;
@@ -111,10 +114,9 @@ namespace Spine {
 		/// <returns>-1 if the bone was not found.</returns>
 		/// <returns>-1 if the bone was not found.</returns>
 		public int FindBoneIndex (string boneName) {
 		public int FindBoneIndex (string boneName) {
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
 			if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
-			var bones = this.bones;
-			var bonesItems = bones.Items;
-			for (int i = 0, n = bones.Count; i < n; i++)
-				if (bonesItems[i].name == boneName) return i;
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++)
+				if (bones[i].name == boneName) return i;
 			return -1;
 			return -1;
 		}
 		}
 
 
@@ -123,9 +125,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public SlotData FindSlot (string slotName) {
 		public SlotData FindSlot (string slotName) {
 			if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
 			if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
-			ExposedList<SlotData> slots = this.slots;
-			for (int i = 0, n = slots.Count; i < n; i++) {
-				SlotData slot = slots.Items[i];
+			var slots = this.slots.Items;
+			for (int i = 0, n = this.slots.Count; i < n; i++) {
+				SlotData slot = slots[i];
 				if (slot.name == slotName) return slot;
 				if (slot.name == slotName) return slot;
 			}
 			}
 			return null;
 			return null;
@@ -165,9 +167,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public Animation FindAnimation (string animationName) {
 		public Animation FindAnimation (string animationName) {
 			if (animationName == null) throw new ArgumentNullException("animationName", "animationName cannot be null.");
 			if (animationName == null) throw new ArgumentNullException("animationName", "animationName cannot be null.");
-			ExposedList<Animation> animations = this.animations;
-			for (int i = 0, n = animations.Count; i < n; i++) {
-				Animation animation = animations.Items[i];
+			var animations = this.animations.Items;
+			for (int i = 0, n = this.animations.Count; i < n; i++) {
+				Animation animation = animations[i];
 				if (animation.name == animationName) return animation;
 				if (animation.name == animationName) return animation;
 			}
 			}
 			return null;
 			return null;
@@ -178,9 +180,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public IkConstraintData FindIkConstraint (string constraintName) {
 		public IkConstraintData FindIkConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<IkConstraintData> ikConstraints = this.ikConstraints;
-			for (int i = 0, n = ikConstraints.Count; i < n; i++) {
-				IkConstraintData ikConstraint = ikConstraints.Items[i];
+			var ikConstraints = this.ikConstraints.Items;
+			for (int i = 0, n = this.ikConstraints.Count; i < n; i++) {
+				IkConstraintData ikConstraint = ikConstraints[i];
 				if (ikConstraint.name == constraintName) return ikConstraint;
 				if (ikConstraint.name == constraintName) return ikConstraint;
 			}
 			}
 			return null;
 			return null;
@@ -191,9 +193,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public TransformConstraintData FindTransformConstraint (string constraintName) {
 		public TransformConstraintData FindTransformConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<TransformConstraintData> transformConstraints = this.transformConstraints;
-			for (int i = 0, n = transformConstraints.Count; i < n; i++) {
-				TransformConstraintData transformConstraint = transformConstraints.Items[i];
+			var transformConstraints = this.transformConstraints.Items;
+			for (int i = 0, n = this.transformConstraints.Count; i < n; i++) {
+				TransformConstraintData transformConstraint = transformConstraints[i];
 				if (transformConstraint.name == constraintName) return transformConstraint;
 				if (transformConstraint.name == constraintName) return transformConstraint;
 			}
 			}
 			return null;
 			return null;
@@ -204,9 +206,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		/// <returns>May be null.</returns>
 		public PathConstraintData FindPathConstraint (string constraintName) {
 		public PathConstraintData FindPathConstraint (string constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			ExposedList<PathConstraintData> pathConstraints = this.pathConstraints;
-			for (int i = 0, n = pathConstraints.Count; i < n; i++) {
-				PathConstraintData constraint = pathConstraints.Items[i];
+			var pathConstraints = this.pathConstraints.Items;
+			for (int i = 0, n = this.pathConstraints.Count; i < n; i++) {
+				PathConstraintData constraint = pathConstraints[i];
 				if (constraint.name.Equals(constraintName)) return constraint;
 				if (constraint.name.Equals(constraintName)) return constraint;
 			}
 			}
 			return null;
 			return null;

+ 333 - 195
spine-csharp/src/SkeletonJson.cs

@@ -41,23 +41,27 @@ using Windows.Storage;
 #endif
 #endif
 
 
 namespace Spine {
 namespace Spine {
-	public class SkeletonJson {
-		public float Scale { get; set; }
 
 
-		private AttachmentLoader attachmentLoader;
-		private List<LinkedMesh> linkedMeshes = new List<LinkedMesh>();
-
-		public SkeletonJson (params Atlas[] atlasArray)
-			: this(new AtlasAttachmentLoader(atlasArray)) {
+	/// <summary>
+	/// Loads skeleton data in the Spine JSON format.
+	/// <para>
+	/// JSON is human readable but the binary format is much smaller on disk and faster to load. See <see cref="SkeletonBinary"/>.</para>
+	/// <para>
+	/// See <a href="http://esotericsoftware.com/spine-json-format">Spine JSON format</a> and
+	/// <a href = "http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data" > JSON and binary data</a> in the Spine
+	/// Runtimes Guide.</para>
+	/// </summary>
+	public class SkeletonJson : SkeletonLoader {
+
+		public SkeletonJson (AttachmentLoader attachmentLoader)
+			: base(attachmentLoader) {
 		}
 		}
 
 
-		public SkeletonJson (AttachmentLoader attachmentLoader) {
-			if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader", "attachmentLoader cannot be null.");
-			this.attachmentLoader = attachmentLoader;
-			Scale = 1;
+		public SkeletonJson (params Atlas[] atlasArray)
+			: base(atlasArray) {
 		}
 		}
 
 
-		#if !IS_UNITY && WINDOWS_STOREAPP
+#if !IS_UNITY && WINDOWS_STOREAPP
 		private async Task<SkeletonData> ReadFile(string path) {
 		private async Task<SkeletonData> ReadFile(string path) {
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false);
 			var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false);
@@ -68,11 +72,11 @@ namespace Spine {
 			}
 			}
 		}
 		}
 
 
-		public SkeletonData ReadSkeletonData (string path) {
+		public override SkeletonData ReadSkeletonData (string path) {
 			return this.ReadFile(path).Result;
 			return this.ReadFile(path).Result;
 		}
 		}
-		#else
-		public SkeletonData ReadSkeletonData (string path) {
+#else
+		public override SkeletonData ReadSkeletonData (string path) {
 		#if WINDOWS_PHONE
 		#if WINDOWS_PHONE
 			using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 			using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 		#else
 		#else
@@ -88,7 +92,7 @@ namespace Spine {
 		public SkeletonData ReadSkeletonData (TextReader reader) {
 		public SkeletonData ReadSkeletonData (TextReader reader) {
 			if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null.");
 			if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null.");
 
 
-			float scale = this.Scale;
+			float scale = this.scale;
 			var skeletonData = new SkeletonData();
 			var skeletonData = new SkeletonData();
 
 
 			var root = Json.Deserialize(reader) as Dictionary<string, Object>;
 			var root = Json.Deserialize(reader) as Dictionary<string, Object>;
@@ -99,8 +103,6 @@ namespace Spine {
 				var skeletonMap = (Dictionary<string, Object>)root["skeleton"];
 				var skeletonMap = (Dictionary<string, Object>)root["skeleton"];
 				skeletonData.hash = (string)skeletonMap["hash"];
 				skeletonData.hash = (string)skeletonMap["hash"];
 				skeletonData.version = (string)skeletonMap["spine"];
 				skeletonData.version = (string)skeletonMap["spine"];
-				if ("3.8.75" == skeletonData.version)
-					throw new Exception("Unsupported skeleton data, please export with a newer version of Spine.");
 				skeletonData.x = GetFloat(skeletonMap, "x", 0);
 				skeletonData.x = GetFloat(skeletonMap, "x", 0);
 				skeletonData.y = GetFloat(skeletonMap, "y", 0);
 				skeletonData.y = GetFloat(skeletonMap, "y", 0);
 				skeletonData.width = GetFloat(skeletonMap, "width", 0);
 				skeletonData.width = GetFloat(skeletonMap, "width", 0);
@@ -283,6 +285,7 @@ namespace Spine {
 							skin.bones.Add(bone);
 							skin.bones.Add(bone);
 						}
 						}
 					}
 					}
+					skin.bones.TrimExcess();
 					if (skinMap.ContainsKey("ik")) {
 					if (skinMap.ContainsKey("ik")) {
 						foreach (string entryName in (List<Object>)skinMap["ik"]) {
 						foreach (string entryName in (List<Object>)skinMap["ik"]) {
 							IkConstraintData constraint = skeletonData.FindIkConstraint(entryName);
 							IkConstraintData constraint = skeletonData.FindIkConstraint(entryName);
@@ -304,6 +307,7 @@ namespace Spine {
 							skin.constraints.Add(constraint);
 							skin.constraints.Add(constraint);
 						}
 						}
 					}
 					}
+					skin.constraints.TrimExcess();
 					if (skinMap.ContainsKey("attachments")) {
 					if (skinMap.ContainsKey("attachments")) {
 						foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap["attachments"]) {
 						foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap["attachments"]) {
 							int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
 							int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
@@ -358,7 +362,7 @@ namespace Spine {
 					try {
 					try {
 						ReadAnimation((Dictionary<string, Object>)entry.Value, entry.Key, skeletonData);
 						ReadAnimation((Dictionary<string, Object>)entry.Value, entry.Key, skeletonData);
 					} catch (Exception e) {
 					} catch (Exception e) {
-						throw new Exception("Error reading animation: " + entry.Key, e);
+						throw new Exception("Error reading animation: " + entry.Key + "\n" + e.Message, e);
 					}
 					}
 				}
 				}
 			}
 			}
@@ -373,7 +377,7 @@ namespace Spine {
 		}
 		}
 
 
 		private Attachment ReadAttachment (Dictionary<string, Object> map, Skin skin, int slotIndex, string name, SkeletonData skeletonData) {
 		private Attachment ReadAttachment (Dictionary<string, Object> map, Skin skin, int slotIndex, string name, SkeletonData skeletonData) {
-			float scale = this.Scale;
+			float scale = this.scale;
 			name = GetString(map, "name", name);
 			name = GetString(map, "name", name);
 
 
 			var typeName = GetString(map, "type", "region");
 			var typeName = GetString(map, "type", "region");
@@ -438,7 +442,7 @@ namespace Spine {
 					mesh.regionUVs = uvs;
 					mesh.regionUVs = uvs;
 					mesh.UpdateUVs();
 					mesh.UpdateUVs();
 
 
-					if (map.ContainsKey("hull")) mesh.HullLength = GetInt(map, "hull", 0) * 2;
+					if (map.ContainsKey("hull")) mesh.HullLength = GetInt(map, "hull", 0) << 1;
 					if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges");
 					if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges");
 					return mesh;
 					return mesh;
 				}
 				}
@@ -505,7 +509,7 @@ namespace Spine {
 			for (int i = 0, n = vertices.Length; i < n;) {
 			for (int i = 0, n = vertices.Length; i < n;) {
 				int boneCount = (int)vertices[i++];
 				int boneCount = (int)vertices[i++];
 				bones.Add(boneCount);
 				bones.Add(boneCount);
-				for (int nn = i + boneCount * 4; i < nn; i += 4) {
+				for (int nn = i + (boneCount << 2); i < nn; i += 4) {
 					bones.Add((int)vertices[i]);
 					bones.Add((int)vertices[i]);
 					weights.Add(vertices[i + 1] * this.Scale);
 					weights.Add(vertices[i + 1] * this.Scale);
 					weights.Add(vertices[i + 2] * this.Scale);
 					weights.Add(vertices[i + 2] * this.Scale);
@@ -517,9 +521,8 @@ namespace Spine {
 		}
 		}
 
 
 		private void ReadAnimation (Dictionary<string, Object> map, string name, SkeletonData skeletonData) {
 		private void ReadAnimation (Dictionary<string, Object> map, string name, SkeletonData skeletonData) {
-			var scale = this.Scale;
+			var scale = this.scale;
 			var timelines = new ExposedList<Timeline>();
 			var timelines = new ExposedList<Timeline>();
-			float duration = 0;
 
 
 			// Slot timelines.
 			// Slot timelines.
 			if (map.ContainsKey("slots")) {
 			if (map.ContainsKey("slots")) {
@@ -529,50 +532,117 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
 						var values = (List<Object>)timelineEntry.Value;
+						if (values.Count == 0) continue;
 						var timelineName = (string)timelineEntry.Key;
 						var timelineName = (string)timelineEntry.Key;
 						if (timelineName == "attachment") {
 						if (timelineName == "attachment") {
-							var timeline = new AttachmentTimeline(values.Count);
-							timeline.slotIndex = slotIndex;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								float time = GetFloat(valueMap, "time", 0);
-								timeline.SetFrame(frameIndex++, time, (string)valueMap["name"]);
+							var timeline = new AttachmentTimeline(values.Count, slotIndex);
+							int frame = 0;
+							foreach (Dictionary<string, Object> keyMap in values) {
+								timeline.SetFrame(frame++, GetFloat(keyMap, "time", 0), (string)keyMap["name"]);
 							}
 							}
 							timelines.Add(timeline);
 							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 
 
 						} else if (timelineName == "color") {
 						} else if (timelineName == "color") {
-							var timeline = new ColorTimeline(values.Count);
-							timeline.slotIndex = slotIndex;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								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);
-								frameIndex++;
+							var timeline = new ColorTimeline(values.Count, values.Count << 2, slotIndex);
+
+							var keyMapEnumerator = values.GetEnumerator();
+							keyMapEnumerator.MoveNext();
+							var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+							float time = GetFloat(keyMap, "time", 0);
+							string color = (string)keyMap["color"];
+							float r = ToColor(color, 0);
+							float g = ToColor(color, 1);
+							float b = ToColor(color, 2);
+							float a = ToColor(color, 3);
+							for (int frame = 0, bezier = 0;; frame++) {
+								timeline.SetFrame(frame, time, r, g, b, a);
+								bool hasNext = keyMapEnumerator.MoveNext();
+								if (!hasNext) {
+									timeline.Shrink(bezier);
+									break;
+								}
+								var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+
+								float time2 = GetFloat(nextMap, "time", 0);
+								color = (string)nextMap["color"];
+								float nr = ToColor(color, 0);
+								float ng = ToColor(color, 1);
+								float nb = ToColor(color, 2);
+								float na = ToColor(color, 3);
+
+								if (keyMap.ContainsKey("curve")) {
+									object curve = keyMap["curve"];
+									bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
+								}
+								time = time2;
+								r = nr;
+								g = ng;
+								b = nb;
+								a = na;
+								keyMap = nextMap;
 							}
 							}
 							timelines.Add(timeline);
 							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * ColorTimeline.ENTRIES]);
 
 
 						} else if (timelineName == "twoColor") {
 						} else if (timelineName == "twoColor") {
-							var timeline = new TwoColorTimeline(values.Count);
-							timeline.slotIndex = slotIndex;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								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),
-									ToColor(dark, 0, 6), ToColor(dark, 1, 6), ToColor(dark, 2, 6));
-								ReadCurve(valueMap, timeline, frameIndex);
-								frameIndex++;
+							var timeline = new TwoColorTimeline(values.Count, values.Count * 7, slotIndex);
+
+							var keyMapEnumerator = values.GetEnumerator();
+							keyMapEnumerator.MoveNext();
+							var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+							float time = GetFloat(keyMap, "time", 0);
+							string color = (string)keyMap["light"];
+							float r = ToColor(color, 0);
+							float g = ToColor(color, 1);
+							float b = ToColor(color, 2);
+							float a = ToColor(color, 3);
+							color = (string)keyMap["dark"];
+							float r2 = ToColor(color, 0);
+							float g2 = ToColor(color, 1);
+							float b2 = ToColor(color, 2);
+							for (int frame = 0, bezier = 0; ; frame++) {
+								timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2);
+								bool hasNext = keyMapEnumerator.MoveNext();
+								if (!hasNext) {
+									timeline.Shrink(bezier);
+									break;
+								}
+								var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+
+								float time2 = GetFloat(nextMap, "time", 0);
+								color = (string)nextMap["light"];
+								float nr = ToColor(color, 0);
+								float ng = ToColor(color, 1);
+								float nb = ToColor(color, 2);
+								float na = ToColor(color, 3);
+								color = (string)nextMap["dark"];
+								float nr2 = ToColor(color, 0);
+								float ng2 = ToColor(color, 1);
+								float nb2 = ToColor(color, 2);
+
+								if (keyMap.ContainsKey("curve")) {
+									object curve = keyMap["curve"];
+									bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1);
+									bezier = ReadCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1);
+								}
+								time = time2;
+								r = nr;
+								g = ng;
+								b = nb;
+								a = na;
+								r2 = nr2;
+								g2 = ng2;
+								b2 = nb2;
+								keyMap = nextMap;
 							}
 							}
 							timelines.Add(timeline);
 							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TwoColorTimeline.ENTRIES]);
 
 
 						} else
 						} else
 							throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
 							throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
@@ -589,47 +659,23 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
 						var values = (List<Object>)timelineEntry.Value;
+						var keyMapEnumerator = values.GetEnumerator();
+						bool hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) continue;
 						var timelineName = (string)timelineEntry.Key;
 						var timelineName = (string)timelineEntry.Key;
-						if (timelineName == "rotate") {
-							var timeline = new RotateTimeline(values.Count);
-							timeline.boneIndex = boneIndex;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "angle", 0));
-								ReadCurve(valueMap, timeline, frameIndex);
-								frameIndex++;
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * RotateTimeline.ENTRIES]);
-
-						} else if (timelineName == "translate" || timelineName == "scale" || timelineName == "shear") {
-							TranslateTimeline timeline;
-							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 {
-								timeline = new TranslateTimeline(values.Count);
-								timelineScale = scale;
-							}
-							timeline.boneIndex = boneIndex;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								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++;
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TranslateTimeline.ENTRIES]);
-
+						if (timelineName == "rotate")
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, new RotateTimeline(values.Count, values.Count, boneIndex), 0, 1));
+						else if (timelineName == "translate") {
+							TranslateTimeline timeline = new TranslateTimeline(values.Count, values.Count << 1, boneIndex);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, scale));
+						}
+						else if (timelineName == "scale") {
+							ScaleTimeline timeline = new ScaleTimeline(values.Count, values.Count << 1, boneIndex);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 1, 1));
+						}
+						else if (timelineName == "shear") {
+							ShearTimeline timeline = new ShearTimeline(values.Count, values.Count << 1, boneIndex);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, 1));
 						} else
 						} else
 							throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
 							throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
 					}
 					}
@@ -638,40 +684,82 @@ namespace Spine {
 
 
 			// IK constraint timelines.
 			// IK constraint timelines.
 			if (map.ContainsKey("ik")) {
 			if (map.ContainsKey("ik")) {
-				foreach (KeyValuePair<string, Object> constraintMap in (Dictionary<string, Object>)map["ik"]) {
-					IkConstraintData constraint = skeletonData.FindIkConstraint(constraintMap.Key);
-					var values = (List<Object>)constraintMap.Value;
-					var timeline = new IkConstraintTimeline(values.Count);
-					timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(constraint);
-					int frameIndex = 0;
-					foreach (Dictionary<string, Object> valueMap in values) {
-						timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "mix", 1),
-							GetFloat(valueMap, "softness", 0) * scale, GetBoolean(valueMap, "bendPositive", true) ? 1 : -1,
-							GetBoolean(valueMap, "compress", false), GetBoolean(valueMap, "stretch", false));
-						ReadCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
+				foreach (KeyValuePair<string, Object> timelineMap in (Dictionary<string, Object>)map["ik"]) {
+					var timelineMapValues = (List<Object>)timelineMap.Value;
+					var keyMapEnumerator = timelineMapValues.GetEnumerator();
+					bool hasNext = keyMapEnumerator.MoveNext();
+					if (!hasNext) continue;
+					var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+					IkConstraintData constraint = skeletonData.FindIkConstraint(timelineMap.Key);
+					IkConstraintTimeline timeline = new IkConstraintTimeline(timelineMapValues.Count, timelineMapValues.Count << 1,
+						skeletonData.IkConstraints.IndexOf(constraint));
+					float time = GetFloat(keyMap, "time", 0);
+					float mix = GetFloat(keyMap, "mix", 1), softness = GetFloat(keyMap, "softness", 0) * scale;
+					for (int frame = 0, bezier = 0; ; frame++) {
+						timeline.SetFrame(frame, time, mix, softness, GetBoolean(keyMap, "bendPositive", true) ? 1 : -1,
+							GetBoolean(keyMap, "compress", false), GetBoolean(keyMap, "stretch", false));
+						hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) {
+							timeline.Shrink(bezier);
+							break;
+						}
+						var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+						float time2 = GetFloat(nextMap, "time", 0);
+						float mix2 = GetFloat(nextMap, "mix", 1), softness2 = GetFloat(nextMap, "softness", 0) * scale;
+						if (keyMap.ContainsKey("curve")) {
+							object curve = keyMap["curve"];
+							bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1);
+							bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale);
+						}
+						time = time2;
+						mix = mix2;
+						softness = softness2;
+						keyMap = nextMap;
 					}
 					}
 					timelines.Add(timeline);
 					timelines.Add(timeline);
-					duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * IkConstraintTimeline.ENTRIES]);
 				}
 				}
 			}
 			}
 
 
 			// Transform constraint timelines.
 			// Transform constraint timelines.
 			if (map.ContainsKey("transform")) {
 			if (map.ContainsKey("transform")) {
-				foreach (KeyValuePair<string, Object> constraintMap in (Dictionary<string, Object>)map["transform"]) {
-					TransformConstraintData constraint = skeletonData.FindTransformConstraint(constraintMap.Key);
-					var values = (List<Object>)constraintMap.Value;
-					var timeline = new TransformConstraintTimeline(values.Count);
-					timeline.transformConstraintIndex = skeletonData.transformConstraints.IndexOf(constraint);
-					int frameIndex = 0;
-					foreach (Dictionary<string, Object> valueMap in values) {
-						timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "rotateMix", 1),
-								GetFloat(valueMap, "translateMix", 1), GetFloat(valueMap, "scaleMix", 1), GetFloat(valueMap, "shearMix", 1));
-						ReadCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
+				foreach (KeyValuePair<string, Object> timelineMap in (Dictionary<string, Object>)map["transform"]) {
+					var timelineMapValues = (List<Object>)timelineMap.Value;
+					var keyMapEnumerator = timelineMapValues.GetEnumerator();
+					bool hasNext = keyMapEnumerator.MoveNext();
+					if (!hasNext) continue;
+					var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+					TransformConstraintData constraint = skeletonData.FindTransformConstraint(timelineMap.Key);
+					TransformConstraintTimeline timeline = new TransformConstraintTimeline(timelineMapValues.Count, timelineMapValues.Count << 2,
+						skeletonData.TransformConstraints.IndexOf(constraint));
+					float time = GetFloat(keyMap, "time", 0);
+					float rotateMix = GetFloat(keyMap, "rotateMix", 1), translateMix = GetFloat(keyMap, "translateMix", 1);
+					float scaleMix = GetFloat(keyMap, "scaleMix", 1), shearMix = GetFloat(keyMap, "shearMix", 1);
+					for (int frame = 0, bezier = 0; ; frame++) {
+						timeline.SetFrame(frame, time, rotateMix, translateMix, scaleMix, shearMix);
+						hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) {
+							timeline.Shrink(bezier);
+							break;
+						}
+						var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+						float time2 = GetFloat(nextMap, "time", 0);
+						float rotateMix2 = GetFloat(nextMap, "rotateMix", 1), translateMix2 = GetFloat(nextMap, "translateMix", 1);
+						float scaleMix2 = GetFloat(nextMap, "scaleMix", 1), shearMix2 = GetFloat(nextMap, "shearMix", 1);
+						if (keyMap.ContainsKey("curve")) {
+							object curve = keyMap["curve"];
+							bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, rotateMix, rotateMix2, 1);
+							bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, translateMix, translateMix2, 1);
+							bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, scaleMix, scaleMix2, 1);
+							bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, shearMix, shearMix2, 1);
+						}
+						time = time2;
+						rotateMix = rotateMix2;
+						translateMix = translateMix2;
+						scaleMix = scaleMix2;
+						shearMix = shearMix2;
+						keyMap = nextMap;
 					}
 					}
 					timelines.Add(timeline);
 					timelines.Add(timeline);
-					duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TransformConstraintTimeline.ENTRIES]);
 				}
 				}
 			}
 			}
 
 
@@ -684,40 +772,22 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)constraintMap.Value;
 					var timelineMap = (Dictionary<string, Object>)constraintMap.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
 						var values = (List<Object>)timelineEntry.Value;
+						var keyMapEnumerator = values.GetEnumerator();
+						bool hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) continue;
 						var timelineName = (string)timelineEntry.Key;
 						var timelineName = (string)timelineEntry.Key;
-						if (timelineName == "position" || timelineName == "spacing") {
-							PathConstraintPositionTimeline timeline;
-							float timelineScale = 1;
-							if (timelineName == "spacing") {
-								timeline = new PathConstraintSpacingTimeline(values.Count);
-								if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) timelineScale = scale;
-							}
-							else {
-								timeline = new PathConstraintPositionTimeline(values.Count);
-								if (data.positionMode == PositionMode.Fixed) timelineScale = scale;
-							}
-							timeline.pathConstraintIndex = index;
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, timelineName, 0) * timelineScale);
-								ReadCurve(valueMap, timeline, frameIndex);
-								frameIndex++;
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * PathConstraintPositionTimeline.ENTRIES]);
+						if (timelineName == "position") {
+							CurveTimeline1 timeline = new PathConstraintPositionTimeline(values.Count, values.Count, index);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, data.positionMode == PositionMode.Fixed ? scale : 1));
+						}
+						else if (timelineName == "spacing") {
+							CurveTimeline1 timeline = new PathConstraintSpacingTimeline(values.Count, values.Count, index);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0,
+								data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1));
 						}
 						}
 						else if (timelineName == "mix") {
 						else if (timelineName == "mix") {
-							PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(values.Count);
-							timeline.pathConstraintIndex = index;
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
-								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), GetFloat(valueMap, "rotateMix", 1),
-									GetFloat(valueMap, "translateMix", 1));
-								ReadCurve(valueMap, timeline, frameIndex);
-								frameIndex++;
-							}
-							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * PathConstraintMixTimeline.ENTRIES]);
+							CurveTimeline2 timeline = new PathConstraintMixTimeline(values.Count, values.Count << 1, index);
+							timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "rotateMix", "translateMix", 1, 1));
 						}
 						}
 					}
 					}
 				}
 				}
@@ -731,26 +801,26 @@ namespace Spine {
 						int slotIndex = skeletonData.FindSlotIndex(slotMap.Key);
 						int slotIndex = skeletonData.FindSlotIndex(slotMap.Key);
 						if (slotIndex == -1) throw new Exception("Slot not found: " + slotMap.Key);
 						if (slotIndex == -1) throw new Exception("Slot not found: " + slotMap.Key);
 						foreach (KeyValuePair<string, Object> timelineMap in (Dictionary<string, Object>)slotMap.Value) {
 						foreach (KeyValuePair<string, Object> timelineMap in (Dictionary<string, Object>)slotMap.Value) {
-							var values = (List<Object>)timelineMap.Value;
+							var timelineMapValues = (List<Object>)timelineMap.Value;
+							var keyMapEnumerator = timelineMapValues.GetEnumerator();
+							bool hasNext = keyMapEnumerator.MoveNext();
+							if (!hasNext) continue;
+							var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
 							VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, timelineMap.Key);
 							VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, timelineMap.Key);
 							if (attachment == null) throw new Exception("Deform attachment not found: " + timelineMap.Key);
 							if (attachment == null) throw new Exception("Deform attachment not found: " + timelineMap.Key);
 							bool weighted = attachment.bones != null;
 							bool weighted = attachment.bones != null;
 							float[] vertices = attachment.vertices;
 							float[] vertices = attachment.vertices;
-							int deformLength = weighted ? vertices.Length / 3 * 2 : vertices.Length;
-
-							var timeline = new DeformTimeline(values.Count);
-							timeline.slotIndex = slotIndex;
-							timeline.attachment = attachment;
-
-							int frameIndex = 0;
-							foreach (Dictionary<string, Object> valueMap in values) {
+							int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length;
+							DeformTimeline timeline = new DeformTimeline(timelineMapValues.Count, timelineMapValues.Count, slotIndex, attachment);
+							float time = GetFloat(keyMap, "time", 0);
+							for (int frame = 0, bezier = 0; ; frame++) {
 								float[] deform;
 								float[] deform;
-								if (!valueMap.ContainsKey("vertices")) {
+								if (!keyMap.ContainsKey("vertices")) {
 									deform = weighted ? new float[deformLength] : vertices;
 									deform = weighted ? new float[deformLength] : vertices;
 								} else {
 								} else {
 									deform = new float[deformLength];
 									deform = new float[deformLength];
-									int start = GetInt(valueMap, "offset", 0);
-									float[] verticesValue = GetFloatArray(valueMap, "vertices", 1);
+									int start = GetInt(keyMap, "offset", 0);
+									float[] verticesValue = GetFloatArray(keyMap, "vertices", 1);
 									Array.Copy(verticesValue, 0, deform, start, verticesValue.Length);
 									Array.Copy(verticesValue, 0, deform, start, verticesValue.Length);
 									if (scale != 1) {
 									if (scale != 1) {
 										for (int i = start, n = i + verticesValue.Length; i < n; i++)
 										for (int i = start, n = i + verticesValue.Length; i < n; i++)
@@ -763,12 +833,22 @@ namespace Spine {
 									}
 									}
 								}
 								}
 
 
-								timeline.SetFrame(frameIndex, GetFloat(valueMap, "time", 0), deform);
-								ReadCurve(valueMap, timeline, frameIndex);
-								frameIndex++;
+								timeline.SetFrame(frame, time, deform);
+								hasNext = keyMapEnumerator.MoveNext();
+								if (!hasNext) {
+									timeline.Shrink(bezier);
+									break;
+								}
+								var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+								float time2 = GetFloat(nextMap, "time", 0);
+								if (keyMap.ContainsKey("curve")) {
+									object curve = keyMap["curve"];
+									bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1);
+								}
+								time = time2;
+								keyMap = nextMap;
 							}
 							}
 							timelines.Add(timeline);
 							timelines.Add(timeline);
-							duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 						}
 						}
 					}
 					}
 				}
 				}
@@ -779,7 +859,7 @@ namespace Spine {
 				var values = (List<Object>)map[map.ContainsKey("drawOrder") ? "drawOrder" : "draworder"];
 				var values = (List<Object>)map[map.ContainsKey("drawOrder") ? "drawOrder" : "draworder"];
 				var timeline = new DrawOrderTimeline(values.Count);
 				var timeline = new DrawOrderTimeline(values.Count);
 				int slotCount = skeletonData.slots.Count;
 				int slotCount = skeletonData.slots.Count;
-				int frameIndex = 0;
+				int frame = 0;
 				foreach (Dictionary<string, Object> drawOrderMap in values) {
 				foreach (Dictionary<string, Object> drawOrderMap in values) {
 					int[] drawOrder = null;
 					int[] drawOrder = null;
 					if (drawOrderMap.ContainsKey("offsets")) {
 					if (drawOrderMap.ContainsKey("offsets")) {
@@ -806,17 +886,17 @@ namespace Spine {
 						for (int i = slotCount - 1; i >= 0; i--)
 						for (int i = slotCount - 1; i >= 0; i--)
 							if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
 							if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
 					}
 					}
-					timeline.SetFrame(frameIndex++, GetFloat(drawOrderMap, "time", 0), drawOrder);
+					timeline.SetFrame(frame, GetFloat(drawOrderMap, "time", 0), drawOrder);
+					++frame;
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 			}
 			}
 
 
 			// Event timeline.
 			// Event timeline.
 			if (map.ContainsKey("events")) {
 			if (map.ContainsKey("events")) {
 				var eventsMap = (List<Object>)map["events"];
 				var eventsMap = (List<Object>)map["events"];
 				var timeline = new EventTimeline(eventsMap.Count);
 				var timeline = new EventTimeline(eventsMap.Count);
-				int frameIndex = 0;
+				int frame = 0;
 				foreach (Dictionary<string, Object> eventMap in eventsMap) {
 				foreach (Dictionary<string, Object> eventMap in eventsMap) {
 					EventData eventData = skeletonData.FindEvent((string)eventMap["name"]);
 					EventData eventData = skeletonData.FindEvent((string)eventMap["name"]);
 					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
 					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
@@ -829,39 +909,97 @@ namespace Spine {
 						e.volume = GetFloat(eventMap, "volume", eventData.Volume);
 						e.volume = GetFloat(eventMap, "volume", eventData.Volume);
 						e.balance = GetFloat(eventMap, "balance", eventData.Balance);
 						e.balance = GetFloat(eventMap, "balance", eventData.Balance);
 					}
 					}
-					timeline.SetFrame(frameIndex++, e);
+					timeline.SetFrame(frame, e);
+					++frame;
 				}
 				}
 				timelines.Add(timeline);
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 			}
 			}
-
 			timelines.TrimExcess();
 			timelines.TrimExcess();
+			float duration = 0;
+			var items = timelines.Items;
+			for (int i = 0, n = timelines.Count; i < n; i++)
+				duration = Math.Max(duration, items[i].Duration);
 			skeletonData.animations.Add(new Animation(name, timelines, duration));
 			skeletonData.animations.Add(new Animation(name, timelines, duration));
 		}
 		}
 
 
-		static void ReadCurve (Dictionary<string, Object> valueMap, CurveTimeline timeline, int frameIndex) {
-			if (!valueMap.ContainsKey("curve"))
-				return;
-			Object curveObject = valueMap["curve"];
-			if (curveObject is string)
-				timeline.SetStepped(frameIndex);
-			else
-				timeline.SetCurve(frameIndex, (float)curveObject, GetFloat(valueMap, "c2", 0), GetFloat(valueMap, "c3", 1), GetFloat(valueMap, "c4", 1));
+		static Timeline ReadTimeline (ref List<object>.Enumerator keyMapEnumerator, CurveTimeline1 timeline, float defaultValue, float scale) {
+			var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+			float time = GetFloat(keyMap, "time", 0);
+			float value = GetFloat(keyMap, "value", defaultValue) * scale;
+			int bezier = 0;
+			for (int frame = 0; ; frame++) {
+				timeline.SetFrame(frame, time, value);
+				bool hasNext = keyMapEnumerator.MoveNext();
+				if (!hasNext)
+					break;
+				var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+				float time2 = GetFloat(nextMap, "time", 0);
+				float value2 = GetFloat(nextMap, "value", defaultValue) * scale;
+				if (keyMap.ContainsKey("curve")) {
+					object curve = keyMap["curve"];
+					bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale);
+				}
+				time = time2;
+				value = value2;
+				keyMap = nextMap;
+			}
+			timeline.Shrink(bezier);
+			return timeline;
 		}
 		}
 
 
-		internal class LinkedMesh {
-			internal string parent, skin;
-			internal int slotIndex;
-			internal MeshAttachment mesh;
-			internal bool inheritDeform;
-
-			public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritDeform) {
-				this.mesh = mesh;
-				this.skin = skin;
-				this.slotIndex = slotIndex;
-				this.parent = parent;
-				this.inheritDeform = inheritDeform;
+		static Timeline ReadTimeline (ref List<object>.Enumerator keyMapEnumerator, CurveTimeline2 timeline, String name1, String name2, float defaultValue,
+			float scale) {
+
+			var keyMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+			float time = GetFloat(keyMap, "time", 0);
+			float value1 = GetFloat(keyMap, name1, defaultValue) * scale, value2 = GetFloat(keyMap, name2, defaultValue) * scale;
+			int bezier = 0;
+			for (int frame = 0; ; frame++) {
+				timeline.SetFrame(frame, time, value1, value2);
+				bool hasNext = keyMapEnumerator.MoveNext();
+				if (!hasNext)
+					break;
+				var nextMap = (Dictionary<string, Object>)keyMapEnumerator.Current;
+				float time2 = GetFloat(nextMap, "time", 0);
+				float nvalue1 = GetFloat(nextMap, name1, defaultValue) * scale, nvalue2 = GetFloat(nextMap, name2, defaultValue) * scale;
+				if (keyMap.ContainsKey("curve")) {
+					object curve = keyMap["curve"];
+					bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale);
+					bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale);
+				}
+				time = time2;
+				value1 = nvalue1;
+				value2 = nvalue2;
+				keyMap = nextMap;
 			}
 			}
+			timeline.Shrink(bezier);
+			return timeline;
+		}
+
+		static int ReadCurve (object curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
+			float value1, float value2, float scale) {
+
+			if (curve is string) {
+				if (value != 0) timeline.SetStepped(frame);
+			}
+			else {
+				var curveValues = (List<object>)curve;
+				int index = value << 2;
+				float cx1 = (float)curveValues[index];
+				++index;
+				float cy1 = ((float)curveValues[index]) * scale;
+				++index;
+				float cx2 = (float)curveValues[index];
+				++index;
+				float cy2 = (float)curveValues[index] * scale;
+				SetBezier(timeline, frame, value, bezier++, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
+			}
+			return bezier;
+		}
+
+		static void SetBezier (CurveTimeline timeline, int frame, int value, int bezier, float time1, float value1, float cx1, float cy1,
+			float cx2, float cy2, float time2, float value2) {
+			timeline.SetBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
 		}
 		}
 
 
 		static float[] GetFloatArray(Dictionary<string, Object> map, string name, float scale) {
 		static float[] GetFloatArray(Dictionary<string, Object> map, string name, float scale) {

+ 92 - 0
spine-csharp/src/SkeletonLoader.cs

@@ -0,0 +1,92 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+
+namespace Spine {
+
+	/// <summary>
+	/// Base class for loading skeleton data from a file.
+	/// <para>
+	/// See<a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the
+	/// Spine Runtimes Guide.</para>
+	/// </summary>
+	public abstract class SkeletonLoader {
+		protected readonly AttachmentLoader attachmentLoader;
+		protected float scale = 1;
+		protected readonly List<LinkedMesh> linkedMeshes = new List<LinkedMesh>();
+
+		/// <summary>Creates a skeleton loader that loads attachments using an <see cref="AtlasAttachmentLoader"/> with the specified atlas.
+		/// </summary>
+		public SkeletonLoader (params Atlas[] atlasArray) {
+			attachmentLoader = new AtlasAttachmentLoader(atlasArray);
+		}
+
+		/// <summary>Creates a skeleton loader that loads attachments using the specified attachment loader.
+		/// <para>See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data'>Loading skeleton data</a> in the
+		/// Spine Runtimes Guide.</para></summary>
+		public SkeletonLoader (AttachmentLoader attachmentLoader) {
+			if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader", "attachmentLoader cannot be null.");
+			this.attachmentLoader = attachmentLoader;
+		}
+
+		/// <summary>Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at
+		/// runtime than were used in Spine.
+		/// <para>
+		/// See <a href="http://esotericsoftware.com/spine-loading-skeleton-data#Scaling">Scaling</a> in the Spine Runtimes Guide.</para>
+		/// </summary>
+		public float Scale {
+			get { return scale; }
+			set {
+				if (scale == 0) throw new ArgumentNullException("scale", "scale cannot be 0.");
+				this.scale = value;
+			}
+		}
+
+		public abstract SkeletonData ReadSkeletonData (string path);
+
+		protected class LinkedMesh {
+			internal string parent, skin;
+			internal int slotIndex;
+			internal MeshAttachment mesh;
+			internal bool inheritDeform;
+
+			public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritDeform) {
+				this.mesh = mesh;
+				this.skin = skin;
+				this.slotIndex = slotIndex;
+				this.parent = parent;
+				this.inheritDeform = inheritDeform;
+			}
+		}
+
+	}
+}

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

@@ -39,6 +39,8 @@ namespace Spine {
 	/// </summary>
 	/// </summary>
 	public class Skin {
 	public class Skin {
 		internal string name;
 		internal string name;
+		// Difference to reference implementation: using Dictionary<SkinKey, SkinEntry> instead of HashSet<SkinEntry>.
+		// Reason is that there is no efficient way to replace or access an already added element, losing any benefits.
 		private Dictionary<SkinKey, SkinEntry> attachments = new Dictionary<SkinKey, SkinEntry>(SkinKeyComparer.Instance);
 		private Dictionary<SkinKey, SkinEntry> attachments = new Dictionary<SkinKey, SkinEntry>(SkinKeyComparer.Instance);
 		internal readonly ExposedList<BoneData> bones = new ExposedList<BoneData>();
 		internal readonly ExposedList<BoneData> bones = new ExposedList<BoneData>();
 		internal readonly ExposedList<ConstraintData> constraints = new ExposedList<ConstraintData>();
 		internal readonly ExposedList<ConstraintData> constraints = new ExposedList<ConstraintData>();
@@ -58,7 +60,6 @@ namespace Spine {
 		/// If the name already exists for the slot, the previous value is replaced.</summary>
 		/// If the name already exists for the slot, the previous value is replaced.</summary>
 		public void SetAttachment (int slotIndex, string name, Attachment attachment) {
 		public void SetAttachment (int slotIndex, string name, Attachment attachment) {
 			if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null.");
 			if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null.");
-			if (slotIndex < 0) throw new ArgumentNullException("slotIndex", "slotIndex must be >= 0.");
 			attachments[new SkinKey(slotIndex, name)] = new SkinEntry(slotIndex, name, attachment);
 			attachments[new SkinKey(slotIndex, name)] = new SkinEntry(slotIndex, name, attachment);
 		}
 		}
 
 
@@ -104,13 +105,14 @@ namespace Spine {
 
 
 		/// <summary> Removes the attachment in the skin for the specified slot index and name, if any.</summary>
 		/// <summary> Removes the attachment in the skin for the specified slot index and name, if any.</summary>
 		public void RemoveAttachment (int slotIndex, string name) {
 		public void RemoveAttachment (int slotIndex, string name) {
-			if (slotIndex < 0) throw new ArgumentOutOfRangeException("slotIndex", "slotIndex must be >= 0");
 			attachments.Remove(new SkinKey(slotIndex, name));
 			attachments.Remove(new SkinKey(slotIndex, name));
 		}
 		}
 
 
 		/// <summary>Returns all attachments in this skin for the specified slot index.</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"/>
 		/// <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) {
 		public void GetAttachments (int slotIndex, List<SkinEntry> attachments) {
+			if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0.");
+			if (attachments == null) throw new ArgumentNullException("attachments", "attachments cannot be null.");
 			foreach (var item in this.attachments) {
 			foreach (var item in this.attachments) {
 				SkinEntry entry = item.Value;
 				SkinEntry entry = item.Value;
 				if (entry.slotIndex == slotIndex) attachments.Add(entry);
 				if (entry.slotIndex == slotIndex) attachments.Add(entry);
@@ -176,10 +178,14 @@ namespace Spine {
 		private struct SkinKey {
 		private struct SkinKey {
 			internal readonly int slotIndex;
 			internal readonly int slotIndex;
 			internal readonly string name;
 			internal readonly string name;
+			internal readonly int hashCode;
 
 
 			public SkinKey (int slotIndex, string name) {
 			public SkinKey (int slotIndex, string name) {
+				if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0.");
+				if (name == null) throw new ArgumentNullException("name", "name cannot be null");
 				this.slotIndex = slotIndex;
 				this.slotIndex = slotIndex;
 				this.name = name;
 				this.name = name;
+				this.hashCode = name.GetHashCode() + slotIndex * 37;
 			}
 			}
 		}
 		}
 
 
@@ -191,7 +197,7 @@ namespace Spine {
 			}
 			}
 
 
 			int IEqualityComparer<SkinKey>.GetHashCode (SkinKey e) {
 			int IEqualityComparer<SkinKey>.GetHashCode (SkinKey e) {
-				return e.name.GetHashCode() + e.slotIndex * 37;
+				return e.hashCode;
 			}
 			}
 		}
 		}
 	}
 	}

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

@@ -32,7 +32,7 @@ using System;
 namespace Spine {
 namespace Spine {
 
 
 	/// <summary>
 	/// <summary>
-	/// Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
+	/// Stores a slot's current pose. Slots organize attachments for <see cref="Skeleton.DrawOrder"/> purposes and provide a place to store
 	/// state for an attachment.State cannot be stored in an attachment itself because attachments are stateless and may be shared
 	/// state for an attachment.State cannot be stored in an attachment itself because attachments are stateless and may be shared
 	/// across multiple skeletons.
 	/// across multiple skeletons.
 	/// </summary>
 	/// </summary>

+ 13 - 27
spine-csharp/src/TransformConstraint.cs

@@ -76,12 +76,8 @@ namespace Spine {
 			shearMix = constraint.shearMix;
 			shearMix = constraint.shearMix;
 		}
 		}
 
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
 		public void Update () {
+			if (rotateMix == 0 && translateMix == 0 && scaleMix == 0 && shearMix == 0) return;
 			if (data.local) {
 			if (data.local) {
 				if (data.relative)
 				if (data.relative)
 					ApplyRelativeLocal();
 					ApplyRelativeLocal();
@@ -101,10 +97,9 @@ namespace Spine {
 			float ta = target.a, tb = target.b, tc = target.c, td = target.d;
 			float ta = target.a, tb = target.b, tc = target.c, td = target.d;
 			float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
 			float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
 			float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
 			float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
-			var bones = this.bones;
-			for (int i = 0, n = bones.Count; i < n; i++) {
-				Bone bone = bones.Items[i];
-				bool modified = false;
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++) {
+				Bone bone = bones[i];
 
 
 				if (rotateMix != 0) {
 				if (rotateMix != 0) {
 					float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
 					float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
@@ -118,7 +113,6 @@ namespace Spine {
 					bone.b = cos * b - sin * d;
 					bone.b = cos * b - sin * d;
 					bone.c = sin * a + cos * c;
 					bone.c = sin * a + cos * c;
 					bone.d = sin * b + cos * d;
 					bone.d = sin * b + cos * d;
-					modified = true;
 				}
 				}
 
 
 				if (translateMix != 0) {
 				if (translateMix != 0) {
@@ -126,7 +120,6 @@ namespace Spine {
 					target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY));
 					target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY));
 					bone.worldX += (tx - bone.worldX) * translateMix;
 					bone.worldX += (tx - bone.worldX) * translateMix;
 					bone.worldY += (ty - bone.worldY) * translateMix;
 					bone.worldY += (ty - bone.worldY) * translateMix;
-					modified = true;
 				}
 				}
 
 
 				if (scaleMix > 0) {
 				if (scaleMix > 0) {
@@ -138,7 +131,6 @@ namespace Spine {
 					if (s != 0) s = (s + ((float)Math.Sqrt(tb * tb + td * td) - s + data.offsetScaleY) * scaleMix) / s;
 					if (s != 0) s = (s + ((float)Math.Sqrt(tb * tb + td * td) - s + data.offsetScaleY) * scaleMix) / s;
 					bone.b *= s;
 					bone.b *= s;
 					bone.d *= s;
 					bone.d *= s;
-					modified = true;
 				}
 				}
 
 
 				if (shearMix > 0) {
 				if (shearMix > 0) {
@@ -152,10 +144,9 @@ namespace Spine {
 					float s = (float)Math.Sqrt(b * b + d * d);
 					float s = (float)Math.Sqrt(b * b + d * d);
 					bone.b = MathUtils.Cos(r) * s;
 					bone.b = MathUtils.Cos(r) * s;
 					bone.d = MathUtils.Sin(r) * s;
 					bone.d = MathUtils.Sin(r) * s;
-					modified = true;
 				}
 				}
 
 
-				if (modified) bone.appliedValid = false;
+				bone.appliedValid = false;
 			}
 			}
 		}
 		}
 
 
@@ -165,10 +156,9 @@ namespace Spine {
 			float ta = target.a, tb = target.b, tc = target.c, td = target.d;
 			float ta = target.a, tb = target.b, tc = target.c, td = target.d;
 			float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
 			float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
 			float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
 			float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
-			var bones = this.bones;
-			for (int i = 0, n = bones.Count; i < n; i++) {
-				Bone bone = bones.Items[i];
-				bool modified = false;
+			var bones = this.bones.Items;
+			for (int i = 0, n = this.bones.Count; i < n; i++) {
+				Bone bone = bones[i];
 
 
 				if (rotateMix != 0) {
 				if (rotateMix != 0) {
 					float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
 					float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
@@ -182,7 +172,6 @@ namespace Spine {
 					bone.b = cos * b - sin * d;
 					bone.b = cos * b - sin * d;
 					bone.c = sin * a + cos * c;
 					bone.c = sin * a + cos * c;
 					bone.d = sin * b + cos * d;
 					bone.d = sin * b + cos * d;
-					modified = true;
 				}
 				}
 
 
 				if (translateMix != 0) {
 				if (translateMix != 0) {
@@ -190,7 +179,6 @@ namespace Spine {
 					target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY));
 					target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY));
 					bone.worldX += tx * translateMix;
 					bone.worldX += tx * translateMix;
 					bone.worldY += ty * translateMix;
 					bone.worldY += ty * translateMix;
-					modified = true;
 				}
 				}
 
 
 				if (scaleMix > 0) {
 				if (scaleMix > 0) {
@@ -200,7 +188,6 @@ namespace Spine {
 					s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * scaleMix + 1;
 					s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * scaleMix + 1;
 					bone.b *= s;
 					bone.b *= s;
 					bone.d *= s;
 					bone.d *= s;
-					modified = true;
 				}
 				}
 
 
 				if (shearMix > 0) {
 				if (shearMix > 0) {
@@ -213,10 +200,9 @@ namespace Spine {
 					float s = (float)Math.Sqrt(b * b + d * d);
 					float s = (float)Math.Sqrt(b * b + d * d);
 					bone.b = MathUtils.Cos(r) * s;
 					bone.b = MathUtils.Cos(r) * s;
 					bone.d = MathUtils.Sin(r) * s;
 					bone.d = MathUtils.Sin(r) * s;
-					modified = true;
 				}
 				}
 
 
-				if (modified) bone.appliedValid = false;
+				bone.appliedValid = false;
 			}
 			}
 		}
 		}
 
 
@@ -224,9 +210,9 @@ namespace Spine {
 			float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 			float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 			Bone target = this.target;
 			Bone target = this.target;
 			if (!target.appliedValid) target.UpdateAppliedTransform();
 			if (!target.appliedValid) target.UpdateAppliedTransform();
-			var bonesItems = this.bones.Items;
+			var bones = this.bones.Items;
 			for (int i = 0, n = this.bones.Count; i < n; i++) {
 			for (int i = 0, n = this.bones.Count; i < n; i++) {
-				Bone bone = bonesItems[i];
+				Bone bone = bones[i];
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 
 
 				float rotation = bone.arotation;
 				float rotation = bone.arotation;
@@ -263,9 +249,9 @@ namespace Spine {
 			float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 			float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 			Bone target = this.target;
 			Bone target = this.target;
 			if (!target.appliedValid) target.UpdateAppliedTransform();
 			if (!target.appliedValid) target.UpdateAppliedTransform();
-			var bonesItems = this.bones.Items;
+			var bones = this.bones.Items;
 			for (int i = 0, n = this.bones.Count; i < n; i++) {
 			for (int i = 0, n = this.bones.Count; i < n; i++) {
-				Bone bone = bonesItems[i];
+				Bone bone = bones[i];
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 
 
 				float rotation = bone.arotation;
 				float rotation = bone.arotation;

+ 26 - 33
spine-unity/Assets/Spine Examples/Scripts/MecanimAnimationMatchModifier/AnimationMatchModifierAsset.cs

@@ -61,32 +61,35 @@ namespace Spine.Unity.Examples {
 
 
 				// Build a reference collection of timelines to match
 				// Build a reference collection of timelines to match
 				// and a collection of dummy timelines that can be used to fill-in missing items.
 				// and a collection of dummy timelines that can be used to fill-in missing items.
-				var timelineDictionary = new Dictionary<int, Spine.Timeline>();
+				var timelineDictionary = new Dictionary<string, Spine.Timeline>();
 				foreach (var animation in animations) {
 				foreach (var animation in animations) {
 					foreach (var timeline in animation.Timelines) {
 					foreach (var timeline in animation.Timelines) {
 						if (timeline is EventTimeline) continue;
 						if (timeline is EventTimeline) continue;
 
 
-						int propertyID = timeline.PropertyId;
-						if (!timelineDictionary.ContainsKey(propertyID)) {
-							timelineDictionary.Add(propertyID, GetFillerTimeline(timeline, skeletonData));
+						foreach (string propertyId in timeline.PropertyIds) {
+							if (!timelineDictionary.ContainsKey(propertyId)) {
+								timelineDictionary.Add(propertyId, GetFillerTimeline(timeline, skeletonData));
+							}
 						}
 						}
 					}
 					}
 				}
 				}
-				var idsToMatch = new List<int>(timelineDictionary.Keys);
+				var idsToMatch = new List<string>(timelineDictionary.Keys);
 
 
 				// For each animation in the list, check for and add missing timelines.
 				// For each animation in the list, check for and add missing timelines.
-				var currentAnimationIDs = new HashSet<int>();
+				var currentAnimationIDs = new HashSet<string>();
 				foreach (var animation in animations) {
 				foreach (var animation in animations) {
 					currentAnimationIDs.Clear();
 					currentAnimationIDs.Clear();
 					foreach (var timeline in animation.Timelines) {
 					foreach (var timeline in animation.Timelines) {
 						if (timeline is EventTimeline) continue;
 						if (timeline is EventTimeline) continue;
-						currentAnimationIDs.Add(timeline.PropertyId);
+						foreach (string propertyId in timeline.PropertyIds) {
+							currentAnimationIDs.Add(propertyId);
+						}
 					}
 					}
 
 
 					var animationTimelines = animation.Timelines;
 					var animationTimelines = animation.Timelines;
-					foreach (int propertyID in idsToMatch) {
-						if (!currentAnimationIDs.Contains(propertyID))
-							animationTimelines.Add(timelineDictionary[propertyID]);
+					foreach (string propertyId in idsToMatch) {
+						if (!currentAnimationIDs.Contains(propertyId))
+							animationTimelines.Add(timelineDictionary[propertyId]);
 					}
 					}
 				}
 				}
 
 
@@ -132,62 +135,52 @@ namespace Spine.Unity.Examples {
 			}
 			}
 
 
 			static RotateTimeline GetFillerTimeline (RotateTimeline timeline, SkeletonData skeletonData) {
 			static RotateTimeline GetFillerTimeline (RotateTimeline timeline, SkeletonData skeletonData) {
-				var t = new RotateTimeline(1);
-				t.BoneIndex = timeline.BoneIndex;
+				var t = new RotateTimeline(1, 0, timeline.BoneIndex);
 				t.SetFrame(0, 0, 0);
 				t.SetFrame(0, 0, 0);
 				return t;
 				return t;
 			}
 			}
 
 
 			static TranslateTimeline GetFillerTimeline (TranslateTimeline timeline, SkeletonData skeletonData) {
 			static TranslateTimeline GetFillerTimeline (TranslateTimeline timeline, SkeletonData skeletonData) {
-				var t = new TranslateTimeline(1);
-				t.BoneIndex = timeline.BoneIndex;
+				var t = new TranslateTimeline(1, 0, timeline.BoneIndex);
 				t.SetFrame(0, 0, 0, 0);
 				t.SetFrame(0, 0, 0, 0);
 				return t;
 				return t;
 			}
 			}
 
 
 			static ScaleTimeline GetFillerTimeline (ScaleTimeline timeline, SkeletonData skeletonData) {
 			static ScaleTimeline GetFillerTimeline (ScaleTimeline timeline, SkeletonData skeletonData) {
-				var t = new ScaleTimeline(1);
-				t.BoneIndex = timeline.BoneIndex;
+				var t = new ScaleTimeline(1, 0, timeline.BoneIndex);
 				t.SetFrame(0, 0, 0, 0);
 				t.SetFrame(0, 0, 0, 0);
 				return t;
 				return t;
 			}
 			}
 
 
 			static ShearTimeline GetFillerTimeline (ShearTimeline timeline, SkeletonData skeletonData) {
 			static ShearTimeline GetFillerTimeline (ShearTimeline timeline, SkeletonData skeletonData) {
-				var t = new ShearTimeline(1);
-				t.BoneIndex = timeline.BoneIndex;
+				var t = new ShearTimeline(1, 0, timeline.BoneIndex);
 				t.SetFrame(0, 0, 0, 0);
 				t.SetFrame(0, 0, 0, 0);
 				return t;
 				return t;
 			}
 			}
 
 
 			static AttachmentTimeline GetFillerTimeline (AttachmentTimeline timeline, SkeletonData skeletonData) {
 			static AttachmentTimeline GetFillerTimeline (AttachmentTimeline timeline, SkeletonData skeletonData) {
-				var t = new AttachmentTimeline(1);
-				t.SlotIndex = timeline.SlotIndex;
+				var t = new AttachmentTimeline(1, timeline.SlotIndex);
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				t.SetFrame(0, 0, slotData.AttachmentName);
 				t.SetFrame(0, 0, slotData.AttachmentName);
 				return t;
 				return t;
 			}
 			}
 
 
 			static ColorTimeline GetFillerTimeline (ColorTimeline timeline, SkeletonData skeletonData) {
 			static ColorTimeline GetFillerTimeline (ColorTimeline timeline, SkeletonData skeletonData) {
-				var t = new ColorTimeline(1);
-				t.SlotIndex = timeline.SlotIndex;
+				var t = new ColorTimeline(1, 0, timeline.SlotIndex);
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A);
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A);
 				return t;
 				return t;
 			}
 			}
 
 
 			static TwoColorTimeline GetFillerTimeline (TwoColorTimeline timeline, SkeletonData skeletonData) {
 			static TwoColorTimeline GetFillerTimeline (TwoColorTimeline timeline, SkeletonData skeletonData) {
-				var t = new TwoColorTimeline(1);
-				t.SlotIndex = timeline.SlotIndex;
+				var t = new TwoColorTimeline(1, 0, timeline.SlotIndex);
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				var slotData = skeletonData.Slots.Items[t.SlotIndex];
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A, slotData.R2, slotData.G2, slotData.B2);
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A, slotData.R2, slotData.G2, slotData.B2);
 				return t;
 				return t;
 			}
 			}
 
 
 			static DeformTimeline GetFillerTimeline (DeformTimeline timeline, SkeletonData skeletonData) {
 			static DeformTimeline GetFillerTimeline (DeformTimeline timeline, SkeletonData skeletonData) {
-				var t = new DeformTimeline(1);
-				t.SlotIndex = timeline.SlotIndex;
-				t.Attachment = timeline.Attachment;
-
+				var t = new DeformTimeline(1, 0, timeline.SlotIndex, timeline.Attachment);
 				if (t.Attachment.IsWeighted()) {
 				if (t.Attachment.IsWeighted()) {
 					t.SetFrame(0, 0, new float[t.Attachment.Vertices.Length]);
 					t.SetFrame(0, 0, new float[t.Attachment.Vertices.Length]);
 				} else {
 				} else {
@@ -204,35 +197,35 @@ namespace Spine.Unity.Examples {
 			}
 			}
 
 
 			static IkConstraintTimeline GetFillerTimeline (IkConstraintTimeline timeline, SkeletonData skeletonData) {
 			static IkConstraintTimeline GetFillerTimeline (IkConstraintTimeline timeline, SkeletonData skeletonData) {
-				var t = new IkConstraintTimeline(1);
+				var t = new IkConstraintTimeline(1, 0, timeline.IkConstraintIndex);
 				var ikConstraintData = skeletonData.IkConstraints.Items[timeline.IkConstraintIndex];
 				var ikConstraintData = skeletonData.IkConstraints.Items[timeline.IkConstraintIndex];
 				t.SetFrame(0, 0, ikConstraintData.Mix, ikConstraintData.Softness, ikConstraintData.BendDirection, ikConstraintData.Compress, ikConstraintData.Stretch);
 				t.SetFrame(0, 0, ikConstraintData.Mix, ikConstraintData.Softness, ikConstraintData.BendDirection, ikConstraintData.Compress, ikConstraintData.Stretch);
 				return t;
 				return t;
 			}
 			}
 
 
 			static TransformConstraintTimeline GetFillerTimeline (TransformConstraintTimeline timeline, SkeletonData skeletonData) {
 			static TransformConstraintTimeline GetFillerTimeline (TransformConstraintTimeline timeline, SkeletonData skeletonData) {
-				var t = new TransformConstraintTimeline(1);
+				var t = new TransformConstraintTimeline(1, 0, timeline.TransformConstraintIndex);
 				var data = skeletonData.TransformConstraints.Items[timeline.TransformConstraintIndex];
 				var data = skeletonData.TransformConstraints.Items[timeline.TransformConstraintIndex];
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix, data.ScaleMix, data.ShearMix);
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix, data.ScaleMix, data.ShearMix);
 				return t;
 				return t;
 			}
 			}
 
 
 			static PathConstraintPositionTimeline GetFillerTimeline (PathConstraintPositionTimeline timeline, SkeletonData skeletonData) {
 			static PathConstraintPositionTimeline GetFillerTimeline (PathConstraintPositionTimeline timeline, SkeletonData skeletonData) {
-				var t = new PathConstraintPositionTimeline(1);
+				var t = new PathConstraintPositionTimeline(1, 0, timeline.PathConstraintIndex);
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				t.SetFrame(0, 0, data.Position);
 				t.SetFrame(0, 0, data.Position);
 				return t;
 				return t;
 			}
 			}
 
 
 			static PathConstraintSpacingTimeline GetFillerTimeline (PathConstraintSpacingTimeline timeline, SkeletonData skeletonData) {
 			static PathConstraintSpacingTimeline GetFillerTimeline (PathConstraintSpacingTimeline timeline, SkeletonData skeletonData) {
-				var t = new PathConstraintSpacingTimeline(1);
+				var t = new PathConstraintSpacingTimeline(1, 0, timeline.PathConstraintIndex);
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				t.SetFrame(0, 0, data.Spacing);
 				t.SetFrame(0, 0, data.Spacing);
 				return t;
 				return t;
 			}
 			}
 
 
 			static PathConstraintMixTimeline GetFillerTimeline (PathConstraintMixTimeline timeline, SkeletonData skeletonData) {
 			static PathConstraintMixTimeline GetFillerTimeline (PathConstraintMixTimeline timeline, SkeletonData skeletonData) {
-				var t = new PathConstraintMixTimeline(1);
+				var t = new PathConstraintMixTimeline(1, 0, timeline.PathConstraintIndex);
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				var data = skeletonData.PathConstraints.Items[timeline.PathConstraintIndex];
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix);
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix);
 				return t;
 				return t;

+ 0 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs

@@ -123,7 +123,6 @@ namespace Spine.Unity.Editor {
 						EditorGUI.indentLevel = 0;
 						EditorGUI.indentLevel = 0;
 
 
 						var mixMode = layerMixModes.GetArrayElementAtIndex(i);
 						var mixMode = layerMixModes.GetArrayElementAtIndex(i);
-						var blendMode = layerBlendModes.GetArrayElementAtIndex(i);
 						rect.position += new Vector2(rect.width, 0);
 						rect.position += new Vector2(rect.width, 0);
 						rect.width = widthMixColumn;
 						rect.width = widthMixColumn;
 						EditorGUI.PropertyField(rect, mixMode, GUIContent.none);
 						EditorGUI.PropertyField(rect, mixMode, GUIContent.none);

+ 0 - 9
spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes.meta

@@ -1,9 +0,0 @@
-fileFormatVersion: 2
-guid: 18ee2876d53412642bbfa1070a1b947f
-folderAsset: yes
-timeCreated: 1527569487
-licenseType: Free
-DefaultImporter:
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 0 - 9
spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor.meta

@@ -1,9 +0,0 @@
-fileFormatVersion: 2
-guid: 1ad4318c20ec5674a9f4d7f786afd681
-folderAsset: yes
-timeCreated: 1496449217
-licenseType: Free
-DefaultImporter:
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 0 - 47
spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor/SlotBlendModesEditor.cs

@@ -1,47 +0,0 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-using UnityEngine;
-using UnityEditor;
-using Spine.Unity.Deprecated;
-using System;
-
-namespace Spine.Unity.Editor {
-	using Editor = UnityEditor.Editor;
-
-	[Obsolete("The spine-unity 3.7 runtime introduced SkeletonDataModifierAssets BlendModeMaterials which replaced SlotBlendModes. Will be removed in spine-unity 3.9.", false)]
-	public class SlotBlendModesEditor : Editor {
-
-		[MenuItem("CONTEXT/SkeletonRenderer/Add Slot Blend Modes Component")]
-		static void AddSlotBlendModesComponent (MenuCommand command) {
-			var skeletonRenderer = (SkeletonRenderer)command.context;
-			skeletonRenderer.gameObject.AddComponent<SlotBlendModes>();
-		}
-	}
-}

+ 0 - 12
spine-unity/Assets/Spine/Editor/spine-unity/Modules/SlotBlendModes/Editor/SlotBlendModesEditor.cs.meta

@@ -1,12 +0,0 @@
-fileFormatVersion: 2
-guid: cbec7dc66dca80a419477536c23b7a0d
-timeCreated: 1496449255
-licenseType: Free
-MonoImporter:
-  serializedVersion: 2
-  defaultReferences: []
-  executionOrder: 0
-  icon: {instanceID: 0}
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 2 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs

@@ -40,8 +40,8 @@ namespace Spine.Unity {
 	public static class SkeletonDataCompatibility {
 	public static class SkeletonDataCompatibility {
 
 
 	#if UNITY_EDITOR
 	#if UNITY_EDITOR
-		static readonly int[][] compatibleBinaryVersions = { new[] { 3, 9, 0 }, new[] { 3, 8, 0 } };
-		static readonly int[][] compatibleJsonVersions = { new[] { 3, 9, 0 }, new[] { 3, 8, 0 } };
+		static readonly int[][] compatibleBinaryVersions = { new[] { 4, 0, 0 } };
+		static readonly int[][] compatibleJsonVersions = { new[] { 4, 0, 0 } };
 
 
 		static bool wasVersionDialogShown = false;
 		static bool wasVersionDialogShown = false;
 		static readonly Regex jsonVersionRegex = new Regex(@"""spine""\s*:\s*""([^""]+)""", RegexOptions.CultureInvariant);
 		static readonly Regex jsonVersionRegex = new Regex(@"""spine""\s*:\s*""([^""]+)""", RegexOptions.CultureInvariant);

+ 0 - 1
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs

@@ -254,7 +254,6 @@ namespace Spine.Unity {
 			private void OnClipAppliedCallback (Spine.Animation clip, AnimatorStateInfo stateInfo,
 			private void OnClipAppliedCallback (Spine.Animation clip, AnimatorStateInfo stateInfo,
 				int layerIndex, float time, bool isLooping, float weight) {
 				int layerIndex, float time, bool isLooping, float weight) {
 
 
-				float clipDuration = clip.duration == 0 ? 1 : clip.duration;
 				float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
 				float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
 				float lastTime = time - (Time.deltaTime * speedFactor);
 				float lastTime = time - (Time.deltaTime * speedFactor);
 				if (isLooping && clip.duration != 0) {
 				if (isLooping && clip.duration != 0) {

+ 0 - 9
spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated.meta

@@ -1,9 +0,0 @@
-fileFormatVersion: 2
-guid: 04817e31b917de6489f349dd332d7468
-folderAsset: yes
-timeCreated: 1563295668
-licenseType: Free
-DefaultImporter:
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 0 - 9
spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes.meta

@@ -1,9 +0,0 @@
-fileFormatVersion: 2
-guid: dfdd78a071ca1a04bb64c6cc41e14aa0
-folderAsset: yes
-timeCreated: 1496447038
-licenseType: Free
-DefaultImporter:
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 0 - 230
spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes/SlotBlendModes.cs

@@ -1,230 +0,0 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-using System.Collections.Generic;
-using UnityEngine;
-using System;
-
-namespace Spine.Unity.Deprecated {
-
-	/// <summary>
-	/// Deprecated. The spine-unity 3.7 runtime introduced SkeletonDataModifierAssets BlendModeMaterials which replaced SlotBlendModes. See the
-	/// <see href="http://esotericsoftware.com/spine-unity-skeletondatamodifierassets#BlendModeMaterials">SkeletonDataModifierAssets BlendModeMaterials documentation page</see> and
-	/// <see href="http://esotericsoftware.com/forum/Slot-blending-not-work-11281">this forum thread</see> for further information.
-	/// This class will be removed in the spine-unity 3.9 runtime.
-	/// </summary>
-	[Obsolete("The spine-unity 3.7 runtime introduced SkeletonDataModifierAssets BlendModeMaterials which replaced SlotBlendModes. Will be removed in spine-unity 3.9.", false)]
-	[DisallowMultipleComponent]
-	public class SlotBlendModes : MonoBehaviour {
-
-		#region Internal Material Dictionary
-		public struct MaterialTexturePair {
-			public Texture2D texture2D;
-			public Material material;
-		}
-
-		internal class MaterialWithRefcount {
-			public Material materialClone;
-			public int refcount = 1;
-
-			public MaterialWithRefcount(Material mat) {
-				this.materialClone = mat;
-			}
-		}
-		static Dictionary<MaterialTexturePair, MaterialWithRefcount> materialTable;
-		internal static Dictionary<MaterialTexturePair, MaterialWithRefcount> MaterialTable {
-			get {
-				if (materialTable == null) materialTable = new Dictionary<MaterialTexturePair, MaterialWithRefcount>();
-				return materialTable;
-			}
-		}
-
-		internal struct SlotMaterialTextureTuple {
-			public Slot slot;
-			public Texture2D texture2D;
-			public Material material;
-
-			public SlotMaterialTextureTuple(Slot slot, Material material, Texture2D texture) {
-				this.slot = slot;
-				this.material = material;
-				this.texture2D = texture;
-			}
-		}
-
-		internal static Material GetOrAddMaterialFor(Material materialSource, Texture2D texture) {
-			if (materialSource == null || texture == null) return null;
-
-			var mt = SlotBlendModes.MaterialTable;
-			MaterialWithRefcount matWithRefcount;
-			var key = new MaterialTexturePair {	material = materialSource, texture2D = texture };
-			if (!mt.TryGetValue(key, out matWithRefcount)) {
-				matWithRefcount = new MaterialWithRefcount(new Material(materialSource));
-				var m = matWithRefcount.materialClone;
-				m.name = "(Clone)" + texture.name + "-" + materialSource.name;
-				m.mainTexture = texture;
-				mt[key] = matWithRefcount;
-			}
-			else {
-				matWithRefcount.refcount++;
-			}
-			return matWithRefcount.materialClone;
-		}
-
-		internal static MaterialWithRefcount GetExistingMaterialFor(Material materialSource, Texture2D texture)
-		{
-			if (materialSource == null || texture == null) return null;
-
-			var mt = SlotBlendModes.MaterialTable;
-			MaterialWithRefcount matWithRefcount;
-			var key = new MaterialTexturePair { material = materialSource, texture2D = texture };
-			if (!mt.TryGetValue(key, out matWithRefcount)) {
-				return null;
-			}
-			return matWithRefcount;
-		}
-
-		internal static void RemoveMaterialFromTable(Material materialSource, Texture2D texture) {
-			var mt = SlotBlendModes.MaterialTable;
-			var key = new MaterialTexturePair { material = materialSource, texture2D = texture };
-			mt.Remove(key);
-		}
-		#endregion
-
-		#region Inspector
-		public Material multiplyMaterialSource;
-		public Material screenMaterialSource;
-
-		Texture2D texture;
-		#endregion
-
-		SlotMaterialTextureTuple[] slotsWithCustomMaterial = new SlotMaterialTextureTuple[0];
-
-		public bool Applied { get; private set; }
-
-		void Start() {
-			if (!Applied) Apply();
-		}
-
-		void OnDestroy() {
-			if (Applied) Remove();
-		}
-
-		public void Apply() {
-			GetTexture();
-			if (texture == null) return;
-
-			var skeletonRenderer = GetComponent<SkeletonRenderer>();
-			if (skeletonRenderer == null) return;
-
-			var slotMaterials = skeletonRenderer.CustomSlotMaterials;
-
-			int numSlotsWithCustomMaterial = 0;
-			foreach (var s in skeletonRenderer.Skeleton.Slots) {
-				switch (s.data.blendMode) {
-				case BlendMode.Multiply:
-					if (multiplyMaterialSource != null) {
-						slotMaterials[s] = GetOrAddMaterialFor(multiplyMaterialSource, texture);
-						++numSlotsWithCustomMaterial;
-					}
-					break;
-				case BlendMode.Screen:
-					if (screenMaterialSource != null) {
-						slotMaterials[s] = GetOrAddMaterialFor(screenMaterialSource, texture);
-						++numSlotsWithCustomMaterial;
-					}
-					break;
-				}
-			}
-			slotsWithCustomMaterial = new SlotMaterialTextureTuple[numSlotsWithCustomMaterial];
-			int storedSlotIndex = 0;
-			foreach (var s in skeletonRenderer.Skeleton.Slots) {
-				switch (s.data.blendMode) {
-				case BlendMode.Multiply:
-					if (multiplyMaterialSource != null) {
-						slotsWithCustomMaterial[storedSlotIndex++] = new SlotMaterialTextureTuple(s, multiplyMaterialSource, texture);
-					}
-					break;
-				case BlendMode.Screen:
-					if (screenMaterialSource != null) {
-						slotsWithCustomMaterial[storedSlotIndex++] = new SlotMaterialTextureTuple(s, screenMaterialSource, texture);
-					}
-					break;
-				}
-			}
-
-			Applied = true;
-			skeletonRenderer.LateUpdate();
-		}
-
-		public void Remove() {
-			GetTexture();
-			if (texture == null) return;
-
-			var skeletonRenderer = GetComponent<SkeletonRenderer>();
-			if (skeletonRenderer == null) return;
-
-			var slotMaterials = skeletonRenderer.CustomSlotMaterials;
-
-			foreach (var slotWithCustomMat in slotsWithCustomMaterial) {
-
-				Slot s = slotWithCustomMat.slot;
-				Material storedMaterialSource = slotWithCustomMat.material;
-				Texture2D storedTexture = slotWithCustomMat.texture2D;
-
-				var matWithRefcount = GetExistingMaterialFor(storedMaterialSource, storedTexture);
-				if (--matWithRefcount.refcount == 0) {
-					RemoveMaterialFromTable(storedMaterialSource, storedTexture);
-				}
-				// we don't want to remove slotMaterials[s] if it has been changed in the meantime.
-				Material m;
-				if (slotMaterials.TryGetValue(s, out m)) {
-					var existingMat = matWithRefcount == null ? null : matWithRefcount.materialClone;
-					if (Material.ReferenceEquals(m, existingMat)) {
-						slotMaterials.Remove(s);
-					}
-				}
-			}
-			slotsWithCustomMaterial = null;
-
-			Applied = false;
-			if (skeletonRenderer.valid) skeletonRenderer.LateUpdate();
-		}
-
-		public void GetTexture() {
-			if (texture == null) {
-				var sr = GetComponent<SkeletonRenderer>(); if (sr == null) return;
-				var sda = sr.skeletonDataAsset; if (sda == null) return;
-				var aa = sda.atlasAssets[0]; if (aa == null) return;
-				var am = aa.PrimaryMaterial; if (am == null) return;
-				texture = am.mainTexture as Texture2D;
-			}
-		}
-
-	}
-}

+ 0 - 16
spine-unity/Assets/Spine/Runtime/spine-unity/Deprecated/SlotBlendModes/SlotBlendModes.cs.meta

@@ -1,16 +0,0 @@
-fileFormatVersion: 2
-guid: f1f8243645ba2e74aa3564bd956eed89
-timeCreated: 1496794038
-licenseType: Free
-MonoImporter:
-  serializedVersion: 2
-  defaultReferences:
-  - multiplyMaterialSource: {fileID: 2100000, guid: 53bf0ab317d032d418cf1252d68f51df,
-      type: 2}
-  - screenMaterialSource: {fileID: 2100000, guid: 73f0f46d3177c614baf0fa48d646a9be,
-      type: 2}
-  executionOrder: 0
-  icon: {instanceID: 0}
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 20 - 20
spine-unity/Assets/Spine/Runtime/spine-unity/Utility/TimelineExtensions.cs

@@ -38,28 +38,28 @@ namespace Spine.Unity.AnimationTools {
 		/// SkeletonData can be accessed from Skeleton.Data or from SkeletonDataAsset.GetSkeletonData.
 		/// SkeletonData can be accessed from Skeleton.Data or from SkeletonDataAsset.GetSkeletonData.
 		/// If no SkeletonData is given, values are computed relative to setup pose instead of local-absolute.</summary>
 		/// If no SkeletonData is given, values are computed relative to setup pose instead of local-absolute.</summary>
 		public static Vector2 Evaluate (this TranslateTimeline timeline, float time, SkeletonData skeletonData = null) {
 		public static Vector2 Evaluate (this TranslateTimeline timeline, float time, SkeletonData skeletonData = null) {
-			const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
-			const int X = 1, Y = 2;
-
 			var frames = timeline.frames;
 			var frames = timeline.frames;
 			if (time < frames[0]) return Vector2.zero;
 			if (time < frames[0]) return Vector2.zero;
 
 
 			float x, y;
 			float x, y;
-			if (time >= frames[frames.Length - TranslateTimeline.ENTRIES]) { // Time is after last frame.
-				x = frames[frames.Length + PREV_X];
-				y = frames[frames.Length + PREV_Y];
-			}
-			else {
-				// Interpolate between the previous frame and the current frame.
-				int frame = Animation.BinarySearch(frames, time, TranslateTimeline.ENTRIES);
-				x = frames[frame + PREV_X];
-				y = frames[frame + PREV_Y];
-				float frameTime = frames[frame];
-				float percent = timeline.GetCurvePercent(frame / TranslateTimeline.ENTRIES - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-				x += (frames[frame + X] - x) * percent;
-				y += (frames[frame + Y] - y) * percent;
+			int i = Animation.Search(frames, time, TranslateTimeline.ENTRIES), curveType = (int)timeline.curves[i / TranslateTimeline.ENTRIES];
+			switch (curveType) {
+				case TranslateTimeline.LINEAR:
+					float before = frames[i];
+					x = frames[i + TranslateTimeline.VALUE1];
+					y = frames[i + TranslateTimeline.VALUE2];
+					float t = (time - before) / (frames[i + TranslateTimeline.ENTRIES] - before);
+					x += (frames[i + TranslateTimeline.ENTRIES + TranslateTimeline.VALUE1] - x) * t;
+					y += (frames[i + TranslateTimeline.ENTRIES + TranslateTimeline.VALUE2] - y) * t;
+					break;
+				case TranslateTimeline.STEPPED:
+					x = frames[i + TranslateTimeline.VALUE1];
+					y = frames[i + TranslateTimeline.VALUE2];
+					break;
+				default:
+					x = timeline.GetBezierValue(time, i, TranslateTimeline.VALUE1, curveType - TranslateTimeline.BEZIER);
+					y = timeline.GetBezierValue(time, i, TranslateTimeline.VALUE2, curveType + TranslateTimeline.BEZIER_SIZE - TranslateTimeline.BEZIER);
+					break;
 			}
 			}
 
 
 			Vector2 xy = new Vector2(x, y);
 			Vector2 xy = new Vector2(x, y);
@@ -67,7 +67,7 @@ namespace Spine.Unity.AnimationTools {
 				return xy;
 				return xy;
 			}
 			}
 			else {
 			else {
-				var boneData = skeletonData.bones.Items[timeline.boneIndex];
+				var boneData = skeletonData.bones.Items[timeline.BoneIndex];
 				return xy + new Vector2(boneData.x, boneData.y);
 				return xy + new Vector2(boneData.x, boneData.y);
 			}
 			}
 		}
 		}
@@ -82,7 +82,7 @@ namespace Spine.Unity.AnimationTools {
 					continue;
 					continue;
 
 
 				var translateTimeline = timeline as TranslateTimeline;
 				var translateTimeline = timeline as TranslateTimeline;
-				if (translateTimeline != null && translateTimeline.boneIndex == boneIndex)
+				if (translateTimeline != null && translateTimeline.BoneIndex == boneIndex)
 					return translateTimeline;
 					return translateTimeline;
 			}
 			}
 			return null;
 			return null;

+ 1 - 1
spine-unity/Assets/Spine/version.txt

@@ -1 +1 @@
-This Spine-Unity runtime works with data exported from Spine Editor version: 3.8.xx and 3.9.xx
+This Spine-Unity runtime works with data exported from Spine Editor version: 4.0.xx

Some files were not shown because too many files changed in this diff