Forráskód Böngészése

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

Harald Csaszar 4 éve
szülő
commit
d2529d410b
30 módosított fájl, 1522 hozzáadás és 1457 törlés
  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

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 449 - 337
spine-csharp/src/Animation.cs


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

@@ -112,7 +112,7 @@ namespace Spine {
 
 		// end of difference
 		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 float timeScale = 1;
 		private int unkeyedState;
@@ -244,7 +244,13 @@ namespace Spine {
 					mix = 0; // Set to setup pose the last time the entry will be applied.
 
 				// 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;
 				var timelines = current.animation.timelines;
 				var timelinesItems = timelines.Items;
@@ -252,9 +258,9 @@ namespace Spine {
 					for (int ii = 0; ii < timelineCount; ii++) {
 						var timeline = timelinesItems[ii];
 						if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
 						else
-							timeline.Apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.In);
+							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.In);
 					}
 				} else {
 					var timelineMode = current.timelineMode.Items;
@@ -268,12 +274,12 @@ namespace Spine {
 						MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup;
 						var rotateTimeline = timeline as RotateTimeline;
 						if (rotateTimeline != null)
-							ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation,
+							ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation,
 												ii << 1, firstFrame);
 						else if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
 						else
-							timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.In);
+							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.In);
 					}
 				}
 				QueueEvents(current, animationTime);
