三国卡牌客户端基础资源仓库
hch
1 天以前 e250c3a8790dd521922244b5443a2aac7da89acf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated July 28, 2023. Replaces all prior versions.
 *
 * Copyright (c) 2013-2023, 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.Collections.Generic;
 
namespace Spine {
 
    /// <summary>
    /// <para>
    /// Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
    /// multiple animations on top of each other (layering).</para>
    /// <para>
    /// See <a href='http://esotericsoftware.com/spine-applying-animations/'>Applying Animations</a> in the Spine Runtimes Guide.</para>
    /// </summary>
    public class AnimationState {
        internal static readonly Animation EmptyAnimation = new Animation("<empty>", new ExposedList<Timeline>(), 0);
 
        /// 1) A previously applied timeline has set this property.<para />
        /// Result: Mix from the current pose to the timeline pose.
        internal const int Subsequent = 0;
        /// 1) This is the first timeline to set this property.<para />
        /// 2) The next track entry applied after this one does not have a timeline to set this property.<para />
        /// Result: Mix from the setup pose to the timeline pose.
        internal const int First = 1;
        /// 1) A previously applied timeline has set this property.<para />
        /// 2) The next track entry to be applied does have a timeline to set this property.<para />
        /// 3) The next track entry after that one does not have a timeline to set this property.<para />
        /// Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading
        /// animations that key the same property. A subsequent timeline will set this property using a mix.
        internal const int HoldSubsequent = 2;
        /// 1) This is the first timeline to set this property.<para />
        /// 2) The next track entry to be applied does have a timeline to set this property.<para />
        /// 3) The next track entry after that one does not have a timeline to set this property.<para />
        /// Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
        /// that key the same property. A subsequent timeline will set this property using a mix.
        internal const int HoldFirst = 3;
        /// 1) This is the first timeline to set this property.<para />
        /// 2) The next track entry to be applied does have a timeline to set this property.<para />
        /// 3) The next track entry after that one does have a timeline to set this property.<para />
        /// 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<para />
        /// Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
        /// 2 track entries in a row have a timeline that sets the same property.<para />
        /// Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
        /// "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
        /// (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed
        /// out position.
        internal const int HoldMix = 4;
 
        internal const int Setup = 1, Current = 2;
 
        protected AnimationStateData data;
        private readonly ExposedList<TrackEntry> tracks = new ExposedList<TrackEntry>();
        private readonly ExposedList<Event> events = new ExposedList<Event>();
        // difference to libgdx reference: delegates are used for event callbacks instead of 'final SnapshotArray<AnimationStateListener> listeners'.
        internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); }
        internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); }
        internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); }
        internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); }
        internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); }
        internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); }
 
        public delegate void TrackEntryDelegate (TrackEntry trackEntry);
        /// <summary>See <see href="http://esotericsoftware.com/spine-api-reference#AnimationStateListener-Methods">
        /// API Reference documentation pages here</see> for details. Usage in C# and spine-unity is explained
        /// <see href="http://esotericsoftware.com/spine-unity#Processing-AnimationState-Events">here</see>
        /// on the spine-unity documentation pages.</summary>
        public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
 
        public delegate void TrackEntryEventDelegate (TrackEntry trackEntry, Event e);
        public event TrackEntryEventDelegate Event;
 
        public void AssignEventSubscribersFrom (AnimationState src) {
            Event = src.Event;
            Start = src.Start;
            Interrupt = src.Interrupt;
            End = src.End;
            Dispose = src.Dispose;
            Complete = src.Complete;
        }
 
        public void AddEventSubscribersFrom (AnimationState src) {
            Event += src.Event;
            Start += src.Start;
            Interrupt += src.Interrupt;
            End += src.End;
            Dispose += src.Dispose;
            Complete += src.Complete;
        }
 
        // end of difference
        private readonly EventQueue queue; // Initialized by constructor.
        private readonly HashSet<string> propertyIds = new HashSet<string>();
        private bool animationsChanged;
        private float timeScale = 1;
        private int unkeyedState;
 
        private readonly Pool<TrackEntry> trackEntryPool = new Pool<TrackEntry>();
 
        public AnimationState (AnimationStateData data) {
            if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
            this.data = data;
            this.queue = new EventQueue(
                this,
                delegate { this.animationsChanged = true; },
                trackEntryPool
            );
        }
 
        /// <summary>
        /// Increments the track entry <see cref="TrackEntry.TrackTime"/>, setting queued animations as current if needed.</summary>
        /// <param name="delta">delta time</param>
        public void Update (float delta) {
            delta *= timeScale;
            TrackEntry[] tracksItems = tracks.Items;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                TrackEntry current = tracksItems[i];
                if (current == null) continue;
 
                current.animationLast = current.nextAnimationLast;
                current.trackLast = current.nextTrackLast;
 
                float currentDelta = delta * current.timeScale;
 
                if (current.delay > 0) {
                    current.delay -= currentDelta;
                    if (current.delay > 0) continue;
                    currentDelta = -current.delay;
                    current.delay = 0;
                }
 
                TrackEntry next = current.next;
                if (next != null) {
                    // When the next entry's delay is passed, change to the next entry, preserving leftover time.
                    float nextTime = current.trackLast - next.delay;
                    if (nextTime >= 0) {
                        next.delay = 0;
                        next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale;
                        current.trackTime += currentDelta;
                        SetCurrent(i, next, true);
                        while (next.mixingFrom != null) {
                            next.mixTime += delta;
                            next = next.mixingFrom;
                        }
                        continue;
                    }
                } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
                    // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
                    tracksItems[i] = null;
                    queue.End(current);
                    ClearNext(current);
                    continue;
                }
                if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) {
                    // End mixing from entries once all have completed.
                    TrackEntry from = current.mixingFrom;
                    current.mixingFrom = null;
                    if (from != null) from.mixingTo = null;
                    while (from != null) {
                        queue.End(from);
                        from = from.mixingFrom;
                    }
                }
 
                current.trackTime += currentDelta;
            }
 
            queue.Drain();
        }
 
        /// <summary>Returns true when all mixing from entries are complete.</summary>
        private bool UpdateMixingFrom (TrackEntry to, float delta) {
            TrackEntry from = to.mixingFrom;
            if (from == null) return true;
 
            bool finished = UpdateMixingFrom(from, delta);
 
            from.animationLast = from.nextAnimationLast;
            from.trackLast = from.nextTrackLast;
 
            // The from entry was applied at least once and the mix is complete.
            if (to.nextTrackLast != -1 && to.mixTime >= to.mixDuration) {
                // Mixing is complete for all entries before the from entry or the mix is instantaneous.
                if (from.totalAlpha == 0 || to.mixDuration == 0) {
                    to.mixingFrom = from.mixingFrom;
                    if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
                    to.interruptAlpha = from.interruptAlpha;
                    queue.End(from);
                }
                return finished;
            }
 
            from.trackTime += delta * from.timeScale;
            to.mixTime += delta;
            return false;
        }
 
        /// <summary>
        /// Poses the skeleton using the track entry animations.  The animation state is not changed, so can be applied to multiple
        /// skeletons to pose them identically.</summary>
        /// <returns>True if any animations were applied.</returns>
        public bool Apply (Skeleton skeleton) {
            if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
            if (animationsChanged) AnimationsChanged();
 
            ExposedList<Event> events = this.events;
            bool applied = false;
            TrackEntry[] tracksItems = tracks.Items;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                TrackEntry current = tracksItems[i];
                if (current == null || current.delay > 0) continue;
                applied = true;
 
                // Track 0 animations aren't for layering, so do not show the previously applied animations before the first key.
                MixBlend blend = i == 0 ? MixBlend.First : current.mixBlend;
 
                // Apply mixing from entries first.
                float alpha = current.alpha;
                if (current.mixingFrom != null)
                    alpha *= ApplyMixingFrom(current, skeleton, blend);
                else if (current.trackTime >= current.trackEnd && current.next == null) //
                    alpha = 0; // Set to setup pose the last time the entry will be applied.
                bool attachments = alpha >= current.alphaAttachmentThreshold;
 
                // Apply current entry.
                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;
                Timeline[] timelines = current.animation.timelines.Items;
                if ((i == 0 && alpha == 1) || blend == MixBlend.Add) {
                    if (i == 0) attachments = true;
                    for (int ii = 0; ii < timelineCount; ii++) {
                        Timeline timeline = timelines[ii];
                        if (timeline is AttachmentTimeline)
                            ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, attachments);
                        else
                            timeline.Apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.In);
                    }
                } else {
                    int[] timelineMode = current.timelineMode.Items;
 
                    bool shortestRotation = current.shortestRotation;
                    bool firstFrame = !shortestRotation && current.timelinesRotation.Count != timelineCount << 1;
                    if (firstFrame) current.timelinesRotation.Resize(timelineCount << 1);
                    float[] timelinesRotation = current.timelinesRotation.Items;
 
                    for (int ii = 0; ii < timelineCount; ii++) {
                        Timeline timeline = timelines[ii];
                        MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup;
                        RotateTimeline rotateTimeline = timeline as RotateTimeline;
                        if (!shortestRotation && rotateTimeline != null)
                            ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation,
                                                ii << 1, firstFrame);
                        else if (timeline is AttachmentTimeline)
                            ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, attachments);
                        else
                            timeline.Apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.In);
                    }
                }
                QueueEvents(current, animationTime);
                events.Clear(false);
                current.nextAnimationLast = animationTime;
                current.nextTrackLast = current.trackTime;
            }
 
            // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so
            // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or
            // the time is before the first key).
            int setupState = unkeyedState + Setup;
            Slot[] slots = skeleton.slots.Items;
            for (int i = 0, n = skeleton.slots.Count; i < n; i++) {
                Slot slot = slots[i];
                if (slot.attachmentState == setupState) {
                    string attachmentName = slot.data.attachmentName;
                    slot.Attachment = (attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName));
                }
            }
            unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.
 
            queue.Drain();
            return applied;
        }
 
        /// <summary>Version of <see cref="Apply"/> only applying and updating time at
        /// EventTimelines for lightweight off-screen updates.</summary>
        /// <param name="issueEvents">When set to false, only animation times of TrackEntries are updated.</param>
        // Note: This method is not part of the libgdx reference implementation.
        public bool ApplyEventTimelinesOnly (Skeleton skeleton, bool issueEvents = true) {
            if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
 
            ExposedList<Event> events = this.events;
            bool applied = false;
            TrackEntry[] tracksItems = tracks.Items;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                TrackEntry current = tracksItems[i];
                if (current == null || current.delay > 0) continue;
                applied = true;
 
                // Apply mixing from entries first.
                if (current.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(current, skeleton, issueEvents);
 
                // Apply current entry.
                float animationLast = current.animationLast, animationTime = current.AnimationTime;
 
                if (issueEvents) {
                    int timelineCount = current.animation.timelines.Count;
                    Timeline[] timelines = current.animation.timelines.Items;
                    for (int ii = 0; ii < timelineCount; ii++) {
                        Timeline timeline = timelines[ii];
                        if (timeline is EventTimeline)
                            timeline.Apply(skeleton, animationLast, animationTime, events, 1.0f, MixBlend.Setup, MixDirection.In);
                    }
                    QueueEvents(current, animationTime);
                    events.Clear(false);
                }
                current.nextAnimationLast = animationTime;
                current.nextTrackLast = current.trackTime;
            }
 
            if (issueEvents)
                queue.Drain();
            return applied;
        }
 
        private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) {
            TrackEntry from = to.mixingFrom;
            if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, blend);
 
            float mix;
            if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
                mix = 1;
                if (blend == MixBlend.First) blend = MixBlend.Setup; // Tracks > 0 are transparent and can't reset to setup pose.
            } else {
                mix = to.mixTime / to.mixDuration;
                if (mix > 1) mix = 1;
                if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend.
            }
 
            bool attachments = mix < from.mixAttachmentThreshold, drawOrder = mix < from.mixDrawOrderThreshold;
            int timelineCount = from.animation.timelines.Count;
            Timeline[] timelines = from.animation.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++)
                    timelines[i].Apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.Out);
            } else {
                int[] timelineMode = from.timelineMode.Items;
                TrackEntry[] timelineHoldMix = from.timelineHoldMix.Items;
 
                bool shortestRotation = from.shortestRotation;
                bool firstFrame = !shortestRotation && from.timelinesRotation.Count != timelineCount << 1;
                if (firstFrame) from.timelinesRotation.Resize(timelineCount << 1);
                float[] timelinesRotation = from.timelinesRotation.Items;
 
                from.totalAlpha = 0;
                for (int i = 0; i < timelineCount; i++) {
                    Timeline timeline = timelines[i];
                    MixDirection direction = MixDirection.Out;
                    MixBlend timelineBlend;
                    float alpha;
                    switch (timelineMode[i]) {
                    case AnimationState.Subsequent:
                        if (!drawOrder && timeline is DrawOrderTimeline) continue;
                        timelineBlend = blend;
                        alpha = alphaMix;
                        break;
                    case AnimationState.First:
                        timelineBlend = MixBlend.Setup;
                        alpha = alphaMix;
                        break;
                    case AnimationState.HoldSubsequent:
                        timelineBlend = blend;
                        alpha = alphaHold;
                        break;
                    case AnimationState.HoldFirst:
                        timelineBlend = MixBlend.Setup;
                        alpha = alphaHold;
                        break;
                    default: // HoldMix
                        timelineBlend = MixBlend.Setup;
                        TrackEntry holdMix = timelineHoldMix[i];
                        alpha = alphaHold * Math.Max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
                        break;
                    }
                    from.totalAlpha += alpha;
                    RotateTimeline rotateTimeline = timeline as RotateTimeline;
                    if (!shortestRotation && rotateTimeline != null) {
                        ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1,
                            firstFrame);
                    } else if (timeline is AttachmentTimeline) {
                        ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend,
                            attachments && alpha >= from.alphaAttachmentThreshold);
                    } else {
                        if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup)
                            direction = MixDirection.In;
                        timeline.Apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
                    }
                }
            }
 
            if (to.mixDuration > 0) QueueEvents(from, animationTime);
            this.events.Clear(false);
            from.nextAnimationLast = animationTime;
            from.nextTrackLast = from.trackTime;
 
            return mix;
        }
 
        /// <summary>Version of <see cref="ApplyMixingFrom"/> only applying and updating time at
        /// EventTimelines for lightweight off-screen updates.</summary>
        /// <param name="issueEvents">When set to false, only animation times of TrackEntries are updated.</param>
        // Note: This method is not part of the libgdx reference implementation.
        private float ApplyMixingFromEventTimelinesOnly (TrackEntry to, Skeleton skeleton, bool issueEvents) {
            TrackEntry from = to.mixingFrom;
            if (from.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(from, skeleton, issueEvents);
 
 
            float mix;
            if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
                mix = 1;
            } else {
                mix = to.mixTime / to.mixDuration;
                if (mix > 1) mix = 1;
            }
 
            ExposedList<Event> eventBuffer = mix < from.eventThreshold ? this.events : null;
            if (eventBuffer == null) return mix;
 
            float animationLast = from.animationLast, animationTime = from.AnimationTime;
            if (issueEvents) {
                int timelineCount = from.animation.timelines.Count;
                Timeline[] timelines = from.animation.timelines.Items;
                for (int i = 0; i < timelineCount; i++) {
                    Timeline timeline = timelines[i];
                    if (timeline is EventTimeline)
                        timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, 0, MixBlend.Setup, MixDirection.Out);
                }
 
                if (to.mixDuration > 0) QueueEvents(from, animationTime);
                this.events.Clear(false);
            }
            from.nextAnimationLast = animationTime;
            from.nextTrackLast = from.trackTime;
 
            return mix;
        }
 
        /// <summary> Applies the attachment timeline and sets <see cref="Slot.attachmentState"/>.</summary>
        /// <param name="attachments">False when: 1) the attachment timeline is mixing out, 2) mix &lt; attachmentThreshold, and 3) the timeline
        /// is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
        /// timelines see any deform.</param>
        private void ApplyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend,
            bool attachments) {
 
            Slot slot = skeleton.slots.Items[timeline.SlotIndex];
            if (!slot.bone.active) return;
 
            float[] frames = timeline.frames;
            if (time < frames[0]) { // Time is before first frame.
                if (blend == MixBlend.Setup || blend == MixBlend.First)
                    SetAttachment(skeleton, slot, slot.data.attachmentName, attachments);
            } else
                SetAttachment(skeleton, slot, timeline.AttachmentNames[Timeline.Search(frames, time)], attachments);
 
            // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
            if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + Setup;
        }
 
        private void SetAttachment (Skeleton skeleton, Slot slot, String attachmentName, bool attachments) {
            slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName);
            if (attachments) slot.attachmentState = unkeyedState + Current;
        }
 
        /// <summary>
        /// Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
        /// the first time the mixing was applied.</summary>
        static private void ApplyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend,
            float[] timelinesRotation, int i, bool firstFrame) {
 
            if (firstFrame) timelinesRotation[i] = 0;
 
            if (alpha == 1) {
                timeline.Apply(skeleton, 0, time, null, 1, blend, MixDirection.In);
                return;
            }
 
            Bone bone = skeleton.bones.Items[timeline.BoneIndex];
            if (!bone.active) return;
 
            float[] frames = timeline.frames;
            float r1, r2;
            if (time < frames[0]) { // Time is before first frame.
                switch (blend) {
                case MixBlend.Setup:
                    bone.rotation = bone.data.rotation;
                    goto default; // Fall through.
                default:
                    return;
                case MixBlend.First:
                    r1 = bone.rotation;
                    r2 = bone.data.rotation;
                    break;
                }
            } else {
                r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation;
                r2 = bone.data.rotation + timeline.GetCurveValue(time);
            }
 
            // Mix between rotations using the direction of the shortest route on the first frame.
            float total, diff = r2 - r1;
            diff -= (float)Math.Ceiling(diff / 360 - 0.5f) * 360;
            if (diff == 0) {
                total = timelinesRotation[i];
            } else {
                float lastTotal, lastDiff;
                if (firstFrame) {
                    lastTotal = 0;
                    lastDiff = diff;
                } else {
                    lastTotal = timelinesRotation[i];
                    lastDiff = timelinesRotation[i + 1];
                }
                float loops = lastTotal - lastTotal % 360;
                total = diff + loops;
                bool current = diff >= 0, dir = lastTotal >= 0;
                if (Math.Abs(lastDiff) <= 90 && Math.Sign(lastDiff) != Math.Sign(diff)) {
                    if (Math.Abs(lastTotal - loops) > 180) {
                        total += 360 * Math.Sign(lastTotal);
                        dir = current;
                    } else if (loops != 0)
                        total -= 360 * Math.Sign(lastTotal);
                    else
                        dir = current;
                }
                if (dir != current) total += 360 * Math.Sign(lastTotal);
                timelinesRotation[i] = total;
            }
            timelinesRotation[i + 1] = diff;
            bone.rotation = r1 + total * alpha;
        }
 
        private void QueueEvents (TrackEntry entry, float animationTime) {
            float animationStart = entry.animationStart, animationEnd = entry.animationEnd;
            float duration = animationEnd - animationStart;
            float trackLastWrapped = entry.trackLast % duration;
 
            // Queue events before complete.
            Event[] eventsItems = this.events.Items;
            int i = 0, n = events.Count;
            for (; i < n; i++) {
                Event e = eventsItems[i];
                if (e.time < trackLastWrapped) break;
                if (e.time > animationEnd) continue; // Discard events outside animation start/end.
                queue.Event(entry, e);
            }
 
            // Queue complete if completed a loop iteration or the animation.
            bool complete = false;
            if (entry.loop) {
                if (duration == 0)
                    complete = true;
                else {
                    int cycles = (int)(entry.trackTime / duration);
                    complete = cycles > 0 && cycles > (int)(entry.trackLast / duration);
                }
            } else
                complete = animationTime >= animationEnd && entry.animationLast < animationEnd;
            if (complete) queue.Complete(entry);
 
            // Queue events after complete.
            for (; i < n; i++) {
                Event e = eventsItems[i];
                if (e.time < animationStart) continue; // Discard events outside animation start/end.
                queue.Event(entry, eventsItems[i]);
            }
        }
 
        /// <summary>
        /// <para>Removes all animations from all tracks, leaving skeletons in their current pose.</para>
        /// <para>
        /// It may be desired to use <see cref="AnimationState.SetEmptyAnimations(float)"/> to mix the skeletons back to the setup pose,
        /// rather than leaving them in their current pose.</para>
        /// </summary>
        public void ClearTracks () {
            bool oldDrainDisabled = queue.drainDisabled;
            queue.drainDisabled = true;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                ClearTrack(i);
            }
            tracks.Clear();
            queue.drainDisabled = oldDrainDisabled;
            queue.Drain();
        }
 
        /// <summary>
        /// <para>Removes all animations from the track, leaving skeletons in their current pose.</para>
        /// <para>
        /// It may be desired to use <see cref="AnimationState.SetEmptyAnimation(int, float)"/> to mix the skeletons back to the setup pose,
        /// rather than leaving them in their current pose.</para>
        /// </summary>
        public void ClearTrack (int trackIndex) {
            if (trackIndex >= tracks.Count) return;
            TrackEntry current = tracks.Items[trackIndex];
            if (current == null) return;
 
            queue.End(current);
 
            ClearNext(current);
 
            TrackEntry entry = current;
            while (true) {
                TrackEntry from = entry.mixingFrom;
                if (from == null) break;
                queue.End(from);
                entry.mixingFrom = null;
                entry.mixingTo = null;
                entry = from;
            }
 
            tracks.Items[current.trackIndex] = null;
 
            queue.Drain();
        }
 
        /// <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);
                current.mixingFrom = from;
                from.mixingTo = current;
                current.mixTime = 0;
 
                // Store the interrupted mix percentage.
                if (from.mixingFrom != null && from.mixDuration > 0)
                    current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration);
 
                from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in.
            }
 
            queue.Start(current); // triggers AnimationsChanged
        }
 
        /// <summary>Sets an animation by name. <seealso cref="SetAnimation(int, Animation, bool)" /></summary>
        public TrackEntry SetAnimation (int trackIndex, string animationName, bool loop) {
            Animation animation = data.skeletonData.FindAnimation(animationName);
            if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName");
            return SetAnimation(trackIndex, animation, loop);
        }
 
        /// <summary>Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
        /// applied to a skeleton, it is replaced (not mixed from).</summary>
        /// <param name="loop">If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
        ///          duration. In either case<see cref="TrackEntry.TrackEnd"/> determines when the track is cleared.</param>
        /// <returns> A track entry to allow further customization of animation playback. References to the track entry must not be kept
        ///          after the <see cref="AnimationState.Dispose"/> event occurs.</returns>
        public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) {
            if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null.");
            bool interrupt = true;
            TrackEntry current = ExpandToIndex(trackIndex);
            if (current != null) {
                if (current.nextTrackLast == -1) {
                    // Don't mix from an entry that was never applied.
                    tracks.Items[trackIndex] = current.mixingFrom;
                    queue.Interrupt(current);
                    queue.End(current);
                    ClearNext(current);
                    current = current.mixingFrom;
                    interrupt = false; // mixingFrom is current again, but don't interrupt it twice.
                } else
                    ClearNext(current);
            }
            TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, current);
            SetCurrent(trackIndex, entry, interrupt);
            queue.Drain();
            return entry;
        }
 
        /// <summary>Queues an animation by name.</summary>
        /// <seealso cref="AddAnimation(int, Animation, bool, float)" />
        public TrackEntry AddAnimation (int trackIndex, string animationName, bool loop, float delay) {
            Animation animation = data.skeletonData.FindAnimation(animationName);
            if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName");
            return AddAnimation(trackIndex, animation, loop, delay);
        }
 
        /// <summary>Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is
        /// 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 <see cref="AnimationStateData"/> plus the specified <c>Delay</c> (ie the mix
        /// ends at (<c>Delay</c> = 0) or before (<c>Delay</c> &lt; 0) the previous track entry duration). If the
        /// previous entry is looping, its next loop completion is used instead of its duration.
        /// </param>
        /// <returns>A track entry to allow further customization of animation playback. References to the track entry must not be kept
        /// after the <see cref="AnimationState.Dispose"/> event occurs.</returns>
        public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) {
            if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null.");
 
            TrackEntry last = ExpandToIndex(trackIndex);
            if (last != null) {
                while (last.next != null)
                    last = last.next;
            }
 
            TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, last);
 
            if (last == null) {
                SetCurrent(trackIndex, entry, true);
                queue.Drain();
            } else {
                last.next = entry;
                entry.previous = last;
                if (delay <= 0) delay += last.TrackComplete - entry.mixDuration;
            }
 
            entry.delay = delay;
            return entry;
        }
 
        /// <summary>
        /// <para>Sets an empty animation for a track, discarding any queued animations, and sets the track entry's
        /// <see cref="TrackEntry.getMixDuration()"/>. An empty animation has no timelines and serves as a placeholder for mixing in or out.</para>
        /// <para>
        /// Mixing out is done by setting an empty animation with a mix duration using either <see cref="AnimationState.SetEmptyAnimation(int, float)"/>,
        /// <see cref="AnimationState.SetEmptyAnimations(float)"/>, or <see cref="AnimationState.AddEmptyAnimation(int, float, float)"/>. Mixing to an empty animation causes
        /// the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation
        /// transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of
        /// 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, 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;
            entry.trackEnd = mixDuration;
            return entry;
        }
 
        /// <summary>
        /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
        /// <see cref="TrackEntry.MixDuration"/>. If the track is empty, it is equivalent to calling
        /// <see cref="AnimationState.SetEmptyAnimation(int, float)"/>.</summary>
        /// <seealso cref="AnimationState.SetEmptyAnimation(int, float)"/>
        /// <param name="trackIndex">Track number.</param>
        /// <param name="mixDuration">Mix duration.</param>
        /// <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 plus the specified <c>Delay</c> (ie the mix ends at (<c>Delay</c> = 0) or
        /// before (<c>Delay</c> &lt; 0) the previous track entry duration). If the previous entry is looping, its next
        /// loop completion is used instead of its duration.</param>
        /// <returns> A track entry to allow further customization of animation playback. References to the track entry must not be kept
        /// after the <see cref="AnimationState.Dispose"/> event occurs.
        /// </returns>
        public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) {
            TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay);
            if (delay <= 0) entry.delay += entry.mixDuration - mixDuration;
            entry.mixDuration = mixDuration;
            entry.trackEnd = mixDuration;
            return entry;
        }
 
        /// <summary>
        /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix
        /// duration.</summary>
        public void SetEmptyAnimations (float mixDuration) {
            bool oldDrainDisabled = queue.drainDisabled;
            queue.drainDisabled = true;
            TrackEntry[] tracksItems = tracks.Items;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                TrackEntry current = tracksItems[i];
                if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration);
            }
            queue.drainDisabled = oldDrainDisabled;
            queue.Drain();
        }
 
        private TrackEntry ExpandToIndex (int index) {
            if (index < tracks.Count) return tracks.Items[index];
            tracks.Resize(index + 1);
            return null;
        }
 
        /// <summary>Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values.</summary>
        /// <param name="last">May be null.</param>
        private TrackEntry NewTrackEntry (int trackIndex, Animation animation, bool loop, TrackEntry last) {
            TrackEntry entry = trackEntryPool.Obtain();
            entry.trackIndex = trackIndex;
            entry.animation = animation;
            entry.loop = loop;
            entry.holdPrevious = false;
 
            entry.eventThreshold = 0;
            entry.alphaAttachmentThreshold = 0;
            entry.mixAttachmentThreshold = 0;
            entry.mixDrawOrderThreshold = 0;
 
            entry.animationStart = 0;
            entry.animationEnd = animation.Duration;
            entry.animationLast = -1;
            entry.nextAnimationLast = -1;
 
            entry.delay = 0;
            entry.trackTime = 0;
            entry.trackLast = -1;
            entry.nextTrackLast = -1;
            entry.trackEnd = float.MaxValue;
            entry.timeScale = 1;
 
            entry.alpha = 1;
            entry.interruptAlpha = 1;
            entry.mixTime = 0;
            entry.mixDuration = last == null ? 0 : data.GetMix(last.animation, animation);
            entry.mixBlend = MixBlend.Replace;
            return entry;
        }
 
        /// <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) {
            TrackEntry next = entry.next;
            while (next != null) {
                queue.Dispose(next);
                next = next.next;
            }
            entry.next = null;
        }
 
        private void AnimationsChanged () {
            animationsChanged = false;
 
            // Process in the order that animations are applied.
            propertyIds.Clear();
            int n = tracks.Count;
            TrackEntry[] tracksItems = tracks.Items;
            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.
                    entry = entry.mixingFrom;
                do {
                    if (entry.mixingTo == null || entry.mixBlend != MixBlend.Add) ComputeHold(entry);
                    entry = entry.mixingTo;
                } while (entry != null);
            }
        }
 
        private void ComputeHold (TrackEntry entry) {
            TrackEntry to = entry.mixingTo;
            Timeline[] timelines = entry.animation.timelines.Items;
            int timelinesCount = entry.animation.timelines.Count;
            int[] timelineMode = entry.timelineMode.Resize(timelinesCount).Items;
            entry.timelineHoldMix.Clear();
            TrackEntry[] timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items;
            HashSet<string> propertyIds = this.propertyIds;
 
            if (to != null && to.holdPrevious) {
                for (int i = 0; i < timelinesCount; i++)
                    timelineMode[i] = propertyIds.AddAll(timelines[i].PropertyIds) ? AnimationState.HoldFirst : AnimationState.HoldSubsequent;
 
                return;
            }
 
            // outer:
            for (int i = 0; i < timelinesCount; i++) {
                Timeline timeline = timelines[i];
                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(ids)) {
                    timelineMode[i] = AnimationState.First;
                } else {
                    for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
                        if (next.animation.HasTimeline(ids)) continue;
                        if (next.mixDuration > 0) {
                            timelineMode[i] = AnimationState.HoldMix;
                            timelineHoldMix[i] = next;
                            goto continue_outer; // continue outer;
                        }
                        break;
                    }
                    timelineMode[i] = AnimationState.HoldFirst;
                }
                continue_outer: { }
            }
        }
 
        /// <returns>The track entry for the animation currently playing on the track, or null if no animation is currently playing.</returns>
        public TrackEntry GetCurrent (int trackIndex) {
            if (trackIndex >= tracks.Count) return null;
            return tracks.Items[trackIndex];
        }
 
        /// <summary> Discards all listener notifications that have not yet been delivered. This can be useful to call from an
        /// AnimationState event subscriber when it is known that further notifications that may have been already queued for delivery
        /// are not wanted because new animations are being set.
        /// </summary>
        public void ClearListenerNotifications () {
            queue.Clear();
        }
 
        /// <summary>
        /// <para>Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower
        /// or faster. Defaults to 1.</para>
        /// <para>
        /// See TrackEntry <see cref="TrackEntry.TimeScale"/> for affecting a single animation.</para>
        /// </summary>
        public float TimeScale { get { return timeScale; } set { timeScale = value; } }
 
        /// <summary>The <see cref="AnimationStateData"/> to look up mix durations.</summary>
        public AnimationStateData Data {
            get {
                return data;
            }
            set {
                if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
                this.data = value;
            }
        }
 
        /// <summary>A list of tracks that have animations, which may contain nulls.</summary>
        public ExposedList<TrackEntry> Tracks { get { return tracks; } }
 
        override public string ToString () {
            System.Text.StringBuilder buffer = new System.Text.StringBuilder();
            TrackEntry[] tracksItems = tracks.Items;
            for (int i = 0, n = tracks.Count; i < n; i++) {
                TrackEntry entry = tracksItems[i];
                if (entry == null) continue;
                if (buffer.Length > 0) buffer.Append(", ");
                buffer.Append(entry.ToString());
            }
            if (buffer.Length == 0) return "<none>";
            return buffer.ToString();
        }
    }
 
    /// <summary>
    /// <para>
    /// Stores settings and other state for the playback of an animation on an <see cref="AnimationState"/> track.</para>
    /// <para>
    /// References to a track entry must not be kept after the <see cref="AnimationStateListener.Dispose(TrackEntry)"/> event occurs.</para>
    /// </summary>
    public class TrackEntry : Pool<TrackEntry>.IPoolable {
        internal Animation animation;
 
        internal TrackEntry previous, next, mixingFrom, mixingTo;
        // difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'.
        /// <summary>See <see href="http://esotericsoftware.com/spine-api-reference#AnimationStateListener-Methods">
        /// API Reference documentation pages here</see> for details. Usage in C# and spine-unity is explained
        /// <see href="http://esotericsoftware.com/spine-unity#Processing-AnimationState-Events">here</see>
        /// on the spine-unity documentation pages.</summary>
        public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
        public event AnimationState.TrackEntryEventDelegate Event;
        internal void OnStart () { if (Start != null) Start(this); }
        internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); }
        internal void OnEnd () { if (End != null) End(this); }
        internal void OnDispose () { if (Dispose != null) Dispose(this); }
        internal void OnComplete () { if (Complete != null) Complete(this); }
        internal void OnEvent (Event e) { if (Event != null) Event(this, e); }
 
        internal int trackIndex;
 
        internal bool loop, holdPrevious, reverse, shortestRotation;
        internal float eventThreshold, mixAttachmentThreshold, alphaAttachmentThreshold, mixDrawOrderThreshold;
        internal float animationStart, animationEnd, animationLast, nextAnimationLast;
        internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f;
        internal float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
        internal MixBlend mixBlend = MixBlend.Replace;
        internal readonly ExposedList<int> timelineMode = new ExposedList<int>();
        internal readonly ExposedList<TrackEntry> timelineHoldMix = new ExposedList<TrackEntry>();
        internal readonly ExposedList<float> timelinesRotation = new ExposedList<float>();
 
        // IPoolable.Reset()
        public void Reset () {
            previous = null;
            next = null;
            mixingFrom = null;
            mixingTo = null;
            animation = null;
            // replaces 'listener = null;' since delegates are used for event callbacks
            Start = null;
            Interrupt = null;
            End = null;
            Dispose = null;
            Complete = null;
            Event = null;
            timelineMode.Clear();
            timelineHoldMix.Clear();
            timelinesRotation.Clear();
        }
 
        /// <summary>The index of the track where this entry is either current or queued.</summary>
        /// <seealso cref="AnimationState.GetCurrent(int)"/>
        public int TrackIndex { get { return trackIndex; } }
 
        /// <summary>The animation to apply for this track entry.</summary>
        public Animation Animation { get { return animation; } }
 
        /// <summary>
        /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
        /// duration.</summary>
        public bool Loop { get { return loop; } set { loop = value; } }
 
        /// <summary>
        /// <para>
        /// Seconds to postpone playing the animation. When this track entry is the current track entry, <c>Delay</c>
        /// postpones incrementing the <see cref="TrackEntry.TrackTime"/>. When this track entry is queued, <c>Delay</c> is the time from
        /// the start of the previous animation to when this track entry will become the current track entry (ie when the previous
        /// track entry <see cref="TrackEntry.TrackTime"/> &gt;= this track entry's <c>Delay</c>).</para>
        /// <para>
        /// <see cref="TrackEntry.TimeScale"/> affects the delay.</para>
        /// <para>
        /// When using <see cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with a <c>delay</c> &lt;= 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.</para></summary>
        public float Delay { get { return delay; } set { delay = value; } }
 
        /// <summary>
        /// Current time in seconds this track entry has been the current track entry. The track time determines
        /// <see cref="TrackEntry.AnimationTime"/>. The track time can be set to start the animation at a time other than 0, without affecting
        /// looping.</summary>
        public float TrackTime { get { return trackTime; } set { trackTime = value; } }
 
        /// <summary>
        /// <para>
        /// The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float
        /// value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time
        /// is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the
        /// properties keyed by the animation are set to the setup pose and the track is cleared.</para>
        /// <para>
        /// It may be desired to use <see cref="AnimationState.AddEmptyAnimation(int, float, float)"/>  rather than have the animation
        /// abruptly cease being applied.</para>
        /// </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>
        /// <para>
        /// When changing the <c>AnimationStart</c> time, it often makes sense to set <see cref="TrackEntry.AnimationLast"/> to the same
        /// value to prevent timeline keys before the start time from triggering.</para>
        /// </summary>
        public float AnimationStart { get { return animationStart; } set { animationStart = value; } }
 
        /// <summary>
        /// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
        /// loop back to <see cref="TrackEntry.AnimationStart"/> at this time. Defaults to the animation <see cref="Animation.Duration"/>.
        /// </summary>
        public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } }
 
        /// <summary>
        /// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this
        /// animation is applied, event timelines will fire all events between the <c>AnimationLast</c> time (exclusive) and
        /// <c>AnimationTime</c> (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation
        /// is applied.</summary>
        public float AnimationLast {
            get { return animationLast; }
            set {
                animationLast = value;
                nextAnimationLast = value;
            }
        }
 
        /// <summary>
        /// Uses <see cref="TrackEntry.TrackTime"/> to compute the <c>AnimationTime</c>. When the <c>TrackTime</c> is 0, the
        /// <c>AnimationTime</c> is equal to the <c>AnimationStart</c> time.
        /// <para>
        /// The <c>animationTime</c> is between <see cref="AnimationStart"/> and <see cref="AnimationEnd"/>, except if this
        /// track entry is non-looping and <see cref="AnimationEnd"/> is >= to the animation <see cref="Animation.Duration"/>, then
        /// <c>animationTime</c> continues to increase past <see cref="AnimationEnd"/>.</para>
        /// </summary>
        public float AnimationTime {
            get {
                if (loop) {
                    float duration = animationEnd - animationStart;
                    if (duration == 0) return animationStart;
                    return (trackTime % duration) + animationStart;
                }
                float animationTime = trackTime + animationStart;
                return animationEnd >= animation.duration ? animationTime : Math.Min(animationTime, animationEnd);
            }
        }
 
        /// <summary>
        /// <para>
        /// 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 &lt; 0 are not supported. To play an animation in reverse, use <see cref="Reverse"/>.</para>
        /// <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, bool, float)"/> with a <c>Delay</c> &lt;= 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>
        /// </summary>
        public float TimeScale { get { return timeScale; } set { timeScale = value; } }
 
        /// <summary>
        /// <para>
        /// Values &lt; 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults
        /// to 1, which overwrites the skeleton's current pose with this animation.</para>
        /// <para>
        /// Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to
        /// use alpha on track 0 if the skeleton pose is from the last frame render.</para>
        /// </summary>
        public float Alpha { get { return alpha; } set { alpha = value; } }
 
        public float InterruptAlpha { get { return interruptAlpha; } }
 
        /// <summary>
        /// When the mix percentage (<see cref="TrackEntry.MixTime"/> / <see cref="TrackEntry.MixDuration"/>) is less than the
        /// <c>EventThreshold</c>, event timelines are applied while this animation is being mixed out. Defaults to 0, so event
        /// timelines are not applied while this animation is being mixed out.
        /// </summary>
        public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } }
 
        /// <summary>
        /// When <see cref="Alpha"/> is greater than <c>AlphaAttachmentThreshold</c>, attachment timelines are applied.
        /// Defaults to 0, so attachment timelines are always applied.
        /// </summary>
        public float AlphaAttachmentThreshold { get { return alphaAttachmentThreshold; } set { alphaAttachmentThreshold = value; } }
 
        /// <summary>
        /// When the mix percentage (<see cref="TrackEntry.MixTime"/> / <see cref="TrackEntry.MixDuration"/>) is less than the
        /// <c>MixAttachmentThreshold</c>, attachment timelines are applied while this animation is being mixed out. Defaults
        /// to 0, so attachment timelines are not applied while this animation is being mixed out.
        /// </summary>
        public float MixAttachmentThreshold { get { return mixAttachmentThreshold; } set { mixAttachmentThreshold = value; } }
 
        /// <summary>
        /// When the mix percentage (<see cref="TrackEntry.MixTime"/> / <see cref="TrackEntry.MixDuration"/>) is less than the
        /// <c>MixDrawOrderThreshold</c>, draw order timelines are applied while this animation is being mixed out. Defaults to
        /// 0, so draw order timelines are not applied while this animation is being mixed out.
        /// </summary>
        public float MixDrawOrderThreshold { get { return mixDrawOrderThreshold; } set { mixDrawOrderThreshold = value; } }
 
        /// <summary>
        /// The animation queued to start after this animation, or null if there is none. <c>next</c> 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. <c>previous</c> makes up a doubly linked list.</summary>
        public TrackEntry Previous { get { return previous; } }
 
        /// <summary>Returns true if this track entry has been applied at least once.</summary>
        /// <seealso cref="AnimationState.Apply(Skeleton)"/>
        public bool WasApplied {
            get { return nextTrackLast != -1; }
        }
 
        /// <summary>Returns true if there is a <see cref="Next"/> track entry that will become the current track entry during the
        /// next <see cref="AnimationState.Update(float)"/>.</summary>
        public bool IsNextReady {
            get {
                return (next != null) && (nextTrackLast - next.delay >= 0);
            }
        }
 
        /// <summary>
        /// Returns true if at least one loop has been completed.</summary>
        /// <seealso cref="TrackEntry.Complete"/>
        public bool IsComplete {
            get { return trackTime >= animationEnd - animationStart; }
        }
 
        /// <summary>
        /// Seconds from 0 to the <see cref="TrackEntry.MixDuration"/> when mixing from the previous animation to this animation. May be
        /// slightly more than <c>MixDuration</c> when the mix is complete.</summary>
        public float MixTime { get { return mixTime; } set { mixTime = value; } }
 
        /// <summary>
        /// <para>
        /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData
        /// <see cref="AnimationStateData.GetMix(Animation, Animation)"/> based on the animation before this animation (if any).</para>
        /// <para>
        /// The <c>MixDuration</c> can be set manually rather than use the value from
        /// <see cref="AnimationStateData.GetMix(Animation, Animation)"/>. In that case, the <c>MixDuration</c> 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 <c>Delay</c> &lt;= 0, the
        /// <see cref="TrackEntry.Delay"/> is set using the mix duration from the <see cref=" AnimationStateData"/>. If <c>mixDuration</c> is set
        /// afterward, the delay may need to be adjusted. For example:</para>
        /// <para><c>entry.Delay = entry.previous.TrackComplete - entry.MixDuration;</c></para>
        /// <para>Alternatively, <see cref="SetMixDuration(float, float)"/> can be used to recompute the delay:</para>
        /// <para><c>entry.SetMixDuration(0.25f, 0);</c></para>
        /// </summary>
        public float MixDuration { get { return mixDuration; } set { mixDuration = value; } }
 
        /// <summary>Sets both <see cref="MixDuration"/> and <see cref="Delay"/>.</summary>
        /// <param name="delay">If > 0, sets <see cref="TrackEntry.Delay"/>. If &lt;= 0, the delay set is the duration of the previous track
        ///        entry minus the specified mix duration plus the specified<c> delay</c> (ie the mix ends at
        ///        (<c>delay</c> = 0) or before (<c>delay</c> &lt; 0) the previous track entry duration). If the previous
        ///        entry is looping, its next loop completion is used instead of its duration.</param>
        public void SetMixDuration (float mixDuration, float delay) {
            this.mixDuration = mixDuration;
            if (previous != null && delay <= 0) delay += previous.TrackComplete - mixDuration;
            this.delay = delay;
        }
 
        /// <summary>
        /// <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 <c>MixBlend</c> can be set for a new track entry only before <see cref="AnimationState.Apply(Skeleton)"/> is first
        ///  called.</para>
        /// </summary>
        public MixBlend MixBlend { get { return mixBlend; } set { mixBlend = value; } }
 
        /// <summary>
        /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
        /// mixing is currently occurring. When mixing from multiple animations, <c>MixingFrom</c> makes up a linked list.</summary>
        public TrackEntry MixingFrom { get { return mixingFrom; } }
 
        /// <summary>
        /// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
        /// currently occurring. When mixing to multiple animations, <c>MixingTo</c> makes up a linked list.</summary>
        public TrackEntry MixingTo { get { return mixingTo; } }
 
        /// <summary>
        /// <para>
        /// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
        /// of being mixed out.</para>
        /// <para>
        /// When mixing between animations that key the same property, if a lower track also keys that property then the value will
        /// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
        /// while the second animation mixes from 0% to 100%. Setting <c>HoldPrevious</c> to true applies the first animation
        /// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
        /// keys the property, only when a higher track also keys the property.</para>
        /// <para>
        /// Snapping will occur if <c>HoldPrevious</c> is true and this animation does not key all the same properties as the
        /// previous animation.</para>
        /// </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>
        /// If true, mixing rotation between tracks always uses the shortest rotation direction. If the rotation is animated, the
        /// shortest rotation direction may change during the mix.
        /// </para><para>
        /// If false, the shortest rotation direction is remembered when the mix starts and the same direction is used for the rest
        /// of the mix. Defaults to false.</para></summary>
        public bool ShortestRotation { get { return shortestRotation; } set { shortestRotation = value; } }
 
        /// <summary>Returns true if this entry is for the empty animation. See <see cref="AnimationState.SetEmptyAnimation(int, float)"/>,
        /// <see cref="AnimationState.AddEmptyAnimation(int, float, float)"/>, and <see cref="AnimationState.SetEmptyAnimations(float)"/>.
        /// </summary>
        public bool IsEmptyAnimation { get { return animation == AnimationState.EmptyAnimation; } }
 
        /// <summary>
        /// <para>
        /// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
        /// long way around when using <see cref="alpha"/> and starting animations on other tracks.</para>
        /// <para>
        /// Mixing with <see cref="MixBlend.Replace"/> involves finding a rotation between two others, which has two possible solutions:
        /// the short way or the long way around. The two rotations likely change over time, so which direction is the short or long
        /// way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the
        /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction.</para>
        /// </summary>
        public void ResetRotationDirections () {
            timelinesRotation.Clear();
        }
 
        override public string ToString () {
            return animation == null ? "<none>" : animation.name;
        }
 
        // Note: This method is required by SpineAnimationStateMixerBehaviour,
        // which is part of the timeline extension package. Thus the internal member variable
        // nextTrackLast is not accessible. We favor providing this method
        // over exposing nextTrackLast as public property, which would rather confuse users.
        public void AllowImmediateQueue () {
            if (nextTrackLast < 0) nextTrackLast = 0;
        }
    }
 
    class EventQueue {
        private readonly List<EventQueueEntry> eventQueueEntries = new List<EventQueueEntry>();
        internal bool drainDisabled;
 
        private readonly AnimationState state;
        private readonly Pool<TrackEntry> trackEntryPool;
        internal event Action AnimationsChanged;
 
        internal EventQueue (AnimationState state, Action HandleAnimationsChanged, Pool<TrackEntry> trackEntryPool) {
            this.state = state;
            this.AnimationsChanged += HandleAnimationsChanged;
            this.trackEntryPool = trackEntryPool;
        }
 
        internal void Start (TrackEntry entry) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.Start, entry));
            if (AnimationsChanged != null) AnimationsChanged();
        }
 
        internal void Interrupt (TrackEntry entry) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.Interrupt, entry));
        }
 
        internal void End (TrackEntry entry) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.End, entry));
            if (AnimationsChanged != null) AnimationsChanged();
        }
 
        internal void Dispose (TrackEntry entry) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.Dispose, entry));
        }
 
        internal void Complete (TrackEntry entry) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.Complete, entry));
        }
 
        internal void Event (TrackEntry entry, Event e) {
            eventQueueEntries.Add(new EventQueueEntry(EventType.Event, entry, e));
        }
 
        /// <summary>Raises all events in the queue and drains the queue.</summary>
        internal void Drain () {
            if (drainDisabled) return;
            drainDisabled = true;
 
            List<EventQueueEntry> eventQueueEntries = this.eventQueueEntries;
            AnimationState state = this.state;
 
            // Don't cache eventQueueEntries.Count so callbacks can queue their own events (eg, call SetAnimation in AnimationState_Complete).
            for (int i = 0; i < eventQueueEntries.Count; i++) {
                EventQueueEntry queueEntry = eventQueueEntries[i];
                TrackEntry trackEntry = queueEntry.entry;
 
                switch (queueEntry.type) {
                case EventType.Start:
                    trackEntry.OnStart();
                    state.OnStart(trackEntry);
                    break;
                case EventType.Interrupt:
                    trackEntry.OnInterrupt();
                    state.OnInterrupt(trackEntry);
                    break;
                case EventType.End:
                    trackEntry.OnEnd();
                    state.OnEnd(trackEntry);
                    goto case EventType.Dispose; // Fall through. (C#)
                case EventType.Dispose:
                    trackEntry.OnDispose();
                    state.OnDispose(trackEntry);
                    trackEntryPool.Free(trackEntry);
                    break;
                case EventType.Complete:
                    trackEntry.OnComplete();
                    state.OnComplete(trackEntry);
                    break;
                case EventType.Event:
                    trackEntry.OnEvent(queueEntry.e);
                    state.OnEvent(trackEntry, queueEntry.e);
                    break;
                }
            }
            eventQueueEntries.Clear();
 
            drainDisabled = false;
        }
 
        internal void Clear () {
            eventQueueEntries.Clear();
        }
 
        struct EventQueueEntry {
            public EventType type;
            public TrackEntry entry;
            public Event e;
 
            public EventQueueEntry (EventType eventType, TrackEntry trackEntry, Event e = null) {
                this.type = eventType;
                this.entry = trackEntry;
                this.e = e;
            }
        }
 
        enum EventType {
            Start, Interrupt, End, Dispose, Complete, Event
        }
    }
 
    class Pool<T> where T : class, new() {
        public readonly int max;
        readonly Stack<T> freeObjects;
 
        public int Count { get { return freeObjects.Count; } }
        public int Peak { get; private set; }
 
        public Pool (int initialCapacity = 16, int max = int.MaxValue) {
            freeObjects = new Stack<T>(initialCapacity);
            this.max = max;
        }
 
        public T Obtain () {
            return freeObjects.Count == 0 ? new T() : freeObjects.Pop();
        }
 
        public void Free (T obj) {
            if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null");
            if (freeObjects.Count < max) {
                freeObjects.Push(obj);
                Peak = Math.Max(Peak, freeObjects.Count);
            }
            Reset(obj);
        }
 
        public void Clear () {
            freeObjects.Clear();
        }
 
        protected void Reset (T obj) {
            IPoolable poolable = obj as IPoolable;
            if (poolable != null) poolable.Reset();
        }
 
        public interface IPoolable {
            void Reset ();
        }
    }
 
    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) {
                T item = addSet[i];
                anyItemAdded |= set.Add(item);
            }
            return anyItemAdded;
        }
    }
}