/****************************************************************************** * Spine Runtimes License Agreement * Last updated February 20, 2024. Replaces all prior versions. * * Copyright (c) 2013-2024, 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: * https://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. *****************************************************************************/ package com.esotericsoftware.spine; import java.lang.reflect.Field; import java.util.concurrent.atomic.AtomicInteger; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.backends.lwjgl3.Lwjgl3FileHandle; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Null; import com.badlogic.gdx.utils.Pool; import com.esotericsoftware.spine.AnimationState.AnimationStateListener; import com.esotericsoftware.spine.AnimationState.TrackEntry; import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.ClippingAttachment; import com.esotericsoftware.spine.attachments.MeshAttachment; import com.esotericsoftware.spine.attachments.PathAttachment; import com.esotericsoftware.spine.attachments.PointAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; import com.esotericsoftware.spine.attachments.Sequence; /** Unit tests to ensure {@link AnimationState} is working as expected. */ public class AnimationStateTests { final SkeletonJson json = new SkeletonJson(new AttachmentLoader() { public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) { return null; } public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) { return null; } public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) { return null; } public ClippingAttachment newClippingAttachment (Skin skin, String name) { return null; } public PathAttachment newPathAttachment (Skin skin, String name) { return null; } public PointAttachment newPointAttachment (Skin skin, String name) { return null; } }); final AnimationStateListener stateListener = new AnimationStateListener() { public void start (TrackEntry entry) { add(actual("start", entry)); } public void interrupt (TrackEntry entry) { add(actual("interrupt", entry)); } public void end (TrackEntry entry) { add(actual("end", entry)); } public void dispose (TrackEntry entry) { add(actual("dispose", entry)); } public void complete (TrackEntry entry) { add(actual("complete", entry)); } public void event (TrackEntry entry, Event event) { add(actual("event " + event.getString(), entry)); } private void add (Result result) { while (expected.size > actual.size) { Result note = expected.get(actual.size); if (!note.note) break; actual.add(note); log(note.name); } String message = result.toString(); if (actual.size >= expected.size) { message += "FAIL: "; fail = true; } else if (!expected.get(actual.size).equals(result)) { message += "FAIL: " + expected.get(actual.size); fail = true; } else message += "PASS"; log(message); actual.add(result); } }; final SkeletonData skeletonData; final Array actual = new Array(); final Array expected = new Array(); AnimationStateData stateData; AnimationState state; int entryCount; float time = 0; boolean fail; int test; AnimationStateTests () { skeletonData = json.readSkeletonData(new Lwjgl3FileHandle("test/test.json", FileType.Internal)); TrackEntry entry; setup("0.1 time step", // 1 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "end", 1, 1.1f), // expect(0, "dispose", 1, 1.1f) // ); state.setAnimation(0, "events0", false).setTrackEnd(1); run(0.1f, 1000, null); setup("1/60 time step, dispose queued", // 2 expect(0, "start", 0, 0), // expect(0, "interrupt", 0, 0), // expect(0, "end", 0, 0), // expect(0, "dispose", 0, 0), // expect(1, "dispose", 0, 0), // expect(0, "dispose", 0, 0), // expect(1, "dispose", 0, 0), // note("First 2 set/addAnimation calls are done."), expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.483f, 0.483f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "end", 1, 1.017f), // expect(0, "dispose", 1, 1.017f) // ); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 0); state.addAnimation(0, "events0", false, 0); state.addAnimation(0, "events1", false, 0); state.setAnimation(0, "events0", false).setTrackEnd(1); run(1 / 60f, 1000, null); setup("30 time step", // 3 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 30, 30), // expect(0, "event 30", 30, 30), // expect(0, "complete", 30, 30), // expect(0, "end", 30, 60), // expect(0, "dispose", 30, 60) // ); state.setAnimation(0, "events0", false).setTrackEnd(1); run(30, 1000, null); setup("1 time step", // 4 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 1, 1), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "end", 1, 2), // expect(0, "dispose", 1, 2) // ); state.setAnimation(0, "events0", false).setTrackEnd(1); run(1, 1.01f, null); setup("interrupt", // 5 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "interrupt", 1.1f, 1.1f), // expect(1, "start", 0.1f, 1.1f), // expect(1, "event 0", 0.1f, 1.1f), // expect(0, "end", 1.1f, 1.2f), // expect(0, "dispose", 1.1f, 1.2f), // expect(1, "event 14", 0.5f, 1.5f), // expect(1, "event 30", 1, 2), // expect(1, "complete", 1, 2), // expect(1, "interrupt", 1.1f, 2.1f), // expect(0, "start", 0.1f, 2.1f), // expect(0, "event 0", 0.1f, 2.1f), // expect(1, "end", 1.1f, 2.2f), // expect(1, "dispose", 1.1f, 2.2f), // expect(0, "event 14", 0.5f, 2.5f), // expect(0, "event 30", 1, 3), // expect(0, "complete", 1, 3), // expect(0, "end", 1, 3.1f), // expect(0, "dispose", 1, 3.1f) // ); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 0); state.addAnimation(0, "events0", false, 0).setTrackEnd(1); run(0.1f, 4f, null); setup("interrupt with delay", // 6 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "interrupt", 0.6f, 0.6f), // expect(1, "start", 0.1f, 0.6f), // expect(1, "event 0", 0.1f, 0.6f), // expect(0, "end", 0.6f, 0.7f), // expect(0, "dispose", 0.6f, 0.7f), // expect(1, "event 14", 0.5f, 1.0f), // expect(1, "event 30", 1, 1.5f), // expect(1, "complete", 1, 1.5f), // expect(1, "end", 1, 1.6f), // expect(1, "dispose", 1, 1.6f) // ); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 0.5f).setTrackEnd(1); run(0.1f, 1000, null); setup("interrupt with delay and mix time", // 7 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "interrupt", 1, 1), // expect(1, "start", 0.1f, 1), // expect(0, "complete", 1, 1), // expect(1, "event 0", 0.1f, 1), // expect(1, "event 14", 0.5f, 1.4f), // expect(0, "end", 1.6f, 1.7f), // expect(0, "dispose", 1.6f, 1.7f), // expect(1, "event 30", 1, 1.9f), // expect(1, "complete", 1, 1.9f), // expect(1, "end", 1, 2), // expect(1, "dispose", 1, 2) // ); stateData.setMix("events0", "events1", 0.7f); state.setAnimation(0, "events0", true); state.addAnimation(0, "events1", false, 0.9f).setTrackEnd(1); run(0.1f, 1000, null); setup("animation 0 events do not fire during mix", // 8 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "interrupt", 0.5f, 0.5f), // expect(1, "start", 0.1f, 0.5f), // expect(1, "event 0", 0.1f, 0.5f), // expect(1, "event 14", 0.5f, 0.9f), // expect(0, "complete", 1, 1), // expect(0, "end", 1.1f, 1.2f), // expect(0, "dispose", 1.1f, 1.2f), // expect(1, "event 30", 1, 1.4f), // expect(1, "complete", 1, 1.4f), // expect(1, "end", 1, 1.5f), // expect(1, "dispose", 1, 1.5f) // ); stateData.setDefaultMix(0.7f); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1); run(0.1f, 1000, null); setup("event threshold, some animation 0 events fire during mix", // 9 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "interrupt", 0.5f, 0.5f), // expect(1, "start", 0.1f, 0.5f), // expect(0, "event 14", 0.5f, 0.5f), // expect(1, "event 0", 0.1f, 0.5f), // expect(1, "event 14", 0.5f, 0.9f), // expect(0, "complete", 1, 1), // expect(0, "end", 1.1f, 1.2f), // expect(0, "dispose", 1.1f, 1.2f), // expect(1, "event 30", 1, 1.4f), // expect(1, "complete", 1, 1.4f), // expect(1, "end", 1, 1.5f), // expect(1, "dispose", 1, 1.5f) // ); stateData.setMix("events0", "events1", 0.7f); state.setAnimation(0, "events0", false).setEventThreshold(0.5f); state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1); run(0.1f, 1000, null); setup("event threshold, all animation 0 events fire during mix", // 10 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "interrupt", 0.9f, 0.9f), // expect(1, "start", 0.1f, 0.9f), // expect(1, "event 0", 0.1f, 0.9f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "event 0", 1, 1), // expect(1, "event 14", 0.5f, 1.3f), // expect(0, "end", 1.5f, 1.6f), // expect(0, "dispose", 1.5f, 1.6f), // expect(1, "event 30", 1, 1.8f), // expect(1, "complete", 1, 1.8f), // expect(1, "end", 1, 1.9f), // expect(1, "dispose", 1, 1.9f) // ); state.setAnimation(0, "events0", true).setEventThreshold(1); entry = state.addAnimation(0, "events1", false, 0.8f); entry.setMixDuration(0.7f); entry.setTrackEnd(1); run(0.1f, 1000, null); setup("looping", // 11 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "event 0", 1, 1), // expect(0, "event 14", 1.5f, 1.5f), // expect(0, "event 30", 2, 2), // expect(0, "complete", 2, 2), // expect(0, "event 0", 2, 2), // expect(0, "event 14", 2.5f, 2.5f), // expect(0, "event 30", 3, 3), // expect(0, "complete", 3, 3), // expect(0, "event 0", 3, 3), // expect(0, "event 14", 3.5f, 3.5f), // expect(0, "event 30", 4, 4), // expect(0, "complete", 4, 4), // expect(0, "event 0", 4, 4), // expect(0, "end", 4.1f, 4.1f), // expect(0, "dispose", 4.1f, 4.1f) // ); state.setAnimation(0, "events0", true); run(0.1f, 4, null); setup("not looping, track end past animation 0 duration", // 12 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "interrupt", 2.1f, 2.1f), // expect(1, "start", 0.1f, 2.1f), // expect(1, "event 0", 0.1f, 2.1f), // expect(0, "end", 2.1f, 2.2f), // expect(0, "dispose", 2.1f, 2.2f), // expect(1, "event 14", 0.5f, 2.5f), // expect(1, "event 30", 1, 3), // expect(1, "complete", 1, 3), // expect(1, "end", 1, 3.1f), // expect(1, "dispose", 1, 3.1f) // ); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 2).setTrackEnd(1); run(0.1f, 4f, null); setup("interrupt animation after first loop complete", // 13 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "event 0", 1, 1), // expect(0, "event 14", 1.5f, 1.5f), // expect(0, "event 30", 2, 2), // expect(0, "complete", 2, 2), // expect(0, "event 0", 2, 2), // expect(0, "interrupt", 2.1f, 2.1f), // expect(1, "start", 0.1f, 2.1f), // expect(1, "event 0", 0.1f, 2.1f), // expect(0, "end", 2.1f, 2.2f), // expect(0, "dispose", 2.1f, 2.2f), // expect(1, "event 14", 0.5f, 2.5f), // expect(1, "event 30", 1, 3), // expect(1, "complete", 1, 3), // expect(1, "end", 1, 3.1f), // expect(1, "dispose", 1, 3.1f) // ); state.setAnimation(0, "events0", true); run(0.1f, 6, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events1", false, 0).setTrackEnd(1); } }); setup("add animation on empty track", // 14 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "end", 1, 1.1f), // expect(0, "dispose", 1, 1.1f) // ); state.addAnimation(0, "events0", false, 0).setTrackEnd(1); run(0.1f, 1.9f, null); setup("end time beyond non-looping animation duration", // 15 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "end", 9f, 9.1f), // expect(0, "dispose", 9f, 9.1f) // ); state.setAnimation(0, "events0", false).setTrackEnd(9); run(0.1f, 10, null); setup("looping with animation start", // 16 expect(0, "start", 0, 0), // expect(0, "event 30", 0.4f, 0.4f), // expect(0, "complete", 0.4f, 0.4f), // expect(0, "event 30", 0.8f, 0.8f), // expect(0, "complete", 0.8f, 0.8f), // expect(0, "event 30", 1.2f, 1.2f), // expect(0, "complete", 1.2f, 1.2f), // expect(0, "end", 1.4f, 1.4f), // expect(0, "dispose", 1.4f, 1.4f) // ); entry = state.setAnimation(0, "events0", true); entry.setAnimationLast(0.6f); entry.setAnimationStart(0.6f); run(0.1f, 1.4f, null); setup("looping with animation start and end", // 17 expect(0, "start", 0, 0), // expect(0, "event 14", 0.3f, 0.3f), // expect(0, "complete", 0.6f, 0.6f), // expect(0, "event 14", 0.9f, 0.9f), // expect(0, "complete", 1.2f, 1.2f), // expect(0, "event 14", 1.5f, 1.5f), // expect(0, "end", 1.8f, 1.8f), // expect(0, "dispose", 1.8f, 1.8f) // ); entry = state.setAnimation(0, "events0", true); entry.setAnimationStart(0.2f); entry.setAnimationLast(0.2f); entry.setAnimationEnd(0.8f); run(0.1f, 1.8f, null); setup("non-looping with animation start and end", // 18 expect(0, "start", 0, 0), // expect(0, "event 14", 0.3f, 0.3f), // expect(0, "complete", 0.6f, 0.6f), // expect(0, "end", 1, 1.1f), // expect(0, "dispose", 1, 1.1f) // ); entry = state.setAnimation(0, "events0", false); entry.setAnimationStart(0.2f); entry.setAnimationLast(0.2f); entry.setAnimationEnd(0.8f); entry.setTrackEnd(1); run(0.1f, 1.8f, null); setup("mix out looping with animation start and end", // 19 expect(0, "start", 0, 0), // expect(0, "event 14", 0.3f, 0.3f), // expect(0, "complete", 0.6f, 0.6f), // expect(0, "interrupt", 0.8f, 0.8f), // expect(1, "start", 0.1f, 0.8f), // expect(1, "event 0", 0.1f, 0.8f), // expect(0, "event 14", 0.9f, 0.9f), // expect(0, "complete", 1.2f, 1.2f), // expect(1, "event 14", 0.5f, 1.2f), // expect(0, "end", 1.4f, 1.5f), // expect(0, "dispose", 1.4f, 1.5f), // expect(1, "event 30", 1, 1.7f), // expect(1, "complete", 1, 1.7f), // expect(1, "end", 1, 1.8f), // expect(1, "dispose", 1, 1.8f) // ); entry = state.setAnimation(0, "events0", true); entry.setAnimationStart(0.2f); entry.setAnimationLast(0.2f); entry.setAnimationEnd(0.8f); entry.setEventThreshold(1); entry = state.addAnimation(0, "events1", false, 0.7f); entry.setMixDuration(0.7f); entry.setTrackEnd(1); run(0.1f, 20, null); setup("setAnimation with track entry mix", // 20 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "event 0", 1, 1), // expect(0, "interrupt", 1, 1), // expect(1, "start", 0, 1), // expect(1, "event 0", 0.1f, 1.1f), // expect(1, "event 14", 0.5f, 1.5f), // expect(0, "end", 1.7f, 1.8f), // expect(0, "dispose", 1.7f, 1.8f), // expect(1, "event 30", 1, 2), // expect(1, "complete", 1, 2), // expect(1, "end", 1, 2.1f), // expect(1, "dispose", 1, 2.1f) // ); state.setAnimation(0, "events0", true); run(0.1f, 1000, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 1f)) { TrackEntry entry = state.setAnimation(0, "events1", false); entry.setMixDuration(0.7f); entry.setTrackEnd(1); } } }); setup("setAnimation twice", // 21 expect(0, "start", 0, 0), // expect(0, "interrupt", 0, 0), // expect(0, "end", 0, 0), // expect(0, "dispose", 0, 0), // expect(1, "start", 0, 0), // expect(1, "event 0", 0, 0), // expect(1, "event 14", 0.5f, 0.5f), // note("First 2 setAnimation calls are done."), expect(1, "interrupt", 0.8f, 0.8f), // expect(0, "start", 0, 0.8f), // expect(0, "interrupt", 0, 0.8f), // expect(0, "end", 0, 0.8f), // expect(0, "dispose", 0, 0.8f), // expect(2, "start", 0, 0.8f), // expect(2, "event 0", 0.1f, 0.9f), // expect(1, "end", 0.9f, 1), // expect(1, "dispose", 0.9f, 1), // expect(2, "event 14", 0.5f, 1.3f), // expect(2, "event 30", 1, 1.8f), // expect(2, "complete", 1, 1.8f), // expect(2, "end", 1, 1.9f), // expect(2, "dispose", 1, 1.9f) // ); state.setAnimation(0, "events0", false); // First should be ignored. state.setAnimation(0, "events1", false); run(0.1f, 1000, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 0.8f)) { state.setAnimation(0, "events0", false); // First should be ignored. state.setAnimation(0, "events2", false).setTrackEnd(1); } } }); setup("setAnimation twice with multiple mixing", // 22 expect(0, "start", 0, 0), // expect(0, "interrupt", 0, 0), // expect(0, "end", 0, 0), // expect(0, "dispose", 0, 0), // expect(1, "start", 0, 0), // expect(1, "event 0", 0, 0), // note("First 2 setAnimation calls are done."), expect(1, "interrupt", 0.2f, 0.2f), // expect(0, "start", 0, 0.2f), // expect(0, "interrupt", 0, 0.2f), // expect(0, "end", 0, 0.2f), // expect(0, "dispose", 0, 0.2f), // expect(2, "start", 0, 0.2f), // expect(2, "event 0", 0.1f, 0.3f), // note("Second 2 setAnimation calls are done."), expect(2, "interrupt", 0.2f, 0.4f), // expect(1, "start", 0, 0.4f), // expect(1, "interrupt", 0, 0.4f), // expect(1, "end", 0, 0.4f), // expect(1, "dispose", 0, 0.4f), // expect(0, "start", 0, 0.4f), // expect(0, "event 0", 0.1f, 0.5f), // expect(1, "end", 0.8f, 0.9f), // expect(1, "dispose", 0.8f, 0.9f), // expect(0, "event 14", 0.5f, 0.9f), // expect(2, "end", 0.8f, 1.1f), // expect(2, "dispose", 0.8f, 1.1f), // expect(0, "event 30", 1, 1.4f), // expect(0, "complete", 1, 1.4f), // expect(0, "end", 1, 1.5f), // expect(0, "dispose", 1, 1.5f) // ); stateData.setDefaultMix(0.6f); state.setAnimation(0, "events0", false); // First should be ignored. state.setAnimation(0, "events1", false); run(0.1f, 1000, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 0.2f)) { state.setAnimation(0, "events0", false); // First should be ignored. state.setAnimation(0, "events2", false); } if (MathUtils.isEqual(time, 0.4f)) { state.setAnimation(0, "events1", false); // First should be ignored. state.setAnimation(0, "events0", false).setTrackEnd(1); } } }); setup("addAnimation with delay on empty track", // 23 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 5), // expect(0, "event 14", 0.5f, 5.5f), // expect(0, "event 30", 1, 6), // expect(0, "complete", 1, 6), // expect(0, "end", 1, 6.1f), // expect(0, "dispose", 1, 6.1f) // ); state.addAnimation(0, "events0", false, 5).setTrackEnd(1); run(0.1f, 10, null); setup("setAnimation during AnimationStateListener"); // 24 state.addListener(new AnimationStateListener() { public void start (TrackEntry entry) { if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false); } public void interrupt (TrackEntry entry) { state.addAnimation(3, "events1", false, 0); } public void end (TrackEntry entry) { if (entry.getAnimation().getName().equals("events0")) state.setAnimation(0, "events1", false); } public void dispose (TrackEntry entry) { if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false); } public void complete (TrackEntry entry) { if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false); } public void event (TrackEntry entry, Event event) { if (entry.getTrackIndex() != 2) state.setAnimation(2, "events1", false); } }); state.addAnimation(0, "events0", false, 0); state.addAnimation(0, "events1", false, 0); state.setAnimation(1, "events1", false).setTrackEnd(1); run(0.1f, 10, null); setup("clearTrack", // 25 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "end", 0.7f, 0.7f), // expect(0, "dispose", 0.7f, 0.7f) // ); state.addAnimation(0, "events0", false, 0).setTrackEnd(1); run(0.1f, 10, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0); } }); setup("setEmptyAnimation", // 26 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "interrupt", 0.7f, 0.7f), // expect(-1, "start", 0, 0.7f), // expect(-1, "complete", 0.1f, 0.8f), // expect(0, "end", 0.8f, 0.9f), // expect(0, "dispose", 0.8f, 0.9f), // expect(-1, "end", 0.2f, 1), // expect(-1, "dispose", 0.2f, 1) // ); state.addAnimation(0, "events0", false, 0).setTrackEnd(1); run(0.1f, 10, new TestListener() { public void frame (float time) { if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0); } }); setup("TrackEntry listener"); // 27 final AtomicInteger counter = new AtomicInteger(); state.addAnimation(0, "events0", false, 0).setListener(new AnimationStateListener() { public void start (TrackEntry entry) { counter.addAndGet(1 << 1); } public void interrupt (TrackEntry entry) { counter.addAndGet(1 << 5); } public void end (TrackEntry entry) { counter.addAndGet(1 << 9); } public void dispose (TrackEntry entry) { counter.addAndGet(1 << 13); } public void complete (TrackEntry entry) { counter.addAndGet(1 << 17); } public void event (TrackEntry entry, Event event) { counter.addAndGet(1 << 21); } }); state.addAnimation(0, "events0", false, 0); state.addAnimation(0, "events1", false, 0); state.setAnimation(1, "events1", false).setTrackEnd(1); run(0.1f, 10, null); if (counter.get() != 15082016) { log("TEST 28 FAILED! " + counter); System.exit(0); } setup("looping", // 28 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "event 0", 1, 1), // expect(0, "event 14", 1.5f, 1.5f), // expect(0, "event 30", 2, 2), // expect(0, "complete", 2, 2), // expect(0, "event 0", 2, 2), // expect(0, "event 14", 2.5f, 2.5f), // expect(0, "end", 2.6f, 2.7f), // expect(0, "dispose", 2.6f, 2.7f) // ); state.setAnimation(0, "events0", true).setTrackEnd(2.6f); run(0.1f, 1000, null); setup("set next", // 29 expect(0, "start", 0, 0), // expect(0, "event 0", 0, 0), // expect(0, "event 14", 0.5f, 0.5f), // expect(0, "event 30", 1, 1), // expect(0, "complete", 1, 1), // expect(0, "interrupt", 1.1f, 1.1f), // expect(1, "start", 0.1f, 1.1f), // expect(1, "event 0", 0.1f, 1.1f), // expect(0, "end", 1.1f, 1.2f), // expect(0, "dispose", 1.1f, 1.2f), // expect(1, "event 14", 0.5f, 1.5f), // expect(1, "event 30", 1, 2), // expect(1, "complete", 1, 2), // expect(1, "end", 1, 2.1f), // expect(1, "dispose", 1, 2.1f) // ); state.setAnimation(0, "events0", false); state.addAnimation(0, "events1", false, 0).setTrackEnd(1); run(0.1f, 1000, null); System.out.println("AnimationState tests passed."); } void setup (String description, Result... expectedArray) { test++; expected.addAll(expectedArray); stateData = new AnimationStateData(skeletonData); state = new AnimationState(stateData); Pool trackEntryPool = new Pool() { public TrackEntry obtain () { TrackEntry entry = super.obtain(); entryCount++; // System.out.println("+1: " + entryCount + " " + entry.hashCode()); return entry; } protected TrackEntry newObject () { return new TrackEntry(); } public void free (TrackEntry entry) { entryCount--; // System.out.println("-1: " + entryCount + " " + entry.hashCode()); super.free(entry); } }; try { Field field = state.getClass().getDeclaredField("trackEntryPool"); field.setAccessible(true); field.set(state, trackEntryPool); } catch (Exception ex) { throw new RuntimeException(ex); } time = 0; fail = false; log(test + ": " + description); if (expectedArray.length > 0) { state.addListener(stateListener); log(String.format("%-3s%-12s%-7s%-7s%-7s", "#", "EVENT", "TRACK", "TOTAL", "RESULT")); } } void run (float incr, float endTime, TestListener listener) { Skeleton skeleton = new Skeleton(skeletonData); state.apply(skeleton); while (time < endTime) { time += incr; state.update(incr); // Reduce float discrepancies for tests. for (TrackEntry entry : state.getTracks()) { if (entry == null) continue; entry.trackTime = round(entry.trackTime, 6); entry.delay = round(entry.delay, 3); if (entry.mixingFrom != null) entry.mixingFrom.trackTime = round(entry.mixingFrom.trackTime, 6); } state.apply(skeleton); // Apply multiple times to ensure no side effects. if (expected.size > 0) state.removeListener(stateListener); state.apply(skeleton); state.apply(skeleton); if (expected.size > 0) state.addListener(stateListener); if (listener != null) listener.frame(time); } state.clearTracks(); // Expecting more than actual is a failure. for (int i = actual.size, n = expected.size; i < n; i++) { log(String.format("%-29s", "") + "FAIL: " + expected.get(i)); fail = true; } // Check all allocated entries were freed. if (!fail) { if (entryCount != 0) { log("FAIL: Pool balance: " + entryCount); fail = true; } } actual.clear(); expected.clear(); log(""); if (fail) { log("TEST " + test + " FAILED!"); System.exit(0); } } Result expect (int animationIndex, String name, float trackTime, float totalTime) { Result result = new Result(); result.name = name; result.animationIndex = animationIndex; result.trackTime = trackTime; result.totalTime = totalTime; return result; } Result actual (String name, TrackEntry entry) { Result result = new Result(); result.name = name; result.animationIndex = skeletonData.getAnimations().indexOf(entry.animation, true); result.trackTime = Math.round(entry.trackTime * 1000) / 1000f; result.totalTime = Math.round(time * 1000) / 1000f; return result; } Result note (String message) { Result result = new Result(); result.name = message; result.note = true; return result; } void log (String message) { System.out.println(message); } class Result { String name; int animationIndex; float trackTime, totalTime; boolean note; public int hashCode () { int result = 31 + animationIndex; result = 31 * result + name.hashCode(); result = 31 * result + Float.floatToIntBits(totalTime); result = 31 * result + Float.floatToIntBits(trackTime); return result; } public boolean equals (Object obj) { Result other = (Result)obj; if (animationIndex != other.animationIndex) return false; if (!name.equals(other.name)) return false; if (!MathUtils.isEqual(totalTime, other.totalTime)) return false; if (!MathUtils.isEqual(trackTime, other.trackTime)) return false; return true; } public String toString () { return String.format("%-3s%-12s%-7s%-7s", "" + animationIndex, name, roundTime(trackTime), roundTime(totalTime)); } } static float round (float value, int decimals) { float shift = (float)Math.pow(10, decimals); return Math.round(value * shift) / shift; } static String roundTime (float value) { String text = Float.toString(round(value, 3)); return text.endsWith(".0") ? text.substring(0, text.length() - 2) : text; } static interface TestListener { void frame (float time); } static public void main (String[] args) throws Exception { new AnimationStateTests(); } }