@@ -314,17 +320,23 @@ namespace Spine {
 				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;
-			float animationLast = from.animationLast, animationTime = from.AnimationTime;
 			var timelines = from.animation.timelines;
 			int timelineCount = timelines.Count;
 			var timelinesItems = timelines.Items;
 			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) {
 				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 {
 				var timelineMode = from.timelineMode.Items;
 				var timelineHoldMix = from.timelineHoldMix.Items;
@@ -366,14 +378,14 @@ namespace Spine {
 					from.totalAlpha += alpha;
 					var rotateTimeline = timeline as RotateTimeline;
 					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) {
-						ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, timelineBlend, attachments);
+						ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments);
 					} else {
 						if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup)
 							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,
 			bool attachments) {
 
-			Slot slot = skeleton.slots.Items[timeline.slotIndex];
+			Slot slot = skeleton.slots.Items[timeline.SlotIndex];
 			if (!slot.bone.active) return;
 
 			float[] frames = timeline.frames;
@@ -402,12 +414,7 @@ namespace Spine {
 					SetAttachment(skeleton, slot, slot.data.attachmentName, attachments);
 			}
 			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.
@@ -432,7 +439,7 @@ namespace Spine {
 				return;
 			}
 
-			Bone bone = skeleton.bones.Items[timeline.boneIndex];
+			Bone bone = skeleton.bones.Items[timeline.BoneIndex];
 			if (!bone.active) return;
 
 			float[] frames = timeline.frames;
@@ -441,7 +448,7 @@ namespace Spine {
 				switch (blend) {
 					case MixBlend.Setup:
 						bone.rotation = bone.data.rotation;
-						return;
+						goto default; // Fall through.
 					default:
 						return;
 					case MixBlend.First:
@@ -451,21 +458,7 @@ namespace Spine {
 				}
 			} else {
 				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.
@@ -494,8 +487,7 @@ namespace Spine {
 				timelinesRotation[i] = total;
 			}
 			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) {
@@ -577,10 +569,17 @@ namespace Spine {
 			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>
 		private void SetCurrent (int index, TrackEntry current, bool interrupt) {
 			TrackEntry from = ExpandToIndex(index);
 			tracks.Items[index] = current;
+			current.previous = null;
 
 			if (from != null) {
 				if (interrupt) queue.Interrupt(from);
@@ -647,7 +646,7 @@ namespace Spine {
 		/// equivalent to calling <see cref="SetAnimation(int, Animation, bool)"/>.</summary>
 		/// <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
-		/// 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
 		/// previous entry is looping, its next loop completion is used instead of its duration.
 		/// </param>
@@ -669,18 +668,8 @@ namespace Spine {
 				queue.Drain();
 			} else {
 				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;
@@ -698,11 +687,11 @@ namespace Spine {
 		/// 0 still mixes out over one frame.</para>
 		/// <para>
 		/// 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) {
 			TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false);
 			entry.mixDuration = mixDuration;
@@ -725,10 +714,10 @@ namespace Spine {
 		/// after the <see cref="AnimationState.Dispose"/> event occurs.
 		/// </returns>
 		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.trackEnd = mixDuration;
+			if (delay <= 0 && entry.previous != null) entry.delay = entry.previous.TrackComplete - entry.mixDuration;
 			return entry;
 		}
 
@@ -738,8 +727,9 @@ namespace Spine {
 		public void SetEmptyAnimations (float mixDuration) {
 			bool oldDrainDisabled = queue.drainDisabled;
 			queue.drainDisabled = true;
+			var tracksItems = tracks.Items;
 			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);
 			}
 			queue.drainDisabled = oldDrainDisabled;
@@ -798,10 +788,10 @@ namespace Spine {
 			animationsChanged = false;
 
 			// Process in the order that animations are applied.
-			propertyIDs.Clear();
-
+			propertyIds.Clear();
+			int n = tracks.Count;
 			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];
 				if (entry == null) continue;
 				while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
@@ -814,6 +804,8 @@ namespace Spine {
 			}
 		}
 
+
+
 		private void ComputeHold (TrackEntry entry) {
 			TrackEntry to = entry.mixingTo;
 			var timelines = entry.animation.timelines.Items;
@@ -821,11 +813,11 @@ namespace Spine {
 			var timelineMode = entry.timelineMode.Resize(timelinesCount).Items; //timelineMode.setSize(timelinesCount);
 			entry.timelineHoldMix.Clear();
 			var timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; //timelineHoldMix.setSize(timelinesCount);
-			var propertyIDs = this.propertyIDs;
+			var propertyIds = this.propertyIds;
 
 			if (to != null && to.holdPrevious) {
 				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;
 			}
@@ -833,15 +825,15 @@ namespace Spine {
 			// outer:
 			for (int i = 0; i < timelinesCount; 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;
 				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;
 				} else {
 					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) {
 							timelineMode[i] = AnimationState.HoldMix;
 							timelineHoldMix[i] = next;
@@ -892,8 +884,9 @@ namespace Spine {
 
 		override public string ToString () {
 			var buffer = new System.Text.StringBuilder();
+			var tracksItems = tracks.Items;
 			for (int i = 0, n = tracks.Count; i < n; i++) {
-				TrackEntry entry = tracks.Items[i];
+				TrackEntry entry = tracksItems[i];
 				if (entry == null) continue;
 				if (buffer.Length > 0) buffer.Append(", ");
 				buffer.Append(entry.ToString());
@@ -912,7 +905,7 @@ namespace Spine {
 	public class TrackEntry : Pool<TrackEntry>.IPoolable {
 		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'.
 		public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
 		public event AnimationState.TrackEntryEventDelegate Event;
@@ -925,7 +918,7 @@ namespace Spine {
 
 		internal int trackIndex;
 
-		internal bool loop, holdPrevious;
+		internal bool loop, holdPrevious, reverse;
 		internal float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		internal float animationStart, animationEnd, animationLast, nextAnimationLast;
 		internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f;
@@ -937,6 +930,7 @@ namespace Spine {
 
 		// IPoolable.Reset()
 		public void Reset () {
+			previous = null;
 			next = null;
 			mixingFrom = null;
 			mixingTo = null;
@@ -973,7 +967,10 @@ namespace Spine {
 		/// track entry <see cref="TrackEntry.TrackTime"/> &gt;= this track entry's <code>Delay</code>).</para>
 		/// <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; } }
 
 		/// <summary>
@@ -994,6 +991,21 @@ namespace Spine {
 		/// </summary>
 		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>
 		/// <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
 		/// faster. Defaults to 1.</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
 		/// match the animation speed.</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>
 		/// <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; } }
 
 		/// <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; } }
 
+		/// <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>
 		/// Returns true if at least one loop has been completed.</summary>
 		/// <seealso cref="TrackEntry.Complete"/>
@@ -1108,20 +1129,21 @@ namespace Spine {
 		/// <para>
 		/// 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
-		///  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; } }
 
 		/// <summary>
 		/// <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
 		///  called.</para>
 		/// </summary>
@@ -1153,6 +1175,10 @@ namespace Spine {
 		/// </summary>
 		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>
 		/// <para>
 		/// 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;
 
 		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;
 		}
 

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

@@ -56,7 +56,7 @@ namespace Spine {
 
 			deformAttachment = this;
 			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>
 		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>
 		public Bone (BoneData data, Skeleton skeleton, Bone parent) {
 			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) {
 			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;
-			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) {

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

@@ -79,20 +79,16 @@ namespace Spine {
 			stretch = constraint.stretch;
 		}
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
+			if (mix == 0) return;
 			Bone target = this.target;
-			ExposedList<Bone> bones = this.bones;
-			switch (bones.Count) {
+			var bones = this.bones.Items;
+			switch (this.bones.Count) {
 			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;
 			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;
 			}
 		}
@@ -157,6 +153,7 @@ namespace Spine {
 		/// <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,
 								float alpha) {
+			if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null.");
 			if (!bone.appliedValid) bone.UpdateAppliedTransform();
 			Bone p = bone.parent;
 
@@ -175,16 +172,16 @@ namespace Spine {
 					float sc = pc / bone.skeleton.ScaleY;
 					pb = -sc * s * bone.skeleton.ScaleX;
 					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.
-                }
-                default: {
+				}
+				default: {
 					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;
@@ -198,13 +195,10 @@ namespace Spine {
 			if (compress || stretch) {
 				switch (bone.data.transformMode) {
 					case TransformMode.NoScale:
-                        tx = targetX - bone.worldX;
-                        ty = targetY - bone.worldY;
-                        break;
-                    case TransformMode.NoScaleOrReflection:
+					case TransformMode.NoScaleOrReflection:
 						tx = targetX - bone.worldX;
 						ty = targetY - bone.worldY;
-                        break;
+						break;
 				}
 				float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
 				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>
 		static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float softness,
 			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 (!child.appliedValid) child.UpdateAppliedTransform();
 			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>
 	/// <para>
 	/// 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>
 	/// See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide.</para>
 	/// </summary>
@@ -82,11 +82,6 @@ namespace Spine {
 			translateMix = constraint.translateMix;
 		}
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
 			PathAttachment attachment = target.Attachment as PathAttachment;
 			if (attachment == null) return;

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

@@ -40,7 +40,6 @@ namespace Spine {
 		internal ExposedList<TransformConstraint> transformConstraints;
 		internal ExposedList<PathConstraint> pathConstraints;
 		internal ExposedList<IUpdatable> updateCache = new ExposedList<IUpdatable>();
-		internal ExposedList<Bone> updateCacheReset = new ExposedList<Bone>();
 		internal Skin skin;
 		internal float r = 1, g = 1, b = 1, a = 1;
 		internal float time;
@@ -55,7 +54,13 @@ namespace Spine {
 		public ExposedList<IkConstraint> IkConstraints { get { return ikConstraints; } }
 		public ExposedList<PathConstraint> PathConstraints { get { return pathConstraints; } }
 		public ExposedList<TransformConstraint> TransformConstraints { get { return transformConstraints; } }
-		public Skin Skin { get { return skin; } set { 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 G { get { return g; } set { g = 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.")]
 		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 {
 			get { return bones.Count == 0 ? null : bones.Items[0]; }
 		}
@@ -81,22 +87,23 @@ namespace Spine {
 			this.data = data;
 
 			bones = new ExposedList<Bone>(data.bones.Count);
+			var bonesItems = this.bones.Items;
 			foreach (BoneData boneData in data.bones) {
 				Bone bone;
 				if (boneData.parent == null) {
 					bone = new Bone(boneData, this, null);
 				} else {
-					Bone parent = bones.Items[boneData.parent.index];
+					Bone parent = bonesItems[boneData.parent.index];
 					bone = new Bone(boneData, this, parent);
 					parent.children.Add(bone);
 				}
-				bones.Add(bone);
+				this.bones.Add(bone);
 			}
 
 			slots = new ExposedList<Slot>(data.slots.Count);
 			drawOrder = new ExposedList<Slot>(data.slots.Count);
 			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);
 				slots.Add(slot);
 				drawOrder.Add(slot);
@@ -115,7 +122,7 @@ namespace Spine {
 				pathConstraints.Add(new PathConstraint(pathConstraintData, this));
 
 			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
@@ -123,7 +130,6 @@ namespace Spine {
 		public void UpdateCache () {
 			var updateCache = this.updateCache;
 			updateCache.Clear();
-			this.updateCacheReset.Clear();
 
 			int boneCount = this.bones.Items.Length;
 			var bones = this.bones;
@@ -191,16 +197,19 @@ namespace Spine {
 			Bone parent = constrained.Items[0];
 			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) {
@@ -218,17 +227,17 @@ namespace Spine {
 			Attachment attachment = slot.attachment;
 			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++)
-				SortBone(constrained.Items[i]);
+				SortBone(constrained[i]);
 
 			updateCache.Add(constraint);
 
 			for (int i = 0; i < boneCount; i++)
-				SortReset(constrained.Items[i].children);
+				SortReset(constrained[i].children);
 			for (int i = 0; i < boneCount; i++)
-				constrained.Items[i].sorted = true;
+				constrained[i].sorted = true;
 		}
 
 		private void SortTransformConstraint (TransformConstraint constraint) {
@@ -238,25 +247,25 @@ namespace Spine {
 
 			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) {
 				for (int i = 0; i < boneCount; i++) {
-					Bone child = constrained.Items[i];
+					Bone child = constrained[i];
 					SortBone(child.parent);
-					if (!updateCache.Contains(child)) updateCacheReset.Add(child);
+					SortBone(child);
 				}
 			} else {
 				for (int i = 0; i < boneCount; i++)
-					SortBone(constrained.Items[i]);
+					SortBone(constrained[i]);
 			}
 
 			updateCache.Add(constraint);
 
 			for (int i = 0; i < boneCount; i++)
-				SortReset(constrained.Items[i].children);
+				SortReset(constrained[i].children);
 			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) {
@@ -271,12 +280,12 @@ namespace Spine {
 			if (pathBones == null)
 				SortBone(slotBone);
 			else {
-				var bones = this.bones;
+				var bones = this.bones.Items;
 				for (int i = 0, n = pathBones.Length; i < n;) {
 					int nn = pathBones[i++];
 					nn += i;
 					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 () {
-			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>
@@ -324,22 +326,7 @@ namespace Spine {
 		/// all constraints.
 	 	/// </summary>
 		public void UpdateWorldTransform (Bone parent) {
-			// This partial update avoids computing the world transform for constrained bones when 1) the bone is not updated
-			// 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.
 			Bone rootBone = this.RootBone;
@@ -358,10 +345,9 @@ namespace Spine {
 			rootBone.d = (pc * lb + pd * ld) * scaleY;
 
 			// 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)
 					updatable.Update();
 			}
@@ -375,13 +361,13 @@ namespace Spine {
 
 		/// <summary>Sets the bones and constraints to their setup pose values.</summary>
 		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.softness = constraint.data.softness;
 				constraint.bendDirection = constraint.data.bendDirection;
@@ -389,9 +375,9 @@ namespace Spine {
 				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;
 				constraint.rotateMix = constraintData.rotateMix;
 				constraint.translateMix = constraintData.translateMix;
@@ -399,9 +385,9 @@ namespace Spine {
 				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;
 				constraint.position = constraintData.position;
 				constraint.spacing = constraintData.spacing;
@@ -411,23 +397,21 @@ namespace Spine {
 		}
 
 		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>
 		public Bone FindBone (string boneName) {
 			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;
 			}
 			return null;
@@ -443,13 +427,14 @@ namespace Spine {
 			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>
 		public Slot FindSlot (string slotName) {
 			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;
 			}
 			return null;
@@ -461,11 +446,11 @@ namespace Spine {
 			var slots = this.slots;
 			var slotsItems = slots.Items;
 			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;
 		}
 
-		/// <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) {
 			Skin foundSkin = data.FindSkin(skinName);
 			if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName");
@@ -506,7 +491,7 @@ namespace Spine {
 			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>
 		public Attachment GetAttachment (string slotName, string attachmentName) {
 			return GetAttachment(data.FindSlotIndex(slotName), attachmentName);
@@ -543,34 +528,40 @@ namespace Spine {
 			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>
 		public IkConstraint FindIkConstraint (string constraintName) {
 			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;
 			}
 			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>
 		public TransformConstraint FindTransformConstraint (string constraintName) {
 			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;
 			}
 			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>
 		public PathConstraint FindPathConstraint (string constraintName) {
 			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;
 			}
 			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) {
 			float[] temp = vertexBuffer;
 			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;
-			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;
 				int verticesLength = 0;
 				float[] vertices = null;

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

@@ -34,6 +34,7 @@
 using System;
 using System.IO;
 using System.Collections.Generic;
+using System.Runtime.Serialization;
 
 #if WINDOWS_STOREAPP
 using System.Threading.Tasks;
@@ -41,7 +42,7 @@ using Windows.Storage;
 #endif
 
 namespace Spine {
-	public class SkeletonBinary {
+	public class SkeletonBinary : SkeletonLoader {
 		public const int BONE_ROTATE = 0;
 		public const int BONE_TRANSLATE = 1;
 		public const int BONE_SCALE = 2;
@@ -59,22 +60,15 @@ namespace Spine {
 		public const int CURVE_STEPPED = 1;
 		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) {
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			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;
 		}
-		#else
-		public SkeletonData ReadSkeletonData (String path) {
+#else
+		public override SkeletonData ReadSkeletonData (string path) {
 		#if WINDOWS_PHONE
 			using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 		#else
@@ -119,13 +113,13 @@ namespace Spine {
 
 		public SkeletonData ReadSkeletonData (Stream file) {
 			if (file == null) throw new ArgumentNullException("file");
-			float scale = Scale;
+			float scale = this.scale;
 
 			var skeletonData = new SkeletonData();
 			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();
 			if (skeletonData.version.Length == 0) skeletonData.version = null;
 			if ("3.8.75" == skeletonData.version)
@@ -151,16 +145,15 @@ namespace Spine {
 			Object[] o;
 
 			// 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++)
 				o[i] = input.ReadString();
 
 			// 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++) {
 				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);
 				data.rotation = input.ReadFloat();
 				data.x = input.ReadFloat() * scale;
@@ -169,18 +162,18 @@ namespace Spine {
 				data.scaleY = input.ReadFloat();
 				data.shearX = input.ReadFloat();
 				data.shearY = input.ReadFloat();
-				data.length = input.ReadFloat() * scale;
+				data.Length = input.ReadFloat() * scale;
 				data.transformMode = TransformModeValues[input.ReadInt(true)];
 				data.skinRequired = input.ReadBoolean();
 				if (nonessential) input.ReadInt(); // Skip bone color.
-				o[i] = data;
+				bones[i] = data;
 			}
 
 			// 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++) {
 				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);
 				int color = input.ReadInt();
 				slotData.r = ((color & 0xff000000) >> 24) / 255f;
@@ -198,7 +191,7 @@ namespace Spine {
 
 				slotData.attachmentName = input.ReadStringRef();
 				slotData.blendMode = (BlendMode)input.ReadInt(true);
-				o[i] = slotData;
+				slots[i] = slotData;
 			}
 
 			// IK constraints.
@@ -207,10 +200,10 @@ namespace Spine {
 				IkConstraintData data = new IkConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				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++)
-					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.softness = input.ReadFloat() * scale;
 				data.bendDirection = input.ReadSByte();
@@ -226,10 +219,10 @@ namespace Spine {
 				TransformConstraintData data = new TransformConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				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++)
-					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.relative = input.ReadBoolean();
 				data.offsetRotation = input.ReadFloat();
@@ -251,10 +244,10 @@ namespace Spine {
 				PathConstraintData data = new PathConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				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++)
-					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.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(input.ReadInt(true));
 				data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(input.ReadInt(true));
@@ -286,7 +279,7 @@ namespace Spine {
 			// Linked meshes.
 			n = linkedMeshes.Count;
 			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);
 				if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin);
 				Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent);
@@ -334,16 +327,21 @@ namespace Spine {
 			} else {
 				skin = new Skin(input.ReadStringRef());
 				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++)
-					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++)
-					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++)
-					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++)
-					skin.constraints.Add(skeletonData.pathConstraints.Items[input.ReadInt(true)]);
+					skin.constraints.Add(pathConstraintsItems[input.ReadInt(true)]);
 				skin.constraints.TrimExcess();
+
 				slotCount = input.ReadInt(true);
 			}
 			for (int i = 0; i < slotCount; i++) {
@@ -359,14 +357,12 @@ namespace Spine {
 
 		private Attachment ReadAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex,
 			String attachmentName, bool nonessential) {
-
-			float scale = Scale;
+			float scale = this.scale;
 
 			String name = input.ReadStringRef();
 			if (name == null) name = attachmentName;
 
-			AttachmentType type = (AttachmentType)input.ReadByte();
-			switch (type) {
+			switch ((AttachmentType)input.ReadByte()) {
 			case AttachmentType.Region: {
 					String path = input.ReadStringRef();
 					float rotation = input.ReadFloat();
@@ -529,7 +525,7 @@ namespace Spine {
 		}
 
 		private Vertices ReadVertices (SkeletonInput input, int vertexCount) {
-			float scale = Scale;
+			float scale = this.scale;
 			int verticesLength = vertexCount << 1;
 			Vertices vertices = new Vertices();
 			if(!input.ReadBoolean()) {
@@ -574,66 +570,97 @@ namespace Spine {
 			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) {
-			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.
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 				int slotIndex = input.ReadInt(true);
 				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) {
-					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();
-								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();
-								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++) {
 				int boneIndex = input.ReadInt(true);
 				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;
-						}
-					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;
-						}
 					}
 				}
 			}
 
 			// IK constraint timelines.
 			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);
-				duration = Math.Max(duration, timeline.frames[(frameCount - 1) * IkConstraintTimeline.ENTRIES]);
 			}
 
 			// Transform constraint timelines.
 			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);
-				duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]);
 			}
 
 			// Path constraint timelines.
@@ -719,40 +748,21 @@ namespace Spine {
 				int index = input.ReadInt(true);
 				PathConstraintData data = skeletonData.pathConstraints.Items[index];
 				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_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++) {
 					int slotIndex = input.ReadInt(true);
 					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;
 							int end = input.ReadInt(true);
 							if (end == 0)
@@ -786,7 +796,8 @@ namespace Spine {
 								if (scale == 1) {
 									for (int v = start; v < end; v++)
 										deform[v] = input.ReadFloat();
-								} else {
+								}
+								else {
 									for (int v = start; v < end; v++)
 										deform[v] = input.ReadFloat() * scale;
 								}
@@ -795,12 +806,20 @@ namespace Spine {
 										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);
-						duration = Math.Max(duration, timeline.frames[frameCount - 1]);
 					}
 				}
 			}
@@ -835,7 +854,6 @@ namespace Spine {
 					timeline.SetFrame(i, time, drawOrder);
 				}
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[drawOrderCount - 1]);
 			}
 
 			// Event timeline.
@@ -845,34 +863,75 @@ namespace Spine {
 				for (int i = 0; i < eventCount; i++) {
 					float time = input.ReadFloat();
 					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.balance = input.ReadFloat();
 					}
 					timeline.SetFrame(i, e);
 				}
 				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);
 		}
 
-		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
@@ -883,14 +942,18 @@ namespace Spine {
 
 		internal class SkeletonInput {
 			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;
 
 			public SkeletonInput (Stream input) {
 				this.input = input;
 			}
 
+			public int Read () {
+				return input.ReadByte();
+			}
+
 			public byte ReadByte () {
 				return (byte)input.ReadByte();
 			}
@@ -922,6 +985,18 @@ namespace Spine {
 					+ 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) {
 				int b = input.ReadByte();
 				int result = b & 0x7F;
@@ -959,7 +1034,7 @@ namespace Spine {
 			///<return>May be null.</return>
 			public String ReadStringRef () {
 				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) {
@@ -974,20 +1049,9 @@ namespace Spine {
 			/// <summary>Returns the version string of binary skeleton data.</summary>
 			public string GetVersionString () {
 				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) {
 					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
-		/// 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) {
 			ExposedList<Polygon> polygons = Polygons;
 			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
-		/// 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) {
 			ExposedList<Polygon> polygons = Polygons;
 			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 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; } }
 
 		/// <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; } }
 		/// <summary>The Spine version used to export this data, or null.</summary>
 		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; } }
 
-		/// <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; } }
 
-		/// <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; } }
 
-		/// <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; } }
 
 		// --- Bones.
@@ -99,10 +103,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public BoneData FindBone (string boneName) {
 			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;
 			}
 			return null;
@@ -111,10 +114,9 @@ namespace Spine {
 		/// <returns>-1 if the bone was not found.</returns>
 		public int FindBoneIndex (string boneName) {
 			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;
 		}
 
@@ -123,9 +125,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public SlotData FindSlot (string slotName) {
 			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;
 			}
 			return null;
@@ -165,9 +167,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public Animation FindAnimation (string animationName) {
 			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;
 			}
 			return null;
@@ -178,9 +180,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public IkConstraintData FindIkConstraint (string constraintName) {
 			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;
 			}
 			return null;
@@ -191,9 +193,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public TransformConstraintData FindTransformConstraint (string constraintName) {
 			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;
 			}
 			return null;
@@ -204,9 +206,9 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public PathConstraintData FindPathConstraint (string constraintName) {
 			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;
 			}
 			return null;

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

@@ -41,23 +41,27 @@ using Windows.Storage;
 #endif
 
 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) {
 			var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
 			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;
 		}
-		#else
-		public SkeletonData ReadSkeletonData (string path) {
+#else
+		public override SkeletonData ReadSkeletonData (string path) {
 		#if WINDOWS_PHONE
 			using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) {
 		#else
@@ -88,7 +92,7 @@ namespace Spine {
 		public SkeletonData ReadSkeletonData (TextReader reader) {
 			if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null.");
 
-			float scale = this.Scale;
+			float scale = this.scale;
 			var skeletonData = new SkeletonData();
 
 			var root = Json.Deserialize(reader) as Dictionary<string, Object>;
@@ -99,8 +103,6 @@ namespace Spine {
 				var skeletonMap = (Dictionary<string, Object>)root["skeleton"];
 				skeletonData.hash = (string)skeletonMap["hash"];
 				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.y = GetFloat(skeletonMap, "y", 0);
 				skeletonData.width = GetFloat(skeletonMap, "width", 0);
@@ -283,6 +285,7 @@ namespace Spine {
 							skin.bones.Add(bone);
 						}
 					}
+					skin.bones.TrimExcess();
 					if (skinMap.ContainsKey("ik")) {
 						foreach (string entryName in (List<Object>)skinMap["ik"]) {
 							IkConstraintData constraint = skeletonData.FindIkConstraint(entryName);
@@ -304,6 +307,7 @@ namespace Spine {
 							skin.constraints.Add(constraint);
 						}
 					}
+					skin.constraints.TrimExcess();
 					if (skinMap.ContainsKey("attachments")) {
 						foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap["attachments"]) {
 							int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
@@ -358,7 +362,7 @@ namespace Spine {
 					try {
 						ReadAnimation((Dictionary<string, Object>)entry.Value, entry.Key, skeletonData);
 					} 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) {
-			float scale = this.Scale;
+			float scale = this.scale;
 			name = GetString(map, "name", name);
 
 			var typeName = GetString(map, "type", "region");
@@ -438,7 +442,7 @@ namespace Spine {
 					mesh.regionUVs = uvs;
 					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");
 					return mesh;
 				}
@@ -505,7 +509,7 @@ namespace Spine {
 			for (int i = 0, n = vertices.Length; i < n;) {
 				int boneCount = (int)vertices[i++];
 				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]);
 					weights.Add(vertices[i + 1] * 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) {
-			var scale = this.Scale;
+			var scale = this.scale;
 			var timelines = new ExposedList<Timeline>();
-			float duration = 0;
 
 			// Slot timelines.
 			if (map.ContainsKey("slots")) {
@@ -529,50 +532,117 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
+						if (values.Count == 0) continue;
 						var timelineName = (string)timelineEntry.Key;
 						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);
-							duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 
 						} 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);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * ColorTimeline.ENTRIES]);
 
 						} 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);
-							duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TwoColorTimeline.ENTRIES]);
 
 						} else
 							throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
@@ -589,47 +659,23 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)entry.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
+						var keyMapEnumerator = values.GetEnumerator();
+						bool hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) continue;
 						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
 							throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
 					}
@@ -638,40 +684,82 @@ namespace Spine {
 
 			// IK constraint timelines.
 			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);
-					duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * IkConstraintTimeline.ENTRIES]);
 				}
 			}
 
 			// Transform constraint timelines.
 			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);
-					duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TransformConstraintTimeline.ENTRIES]);
 				}
 			}
 
@@ -684,40 +772,22 @@ namespace Spine {
 					var timelineMap = (Dictionary<string, Object>)constraintMap.Value;
 					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
 						var values = (List<Object>)timelineEntry.Value;
+						var keyMapEnumerator = values.GetEnumerator();
+						bool hasNext = keyMapEnumerator.MoveNext();
+						if (!hasNext) continue;
 						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") {
-							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);
 						if (slotIndex == -1) throw new Exception("Slot not found: " + slotMap.Key);
 						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);
 							if (attachment == null) throw new Exception("Deform attachment not found: " + timelineMap.Key);
 							bool weighted = attachment.bones != null;
 							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;
-								if (!valueMap.ContainsKey("vertices")) {
+								if (!keyMap.ContainsKey("vertices")) {
 									deform = weighted ? new float[deformLength] : vertices;
 								} else {
 									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);
 									if (scale != 1) {
 										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);
-							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 timeline = new DrawOrderTimeline(values.Count);
 				int slotCount = skeletonData.slots.Count;
-				int frameIndex = 0;
+				int frame = 0;
 				foreach (Dictionary<string, Object> drawOrderMap in values) {
 					int[] drawOrder = null;
 					if (drawOrderMap.ContainsKey("offsets")) {
@@ -806,17 +886,17 @@ namespace Spine {
 						for (int i = slotCount - 1; i >= 0; i--)
 							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);
-				duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
 			}
 
 			// Event timeline.
 			if (map.ContainsKey("events")) {
 				var eventsMap = (List<Object>)map["events"];
 				var timeline = new EventTimeline(eventsMap.Count);
-				int frameIndex = 0;
+				int frame = 0;
 				foreach (Dictionary<string, Object> eventMap in eventsMap) {
 					EventData eventData = skeletonData.FindEvent((string)eventMap["name"]);
 					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
@@ -829,39 +909,97 @@ namespace Spine {
 						e.volume = GetFloat(eventMap, "volume", eventData.Volume);
 						e.balance = GetFloat(eventMap, "balance", eventData.Balance);
 					}
-					timeline.SetFrame(frameIndex++, e);
+					timeline.SetFrame(frame, e);
+					++frame;
 				}
 				timelines.Add(timeline);
-				duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 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);
 			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) {

+ 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>
 	public class Skin {
 		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);
 		internal readonly ExposedList<BoneData> bones = new ExposedList<BoneData>();
 		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>
 		public void SetAttachment (int slotIndex, string name, Attachment attachment) {
 			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);
 		}
 
@@ -104,13 +105,14 @@ namespace Spine {
 
 		/// <summary> Removes the attachment in the skin for the specified slot index and name, if any.</summary>
 		public void RemoveAttachment (int slotIndex, string name) {
-			if (slotIndex < 0) throw new ArgumentOutOfRangeException("slotIndex", "slotIndex must be >= 0");
 			attachments.Remove(new SkinKey(slotIndex, name));
 		}
 
 		/// <summary>Returns all attachments in this skin for the specified slot index.</summary>
 		/// <param name="slotIndex">The target slotIndex. To find the slot index, use <see cref="Spine.Skeleton.FindSlotIndex"/> or <see cref="Spine.SkeletonData.FindSlotIndex"/>
 		public void GetAttachments (int slotIndex, List<SkinEntry> attachments) {
+			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) {
 				SkinEntry entry = item.Value;
 				if (entry.slotIndex == slotIndex) attachments.Add(entry);
@@ -176,10 +178,14 @@ namespace Spine {
 		private struct SkinKey {
 			internal readonly int slotIndex;
 			internal readonly string name;
+			internal readonly int hashCode;
 
 			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.name = name;
+				this.hashCode = name.GetHashCode() + slotIndex * 37;
 			}
 		}
 
@@ -191,7 +197,7 @@ namespace Spine {
 			}
 
 			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 {
 
 	/// <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
 	/// across multiple skeletons.
 	/// </summary>

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

@@ -76,12 +76,8 @@ namespace Spine {
 			shearMix = constraint.shearMix;
 		}
 
-		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Apply () {
-			Update();
-		}
-
 		public void Update () {
+			if (rotateMix == 0 && translateMix == 0 && scaleMix == 0 && shearMix == 0) return;
 			if (data.local) {
 				if (data.relative)
 					ApplyRelativeLocal();
@@ -101,10 +97,9 @@ namespace Spine {
 			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 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) {
 					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.c = sin * a + cos * c;
 					bone.d = sin * b + cos * d;
-					modified = true;
 				}
 
 				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));
 					bone.worldX += (tx - bone.worldX) * translateMix;
 					bone.worldY += (ty - bone.worldY) * translateMix;
-					modified = true;
 				}
 
 				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;
 					bone.b *= s;
 					bone.d *= s;
-					modified = true;
 				}
 
 				if (shearMix > 0) {
@@ -152,10 +144,9 @@ namespace Spine {
 					float s = (float)Math.Sqrt(b * b + d * d);
 					bone.b = MathUtils.Cos(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 degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
 			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) {
 					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.c = sin * a + cos * c;
 					bone.d = sin * b + cos * d;
-					modified = true;
 				}
 
 				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));
 					bone.worldX += tx * translateMix;
 					bone.worldY += ty * translateMix;
-					modified = true;
 				}
 
 				if (scaleMix > 0) {
@@ -200,7 +188,6 @@ namespace Spine {
 					s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * scaleMix + 1;
 					bone.b *= s;
 					bone.d *= s;
-					modified = true;
 				}
 
 				if (shearMix > 0) {
@@ -213,10 +200,9 @@ namespace Spine {
 					float s = (float)Math.Sqrt(b * b + d * d);
 					bone.b = MathUtils.Cos(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;
 			Bone target = this.target;
 			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++) {
-				Bone bone = bonesItems[i];
+				Bone bone = bones[i];
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 
 				float rotation = bone.arotation;
@@ -263,9 +249,9 @@ namespace Spine {
 			float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 			Bone target = this.target;
 			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++) {
-				Bone bone = bonesItems[i];
+				Bone bone = bones[i];
 				if (!bone.appliedValid) bone.UpdateAppliedTransform();
 
 				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
 				// 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 timeline in animation.Timelines) {
 						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.
-				var currentAnimationIDs = new HashSet<int>();
+				var currentAnimationIDs = new HashSet<string>();
 				foreach (var animation in animations) {
 					currentAnimationIDs.Clear();
 					foreach (var timeline in animation.Timelines) {
 						if (timeline is EventTimeline) continue;
-						currentAnimationIDs.Add(timeline.PropertyId);
+						foreach (string propertyId in timeline.PropertyIds) {
+							currentAnimationIDs.Add(propertyId);
+						}
 					}
 
 					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) {
-				var t = new RotateTimeline(1);
-				t.BoneIndex = timeline.BoneIndex;
+				var t = new RotateTimeline(1, 0, timeline.BoneIndex);
 				t.SetFrame(0, 0, 0);
 				return t;
 			}
 
 			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);
 				return t;
 			}
 
 			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);
 				return t;
 			}
 
 			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);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, slotData.AttachmentName);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, slotData.R, slotData.G, slotData.B, slotData.A, slotData.R2, slotData.G2, slotData.B2);
 				return t;
 			}
 
 			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()) {
 					t.SetFrame(0, 0, new float[t.Attachment.Vertices.Length]);
 				} else {
@@ -204,35 +197,35 @@ namespace Spine.Unity.Examples {
 			}
 
 			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];
 				t.SetFrame(0, 0, ikConstraintData.Mix, ikConstraintData.Softness, ikConstraintData.BendDirection, ikConstraintData.Compress, ikConstraintData.Stretch);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix, data.ScaleMix, data.ShearMix);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, data.Position);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, data.Spacing);
 				return t;
 			}
 
 			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];
 				t.SetFrame(0, 0, data.RotateMix, data.TranslateMix);
 				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;
 
 						var mixMode = layerMixModes.GetArrayElementAtIndex(i);
-						var blendMode = layerBlendModes.GetArrayElementAtIndex(i);
 						rect.position += new Vector2(rect.width, 0);
 						rect.width = widthMixColumn;
 						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 {
 
 	#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 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,
 				int layerIndex, float time, bool isLooping, float weight) {
 
-				float clipDuration = clip.duration == 0 ? 1 : clip.duration;
 				float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
 				float lastTime = time - (Time.deltaTime * speedFactor);
 				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.
 		/// 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) {
-			const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
-			const int X = 1, Y = 2;
-
 			var frames = timeline.frames;
 			if (time < frames[0]) return Vector2.zero;
 
 			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);
@@ -67,7 +67,7 @@ namespace Spine.Unity.AnimationTools {
 				return xy;
 			}
 			else {
-				var boneData = skeletonData.bones.Items[timeline.boneIndex];
+				var boneData = skeletonData.bones.Items[timeline.BoneIndex];
 				return xy + new Vector2(boneData.x, boneData.y);
 			}
 		}
@@ -82,7 +82,7 @@ namespace Spine.Unity.AnimationTools {
 					continue;
 
 				var translateTimeline = timeline as TranslateTimeline;
-				if (translateTimeline != null && translateTimeline.boneIndex == boneIndex)
+				if (translateTimeline != null && translateTimeline.BoneIndex == boneIndex)
 					return translateTimeline;
 			}
 			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

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott