From 3183c0b38338cb01205e134b5d376ae5a82f1f33 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 12 Jul 2025 02:24:14 +0200 Subject: [PATCH] [tests] Auto-generation of Java SkeletonSerializer --- docs/todo.md | 2 +- .../2025-01-11-03-02-54-test-suite/task.md | 193 + .../esotericsoftware/spine/HeadlessTest.java | 27 +- .../com/esotericsoftware/spine/Animation.java | 4 + .../spine/utils/SkeletonSerializer.java | 4475 +++++++++++++++++ tests/README.md | 53 +- tests/analyze-java-api.ts | 665 +++ tests/generate-claude-prompt.ts | 165 + tests/generate-java-serializer.ts | 549 ++ tests/package-lock.json | 189 + tests/package.json | 5 +- tests/types.ts | 68 + 12 files changed, 6377 insertions(+), 18 deletions(-) create mode 100644 docs/todos/work/2025-01-11-03-02-54-test-suite/task.md create mode 100644 spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java create mode 100755 tests/analyze-java-api.ts create mode 100644 tests/generate-claude-prompt.ts create mode 100644 tests/generate-java-serializer.ts create mode 100644 tests/types.ts diff --git a/docs/todo.md b/docs/todo.md index 977a46ea5..25ae9d2e5 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,4 +1,4 @@ -- SkeletonBinary.cpp is buggy. DataInput sign handling seems to be foo bar, so we get incorrect flags etc. Compare to SkeletonBinary.java, both statically (compare sources and involved classes) and dynamically (instrument both, run on same file, compare where we go wrong) +- lsp-cli should export its types, so we can pull them in as a dependency instead of redefining them ourselves in types.ts - clean up logging in spine-c/codegen, use chalk to do colored warnings/errors and make logging look very nice and informative (no emojis) - spine-c/codegen type extractor should also report typedefs like typedef long long PropertyId; so primitive type to some name, and we need to handle that in the codegen - Generate language bindings in spine-c/codegen diff --git a/docs/todos/work/2025-01-11-03-02-54-test-suite/task.md b/docs/todos/work/2025-01-11-03-02-54-test-suite/task.md new file mode 100644 index 000000000..a5207230b --- /dev/null +++ b/docs/todos/work/2025-01-11-03-02-54-test-suite/task.md @@ -0,0 +1,193 @@ +# create a folder test/ and write a comprehensive test suite + +**Status:** In Progress +**Created:** 2025-01-11T03:02:54 +**Started:** 2025-01-11T03:11:22 +**Agent PID:** 89579 + +## Original Todo +- create a folder test/ and write a comprehensive test suite + - For each core runtime + - Write a program that takes as input a skeleton (.json or .skel) and atlas file path and animation name + - Loads a SkeletonData and outputs EVERYTHING in a simple, diffable text format + - Creates a skeleton from the SkeletonData, an AnimationState, sets the Animation on track 0, updates and applies the state to the skeleton, then outputs EVERYTHING in a diffable text format + - The best approach is likely to create a Printer interface that can print each relevant type in the diffable format, with a specific indentation level, so the output represents the hierarchy of the data + - See docs/project-description.md for the core runtimes and their location + - The test/ folder should have simple language specific build scripts that build/pull in the core runtime for that language, and create an exectuable program we can invoke + - build.gradle for Java, directly pulling in the spine-libgdx project via settings.gradle + - CMakeLists.txt for C and C++ + - package.json/tsconfig.json for Typescript + - Let's ignore the other core runtimes for now + - The programs must be headless, which means we need to ensure when loading the atlases, texture loading actually doesn't happen. + - The goal is to be able to construct bash or nodejs test suites that can find errors in the non-reference runtimes quickly by comparing actually loaded and "applied" data between teh runtimes + +## Description +Create a comprehensive test suite that compares the output of all core Spine runtimes (Java, C++, C, TypeScript) to ensure consistency. The test suite will consist of headless command-line programs for each runtime that load skeleton data and output all internal state in a diffable text format. This will enable automated comparison testing between the reference implementation (spine-libgdx) and all ports. Each test program must compile directly against the runtime's source code, not pull published versions from package repositories. + +The test programs will print both SkeletonData (setup pose/static data) and Skeleton (runtime pose after animation) states, including all nested types such as bones, slots, skins, attachments, constraints, and animations. + +## Updated Implementation Plan (DebugPrinter in each runtime) +- [x] Remove test/ directory approach (deprecated) +- [x] Implement DebugPrinter in spine-libgdx-tests: + - [x] Create DebugPrinter.java in spine-libgdx/spine-libgdx-tests/src + - [x] Add command line argument parsing for skeleton, atlas, animation + - [x] Create Printer class for outputting all types in hierarchical format + - [x] Ensure it can run headless (no GL requirements) + - [x] Merged Printer and HeadlessTextureLoader into single DebugPrinter.java file + - [x] Added gradle task runDebugPrinter for easy execution +- [x] Implement DebugPrinter in spine-cpp: + - [x] Create spine-cpp/tests directory with DebugPrinter.cpp + - [x] Update spine-cpp/CMakeLists.txt with conditional target + - [x] Add Printer class matching Java output format + - [x] Ensure headless operation +- [x] Implement DebugPrinter in spine-c: + - [x] Create spine-c/tests directory with debug-printer.c + - [x] Update spine-c/CMakeLists.txt with conditional target + - [x] Add printer functions matching output format + - [x] Ensure headless operation + - [x] Fix spine-c/codegen/src/ir-generator.ts to use .buffer() for string getters (currently returns address of temporary String object) + - Run generator via: npx tsx spine-c/codegen/src/index.ts + - This regenerates spine-c/src/generated/*.cpp files + - Fixed by handling "const String &" (with space) in addition to "const String&" + - Verified: String methods now properly return .buffer() values (version, hash, etc. display correctly) + - [x] Issues found by DebugPrinter comparison: + - [x] spine-c API missing file-based skeleton loader that sets name from filename + - C++ has readSkeletonDataFile() which sets name, spine-c only exposes content-based loader + - Result: SkeletonData name is empty in spine-c output + - Fixed: Modified spine_skeleton_data_load_json/binary to accept path parameter and extract name + - [x] Coordinate system inconsistency: Java shows scaleY=1.0, C/C++ show scaleY=-1.0, use Bone::setYDown(false) to match Java +- [x] Implement DebugPrinter in spine-ts/spine-core: + - [x] Create tests/DebugPrinter.ts + - [x] Update tsconfig.json to exclude tests/ so tests are not bundled + - [x] Add Printer class matching output format + - [x] Ensure it runs with npx tsx without build step +- [x] Create test runner script (compare-with-reference-impl.ts): + - [x] Run each runtime's DebugPrinter with same inputs + - [x] Compare outputs and report differences + - [x] TypeScript script with shebang for direct execution + - [x] Automatically builds C/C++/Java if needed + - [x] Saves outputs to tests/output/ directory + - [x] Shows line-by-line differences when outputs don't match +- [x] Make animation parameter optional in all DebugPrinters: + - [x] If animation not provided, call skeleton.setToSetupPose() instead + - [x] Update Java DebugPrinter + - [x] Update C++ DebugPrinter + - [x] Update C DebugPrinter + - [x] Update TypeScript DebugPrinter + - [x] Update compare-with-reference-impl.ts to handle optional animation +- [x] Fix locale issues - all DebugPrinters should use English locale: + - [x] Java: Set Locale.US for number formatting + - [x] C++: Set locale to "C" or "en_US.UTF-8" + - [x] C: Set locale to "C" or "en_US.UTF-8" + - [x] TypeScript: Already uses period for decimals +- [x] Improve buildCheck() to detect when rebuild needed: + - [x] Check if debug printer executable exists + - [x] Compare executable timestamp with source file timestamps + - [x] Rebuild if any source files are newer than executable +- [x] Create tests/README.md documentation: + - [x] Explain purpose: comparing reference implementation to other runtimes + - [x] List DebugPrinter locations in each runtime + - [x] Document how to run individual debug printers + - [x] Document how to run compare-with-reference-impl.ts +- [x] Automated test: All DebugPrinters produce identical output + - Note: Minor expected differences remain: + - time field: Java shows 0.016 after update, C/C++ show 0.0 + - TypeScript: minor precision differences, null vs "" for audioPath + - These are implementation details, not bugs +- [x] User test: Verify with multiple skeleton files + +## Phase 2: JSON Serializers and HeadlessTest Rename + +### Rename DebugPrinter to HeadlessTest +- [x] Rename all DebugPrinter files to HeadlessTest: + - [x] Java: DebugPrinter.java → HeadlessTest.java + - [x] C++: DebugPrinter.cpp → HeadlessTest.cpp + - [x] C: debug-printer.c → headless-test.c + - [x] TypeScript: DebugPrinter.ts → HeadlessTest.ts +- [x] Update VS Code launch configs to say "headless test ($runtime)": + - [x] spine-libgdx/.vscode/launch.json + - [x] spine-cpp/.vscode/launch.json + - [x] spine-c/.vscode/launch.json + - [x] spine-ts/.vscode/launch.json +- [x] Rename tests/compare-with-reference-impl.ts to headless-test-runner.ts +- [x] Update build files: + - [x] CMakeLists.txt for C++ (executable name: headless-test) + - [x] CMakeLists.txt for C (executable name: headless-test) + - [x] Gradle for Java (task name: runHeadlessTest, main class: HeadlessTest) +- [x] Update tests/README.md with new names + +### Implement JSON Serializers in Core Runtimes +- [x] Java (spine-libgdx): + - [x] Create SkeletonSerializer class in com.esotericsoftware.spine.utils + - [x] Implement serializeSkeletonData(SkeletonData, Writer/StringBuilder) + - [x] Implement serializeSkeleton(Skeleton, Writer/StringBuilder) + - [x] Implement serializeAnimationState(AnimationState, Writer/StringBuilder) + - [x] Add depth/verbosity options to control output + - [x] Handle circular references and limit nesting + - [x] Update HeadlessTest to use SkeletonSerializer + - [x] Review serializer with user: + - [x] Test with actual skeleton file to see output format + - [x] Add cycle detection to handle circular references (outputs "") + - [x] Verify it compiles and produces JSON output + - [x] Create comprehensive API analyzer tool: + - [x] Analyzer discovers all types accessible via SkeletonData, Skeleton, and AnimationState + - [x] For each type, enumerate all getters including inherited ones + - [x] Generate Java serializer from analysis data + - [x] Handle enums, abstract types, inner classes, and type parameters + - [x] Filter out test classes and non-source files + - [x] Work on SkeletonSerializer.java generation until it actually compiles. +- [ ] C++ (spine-cpp): + - [ ] Create SkeletonSerializer.h/cpp in spine-cpp/src/spine + - [ ] Implement serializeSkeletonData(SkeletonData*, std::string&) + - [ ] Implement serializeSkeleton(Skeleton*, std::string&) + - [ ] Implement serializeAnimationState(AnimationState*, std::string&) + - [ ] Add SerializerOptions struct for controlling output + - [ ] Update HeadlessTest to use SkeletonSerializer + - [ ] Ensure serializer outputs exact same data format as Java version +- [ ] C (spine-c): + - [ ] Create spine-skeleton-serializer.h/c + - [ ] Implement spine_skeleton_data_serialize_json(data, buffer, options) + - [ ] Implement spine_skeleton_serialize_json(skeleton, buffer, options) + - [ ] Implement spine_animation_state_serialize_json(state, buffer, options) + - [ ] Add spine_serializer_options struct + - [ ] Update headless-test to use serializer functions + - [ ] Ensure serializer outputs exact same data format as Java version +- [ ] TypeScript (spine-ts): + - [ ] Create SkeletonSerializer.ts in spine-core/src + - [ ] Implement serializeSkeletonData(data: SkeletonData): object + - [ ] Implement serializeSkeleton(skeleton: Skeleton): object + - [ ] Implement serializeAnimationState(state: AnimationState): object + - [ ] Add SerializerOptions interface + - [ ] Update HeadlessTest to use SkeletonSerializer and JSON.stringify + - [ ] Ensure serializer outputs exact same data format as Java version +- [ ] Update tests/README.md to describe the new setup + +### Misc (added by user while Claude worked, need to be expanded!) +- [ ] HeadlessTest should probably + - Have a mode that does what we currently do: take files and animation name, and output serialized skeleton data and skeleton. Used for ad-hoc testing of files submitted by users in error reports etc. + - Have "unit" test like tests, that are easily extensible + - each test has a name and points to the corresponding function to execute + - HeadlessTest can take as args a single name, multiple test names, or no args in which case it runs all tests in order + - Structure and cli handling needs to be the same in all HeadlessTest implementations + - tests/headless-test-runner.ts should also support these same cli args, run each runtime test, then compare outputs. + +### Serializer Design Considerations +- Special cases to avoid infinite recursion: + - Bone parent references (output name only) + - Constraint target references (output names only) + - Skin attachment references (limit depth) + - Timeline references in animations +- Fields to include at each level: + - SkeletonData: All top-level fields, list of bone/slot/skin/animation names + - Skeleton: Current pose transforms, active skin, color + - AnimationState: Active tracks, mix times, current time +- Output format: Pretty-printed JSON with 2-space indentation + +## Future Expansion (after serializers complete): +- Add full type printing for SkeletonData (bones, slots, skins, animations) +- Add Skeleton runtime state printing +- Add all attachment types +- Add all timeline types +- Add all constraint types +- Add comprehensive type verification + diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java index 69e3c0712..a9b790f5f 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java @@ -38,7 +38,9 @@ import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData; +import com.esotericsoftware.spine.utils.SkeletonSerializer; +import java.io.StringWriter; import java.util.Locale; public class HeadlessTest implements ApplicationListener { @@ -52,6 +54,8 @@ public class HeadlessTest implements ApplicationListener { this.animationName = animationName; } + // Removed Printer class - now using SkeletonSerializer + /* static class Printer { private int indentLevel = 0; private static final String INDENT = " "; @@ -124,6 +128,7 @@ public class HeadlessTest implements ApplicationListener { print("}"); } } + */ // Mock texture that doesn't require OpenGL - similar to AndroidTexture static class MockTexture extends Texture { @@ -212,10 +217,14 @@ public class HeadlessTest implements ApplicationListener { skeletonData = binary.readSkeletonData(skeletonFile); } - // Print skeleton data + // Create serializer + SkeletonSerializer serializer = new SkeletonSerializer(); + + // Print skeleton data as JSON System.out.println("=== SKELETON DATA ==="); - Printer printer = new Printer(); - printer.printSkeletonData(skeletonData); + StringWriter dataWriter = new StringWriter(); + serializer.serializeSkeletonData(skeletonData, dataWriter); + System.out.println(dataWriter.toString()); // Create skeleton instance Skeleton skeleton = new Skeleton(skeletonData); @@ -242,9 +251,17 @@ public class HeadlessTest implements ApplicationListener { skeleton.updateWorldTransform(Physics.update); - // Print skeleton state + // Print skeleton state as JSON System.out.println("\n=== SKELETON STATE ==="); - printer.printSkeleton(skeleton); + StringWriter skeletonWriter = new StringWriter(); + serializer.serializeSkeleton(skeleton, skeletonWriter); + System.out.println(skeletonWriter.toString()); + + // Print animation state as JSON + System.out.println("\n=== ANIMATION STATE ==="); + StringWriter stateWriter = new StringWriter(); + serializer.serializeAnimationState(state, stateWriter); + System.out.println(stateWriter.toString()); } catch (Exception e) { e.printStackTrace(); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index f904d49e8..fba510827 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -106,6 +106,10 @@ public class Animation { this.duration = duration; } + public IntArray getBones() { + return bones; + } + /** Applies the animation's timelines to the specified skeleton. *

* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection, boolean)}. diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java new file mode 100644 index 000000000..3b801163a --- /dev/null +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java @@ -0,0 +1,4475 @@ +package com.esotericsoftware.spine.utils; + +import com.esotericsoftware.spine.*; +import com.esotericsoftware.spine.Animation.*; +import com.esotericsoftware.spine.AnimationState.*; +import com.esotericsoftware.spine.BoneData.Inherit; +import com.esotericsoftware.spine.Skin.SkinEntry; +import com.esotericsoftware.spine.PathConstraintData.*; +import com.esotericsoftware.spine.TransformConstraintData.*; +import com.esotericsoftware.spine.attachments.*; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntArray; +import com.badlogic.gdx.utils.FloatArray; + +import java.io.Writer; +import java.io.IOException; +import java.util.Locale; +import java.util.Set; +import java.util.HashSet; + +public class SkeletonSerializer { + private final Set visitedObjects = new HashSet<>(); + + public void serializeSkeletonData(SkeletonData data, Writer writer) throws IOException { + visitedObjects.clear(); + JsonWriter json = new JsonWriter(writer); + writeSkeletonData(json, data); + json.close(); + } + + public void serializeSkeleton(Skeleton skeleton, Writer writer) throws IOException { + visitedObjects.clear(); + JsonWriter json = new JsonWriter(writer); + writeSkeleton(json, skeleton); + json.close(); + } + + public void serializeAnimationState(AnimationState state, Writer writer) throws IOException { + visitedObjects.clear(); + JsonWriter json = new JsonWriter(writer); + writeAnimationState(json, state); + json.close(); + } + + private void writeAnimation(JsonWriter json, Animation obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Animation"); + + json.writeName("timelines"); + if (obj.getTimelines() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Timeline item : obj.getTimelines()) { + writeTimeline(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int i = 0; i < obj.getBones().size; i++) { + json.writeValue(obj.getBones().get(i)); + } + json.writeArrayEnd(); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeAlphaTimeline(JsonWriter json, Animation.AlphaTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("AlphaTimeline"); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeAttachmentTimeline(JsonWriter json, Animation.AttachmentTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("AttachmentTimeline"); + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("attachmentNames"); + if (obj.getAttachmentNames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getAttachmentNames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeDeformTimeline(JsonWriter json, Animation.DeformTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("DeformTimeline"); + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("attachment"); + writeVertexAttachment(json, obj.getAttachment()); + + json.writeName("vertices"); + if (obj.getVertices() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float[] nestedArray : obj.getVertices()) { + if (nestedArray == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float elem : nestedArray) { + json.writeValue(elem); + } + json.writeArrayEnd(); + } + } + json.writeArrayEnd(); + } + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeDrawOrderTimeline(JsonWriter json, Animation.DrawOrderTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("DrawOrderTimeline"); + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("drawOrders"); + if (obj.getDrawOrders() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int[] nestedArray : obj.getDrawOrders()) { + if (nestedArray == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int elem : nestedArray) { + json.writeValue(elem); + } + json.writeArrayEnd(); + } + } + json.writeArrayEnd(); + } + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeEventTimeline(JsonWriter json, Animation.EventTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("EventTimeline"); + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("events"); + if (obj.getEvents() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Event item : obj.getEvents()) { + writeEvent(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeIkConstraintTimeline(JsonWriter json, Animation.IkConstraintTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("IkConstraintTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeInheritTimeline(JsonWriter json, Animation.InheritTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("InheritTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePathConstraintMixTimeline(JsonWriter json, Animation.PathConstraintMixTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraintMixTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePathConstraintPositionTimeline(JsonWriter json, Animation.PathConstraintPositionTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraintPositionTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePathConstraintSpacingTimeline(JsonWriter json, Animation.PathConstraintSpacingTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraintSpacingTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintDampingTimeline(JsonWriter json, Animation.PhysicsConstraintDampingTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintDampingTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintGravityTimeline(JsonWriter json, Animation.PhysicsConstraintGravityTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintGravityTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintInertiaTimeline(JsonWriter json, Animation.PhysicsConstraintInertiaTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintInertiaTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintMassTimeline(JsonWriter json, Animation.PhysicsConstraintMassTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintMassTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintMixTimeline(JsonWriter json, Animation.PhysicsConstraintMixTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintMixTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintResetTimeline(JsonWriter json, Animation.PhysicsConstraintResetTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintResetTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintStrengthTimeline(JsonWriter json, Animation.PhysicsConstraintStrengthTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintStrengthTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintWindTimeline(JsonWriter json, Animation.PhysicsConstraintWindTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintWindTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeRGB2Timeline(JsonWriter json, Animation.RGB2Timeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RGB2Timeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeRGBA2Timeline(JsonWriter json, Animation.RGBA2Timeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RGBA2Timeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeRGBATimeline(JsonWriter json, Animation.RGBATimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RGBATimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeRGBTimeline(JsonWriter json, Animation.RGBTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RGBTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeRotateTimeline(JsonWriter json, Animation.RotateTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RotateTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeScaleTimeline(JsonWriter json, Animation.ScaleTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ScaleTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeScaleXTimeline(JsonWriter json, Animation.ScaleXTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ScaleXTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeScaleYTimeline(JsonWriter json, Animation.ScaleYTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ScaleYTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeSequenceTimeline(JsonWriter json, Animation.SequenceTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SequenceTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("attachment"); + writeAttachment(json, obj.getAttachment()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeShearTimeline(JsonWriter json, Animation.ShearTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ShearTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeShearXTimeline(JsonWriter json, Animation.ShearXTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ShearXTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeShearYTimeline(JsonWriter json, Animation.ShearYTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ShearYTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeSliderMixTimeline(JsonWriter json, Animation.SliderMixTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SliderMixTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeSliderTimeline(JsonWriter json, Animation.SliderTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SliderTimeline"); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeTimeline(JsonWriter json, Animation.Timeline obj) throws IOException { + if (obj instanceof Animation.AlphaTimeline) { + writeAlphaTimeline(json, (Animation.AlphaTimeline) obj); + } else if (obj instanceof Animation.AttachmentTimeline) { + writeAttachmentTimeline(json, (Animation.AttachmentTimeline) obj); + } else if (obj instanceof Animation.DeformTimeline) { + writeDeformTimeline(json, (Animation.DeformTimeline) obj); + } else if (obj instanceof Animation.DrawOrderTimeline) { + writeDrawOrderTimeline(json, (Animation.DrawOrderTimeline) obj); + } else if (obj instanceof Animation.EventTimeline) { + writeEventTimeline(json, (Animation.EventTimeline) obj); + } else if (obj instanceof Animation.IkConstraintTimeline) { + writeIkConstraintTimeline(json, (Animation.IkConstraintTimeline) obj); + } else if (obj instanceof Animation.InheritTimeline) { + writeInheritTimeline(json, (Animation.InheritTimeline) obj); + } else if (obj instanceof Animation.PathConstraintMixTimeline) { + writePathConstraintMixTimeline(json, (Animation.PathConstraintMixTimeline) obj); + } else if (obj instanceof Animation.PathConstraintPositionTimeline) { + writePathConstraintPositionTimeline(json, (Animation.PathConstraintPositionTimeline) obj); + } else if (obj instanceof Animation.PathConstraintSpacingTimeline) { + writePathConstraintSpacingTimeline(json, (Animation.PathConstraintSpacingTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintDampingTimeline) { + writePhysicsConstraintDampingTimeline(json, (Animation.PhysicsConstraintDampingTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintGravityTimeline) { + writePhysicsConstraintGravityTimeline(json, (Animation.PhysicsConstraintGravityTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintInertiaTimeline) { + writePhysicsConstraintInertiaTimeline(json, (Animation.PhysicsConstraintInertiaTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintMassTimeline) { + writePhysicsConstraintMassTimeline(json, (Animation.PhysicsConstraintMassTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintMixTimeline) { + writePhysicsConstraintMixTimeline(json, (Animation.PhysicsConstraintMixTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintResetTimeline) { + writePhysicsConstraintResetTimeline(json, (Animation.PhysicsConstraintResetTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintStrengthTimeline) { + writePhysicsConstraintStrengthTimeline(json, (Animation.PhysicsConstraintStrengthTimeline) obj); + } else if (obj instanceof Animation.PhysicsConstraintWindTimeline) { + writePhysicsConstraintWindTimeline(json, (Animation.PhysicsConstraintWindTimeline) obj); + } else if (obj instanceof Animation.RGB2Timeline) { + writeRGB2Timeline(json, (Animation.RGB2Timeline) obj); + } else if (obj instanceof Animation.RGBA2Timeline) { + writeRGBA2Timeline(json, (Animation.RGBA2Timeline) obj); + } else if (obj instanceof Animation.RGBATimeline) { + writeRGBATimeline(json, (Animation.RGBATimeline) obj); + } else if (obj instanceof Animation.RGBTimeline) { + writeRGBTimeline(json, (Animation.RGBTimeline) obj); + } else if (obj instanceof Animation.RotateTimeline) { + writeRotateTimeline(json, (Animation.RotateTimeline) obj); + } else if (obj instanceof Animation.ScaleTimeline) { + writeScaleTimeline(json, (Animation.ScaleTimeline) obj); + } else if (obj instanceof Animation.ScaleXTimeline) { + writeScaleXTimeline(json, (Animation.ScaleXTimeline) obj); + } else if (obj instanceof Animation.ScaleYTimeline) { + writeScaleYTimeline(json, (Animation.ScaleYTimeline) obj); + } else if (obj instanceof Animation.SequenceTimeline) { + writeSequenceTimeline(json, (Animation.SequenceTimeline) obj); + } else if (obj instanceof Animation.ShearTimeline) { + writeShearTimeline(json, (Animation.ShearTimeline) obj); + } else if (obj instanceof Animation.ShearXTimeline) { + writeShearXTimeline(json, (Animation.ShearXTimeline) obj); + } else if (obj instanceof Animation.ShearYTimeline) { + writeShearYTimeline(json, (Animation.ShearYTimeline) obj); + } else if (obj instanceof Animation.SliderMixTimeline) { + writeSliderMixTimeline(json, (Animation.SliderMixTimeline) obj); + } else if (obj instanceof Animation.SliderTimeline) { + writeSliderTimeline(json, (Animation.SliderTimeline) obj); + } else if (obj instanceof Animation.TransformConstraintTimeline) { + writeTransformConstraintTimeline(json, (Animation.TransformConstraintTimeline) obj); + } else if (obj instanceof Animation.TranslateTimeline) { + writeTranslateTimeline(json, (Animation.TranslateTimeline) obj); + } else if (obj instanceof Animation.TranslateXTimeline) { + writeTranslateXTimeline(json, (Animation.TranslateXTimeline) obj); + } else if (obj instanceof Animation.TranslateYTimeline) { + writeTranslateYTimeline(json, (Animation.TranslateYTimeline) obj); + } else { + throw new RuntimeException("Unknown Timeline type: " + obj.getClass().getName()); + } + } + + private void writeTransformConstraintTimeline(JsonWriter json, Animation.TransformConstraintTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TransformConstraintTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("constraintIndex"); + json.writeValue(obj.getConstraintIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeTranslateTimeline(JsonWriter json, Animation.TranslateTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TranslateTimeline"); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeTranslateXTimeline(JsonWriter json, Animation.TranslateXTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TranslateXTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeTranslateYTimeline(JsonWriter json, Animation.TranslateYTimeline obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TranslateYTimeline"); + + json.writeName("boneIndex"); + json.writeValue(obj.getBoneIndex()); + + json.writeName("frameEntries"); + json.writeValue(obj.getFrameEntries()); + + json.writeName("propertyIds"); + if (obj.getPropertyIds() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (String item : obj.getPropertyIds()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frames"); + if (obj.getFrames() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getFrames()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("frameCount"); + json.writeValue(obj.getFrameCount()); + + json.writeName("duration"); + json.writeValue(obj.getDuration()); + + json.writeObjectEnd(); + } + + private void writeAnimationState(JsonWriter json, AnimationState obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("AnimationState"); + + json.writeName("timeScale"); + json.writeValue(obj.getTimeScale()); + + json.writeName("data"); + writeAnimationStateData(json, obj.getData()); + + json.writeName("tracks"); + if (obj.getTracks() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (TrackEntry item : obj.getTracks()) { + writeTrackEntry(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeAnimationStateListener(JsonWriter json, AnimationState.AnimationStateListener obj) throws IOException { + json.writeNull(); // No concrete implementations + } + + private void writeTrackEntry(JsonWriter json, AnimationState.TrackEntry obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TrackEntry"); + + json.writeName("trackIndex"); + json.writeValue(obj.getTrackIndex()); + + json.writeName("animation"); + writeAnimation(json, obj.getAnimation()); + + json.writeName("loop"); + json.writeValue(obj.getLoop()); + + json.writeName("delay"); + json.writeValue(obj.getDelay()); + + json.writeName("trackTime"); + json.writeValue(obj.getTrackTime()); + + json.writeName("trackEnd"); + json.writeValue(obj.getTrackEnd()); + + json.writeName("trackComplete"); + json.writeValue(obj.getTrackComplete()); + + json.writeName("animationStart"); + json.writeValue(obj.getAnimationStart()); + + json.writeName("animationEnd"); + json.writeValue(obj.getAnimationEnd()); + + json.writeName("animationLast"); + json.writeValue(obj.getAnimationLast()); + + json.writeName("animationTime"); + json.writeValue(obj.getAnimationTime()); + + json.writeName("timeScale"); + json.writeValue(obj.getTimeScale()); + + json.writeName("listener"); + if (obj.getListener() == null) { + json.writeNull(); + } else { + writeAnimationStateListener(json, obj.getListener()); + } + + json.writeName("alpha"); + json.writeValue(obj.getAlpha()); + + json.writeName("eventThreshold"); + json.writeValue(obj.getEventThreshold()); + + json.writeName("alphaAttachmentThreshold"); + json.writeValue(obj.getAlphaAttachmentThreshold()); + + json.writeName("mixAttachmentThreshold"); + json.writeValue(obj.getMixAttachmentThreshold()); + + json.writeName("mixDrawOrderThreshold"); + json.writeValue(obj.getMixDrawOrderThreshold()); + + json.writeName("next"); + if (obj.getNext() == null) { + json.writeNull(); + } else { + writeTrackEntry(json, obj.getNext()); + } + + json.writeName("previous"); + if (obj.getPrevious() == null) { + json.writeNull(); + } else { + writeTrackEntry(json, obj.getPrevious()); + } + + json.writeName("mixTime"); + json.writeValue(obj.getMixTime()); + + json.writeName("mixDuration"); + json.writeValue(obj.getMixDuration()); + + json.writeName("mixBlend"); + json.writeValue(obj.getMixBlend().name()); + + json.writeName("mixingFrom"); + if (obj.getMixingFrom() == null) { + json.writeNull(); + } else { + writeTrackEntry(json, obj.getMixingFrom()); + } + + json.writeName("mixingTo"); + if (obj.getMixingTo() == null) { + json.writeNull(); + } else { + writeTrackEntry(json, obj.getMixingTo()); + } + + json.writeName("holdPrevious"); + json.writeValue(obj.getHoldPrevious()); + + json.writeName("shortestRotation"); + json.writeValue(obj.getShortestRotation()); + + json.writeName("reverse"); + json.writeValue(obj.getReverse()); + + json.writeObjectEnd(); + } + + private void writeAnimationStateData(JsonWriter json, AnimationStateData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("AnimationStateData"); + + json.writeName("skeletonData"); + writeSkeletonData(json, obj.getSkeletonData()); + + json.writeName("defaultMix"); + json.writeValue(obj.getDefaultMix()); + + json.writeObjectEnd(); + } + + private void writeAttachment(JsonWriter json, Attachment obj) throws IOException { + if (obj instanceof BoundingBoxAttachment) { + writeBoundingBoxAttachment(json, (BoundingBoxAttachment) obj); + } else if (obj instanceof ClippingAttachment) { + writeClippingAttachment(json, (ClippingAttachment) obj); + } else if (obj instanceof MeshAttachment) { + writeMeshAttachment(json, (MeshAttachment) obj); + } else if (obj instanceof PathAttachment) { + writePathAttachment(json, (PathAttachment) obj); + } else if (obj instanceof PointAttachment) { + writePointAttachment(json, (PointAttachment) obj); + } else if (obj instanceof RegionAttachment) { + writeRegionAttachment(json, (RegionAttachment) obj); + } else if (obj instanceof SkeletonAttachment) { + writeSkeletonAttachment(json, (SkeletonAttachment) obj); + } else { + throw new RuntimeException("Unknown Attachment type: " + obj.getClass().getName()); + } + } + + private void writeBlendMode(JsonWriter json, BlendMode obj) throws IOException { + json.writeValue(obj.name()); + } + + private void writeBone(JsonWriter json, Bone obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Bone"); + + json.writeName("parent"); + if (obj.getParent() == null) { + json.writeNull(); + } else { + writeBone(json, obj.getParent()); + } + + json.writeName("children"); + if (obj.getChildren() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Bone item : obj.getChildren()) { + writeBone(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("data"); + writeBoneData(json, obj.getData()); + + json.writeName("pose"); + writeBoneLocal(json, obj.getPose()); + + json.writeName("appliedPose"); + writeBonePose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writeBoneData(JsonWriter json, BoneData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("BoneData"); + + json.writeName("index"); + json.writeValue(obj.getIndex()); + + json.writeName("parent"); + if (obj.getParent() == null) { + json.writeNull(); + } else { + writeBoneData(json, obj.getParent()); + } + + json.writeName("length"); + json.writeValue(obj.getLength()); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("icon"); + json.writeValue(obj.getIcon()); + + json.writeName("visible"); + json.writeValue(obj.getVisible()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writeBoneLocal(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writeBoneLocal(JsonWriter json, BoneLocal obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("BoneLocal"); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("rotation"); + json.writeValue(obj.getRotation()); + + json.writeName("scaleX"); + json.writeValue(obj.getScaleX()); + + json.writeName("scaleY"); + json.writeValue(obj.getScaleY()); + + json.writeName("shearX"); + json.writeValue(obj.getShearX()); + + json.writeName("shearY"); + json.writeValue(obj.getShearY()); + + json.writeName("inherit"); + json.writeValue(obj.getInherit().name()); + + json.writeObjectEnd(); + } + + private void writeBonePose(JsonWriter json, BonePose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("BonePose"); + + json.writeName("a"); + json.writeValue(obj.getA()); + + json.writeName("b"); + json.writeValue(obj.getB()); + + json.writeName("c"); + json.writeValue(obj.getC()); + + json.writeName("d"); + json.writeValue(obj.getD()); + + json.writeName("worldX"); + json.writeValue(obj.getWorldX()); + + json.writeName("worldY"); + json.writeValue(obj.getWorldY()); + + json.writeName("worldRotationX"); + json.writeValue(obj.getWorldRotationX()); + + json.writeName("worldRotationY"); + json.writeValue(obj.getWorldRotationY()); + + json.writeName("worldScaleX"); + json.writeValue(obj.getWorldScaleX()); + + json.writeName("worldScaleY"); + json.writeValue(obj.getWorldScaleY()); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("rotation"); + json.writeValue(obj.getRotation()); + + json.writeName("scaleX"); + json.writeValue(obj.getScaleX()); + + json.writeName("scaleY"); + json.writeValue(obj.getScaleY()); + + json.writeName("shearX"); + json.writeValue(obj.getShearX()); + + json.writeName("shearY"); + json.writeValue(obj.getShearY()); + + json.writeName("inherit"); + json.writeValue(obj.getInherit().name()); + + json.writeObjectEnd(); + } + + private void writeBoundingBoxAttachment(JsonWriter json, BoundingBoxAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("BoundingBoxAttachment"); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int item : obj.getBones()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("vertices"); + if (obj.getVertices() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getVertices()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("worldVerticesLength"); + json.writeValue(obj.getWorldVerticesLength()); + + json.writeName("timelineAttachment"); + if (obj.getTimelineAttachment() == null) { + json.writeNull(); + } else { + writeAttachment(json, obj.getTimelineAttachment()); + } + + json.writeName("id"); + json.writeValue(obj.getId()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeClippingAttachment(JsonWriter json, ClippingAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ClippingAttachment"); + + json.writeName("endSlot"); + if (obj.getEndSlot() == null) { + json.writeNull(); + } else { + writeSlotData(json, obj.getEndSlot()); + } + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int item : obj.getBones()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("vertices"); + if (obj.getVertices() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getVertices()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("worldVerticesLength"); + json.writeValue(obj.getWorldVerticesLength()); + + json.writeName("timelineAttachment"); + if (obj.getTimelineAttachment() == null) { + json.writeNull(); + } else { + writeAttachment(json, obj.getTimelineAttachment()); + } + + json.writeName("id"); + json.writeValue(obj.getId()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeConstraint(JsonWriter json, Constraint obj) throws IOException { + if (obj instanceof IkConstraint) { + writeIkConstraint(json, (IkConstraint) obj); + } else if (obj instanceof PathConstraint) { + writePathConstraint(json, (PathConstraint) obj); + } else if (obj instanceof PhysicsConstraint) { + writePhysicsConstraint(json, (PhysicsConstraint) obj); + } else if (obj instanceof Slider) { + writeSlider(json, (Slider) obj); + } else if (obj instanceof TransformConstraint) { + writeTransformConstraint(json, (TransformConstraint) obj); + } else { + throw new RuntimeException("Unknown Constraint type: " + obj.getClass().getName()); + } + } + + private void writeConstraintData(JsonWriter json, ConstraintData obj) throws IOException { + if (obj instanceof IkConstraintData) { + writeIkConstraintData(json, (IkConstraintData) obj); + } else if (obj instanceof PathConstraintData) { + writePathConstraintData(json, (PathConstraintData) obj); + } else if (obj instanceof PhysicsConstraintData) { + writePhysicsConstraintData(json, (PhysicsConstraintData) obj); + } else if (obj instanceof SliderData) { + writeSliderData(json, (SliderData) obj); + } else if (obj instanceof TransformConstraintData) { + writeTransformConstraintData(json, (TransformConstraintData) obj); + } else { + throw new RuntimeException("Unknown ConstraintData type: " + obj.getClass().getName()); + } + } + + private void writeEvent(JsonWriter json, Event obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Event"); + + json.writeName("int"); + json.writeValue(obj.getInt()); + + json.writeName("float"); + json.writeValue(obj.getFloat()); + + json.writeName("string"); + json.writeValue(obj.getString()); + + json.writeName("volume"); + json.writeValue(obj.getVolume()); + + json.writeName("balance"); + json.writeValue(obj.getBalance()); + + json.writeName("time"); + json.writeValue(obj.getTime()); + + json.writeName("data"); + writeEventData(json, obj.getData()); + + json.writeObjectEnd(); + } + + private void writeEventData(JsonWriter json, EventData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("EventData"); + + json.writeName("int"); + json.writeValue(obj.getInt()); + + json.writeName("float"); + json.writeValue(obj.getFloat()); + + json.writeName("string"); + json.writeValue(obj.getString()); + + json.writeName("audioPath"); + json.writeValue(obj.getAudioPath()); + + json.writeName("volume"); + json.writeValue(obj.getVolume()); + + json.writeName("balance"); + json.writeValue(obj.getBalance()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeIkConstraint(JsonWriter json, IkConstraint obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("IkConstraint"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BonePose item : obj.getBones()) { + writeBonePose(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("target"); + writeBone(json, obj.getTarget()); + + json.writeName("data"); + writeIkConstraintData(json, obj.getData()); + + json.writeName("pose"); + writeIkConstraintPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writeIkConstraintPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writeIkConstraintData(JsonWriter json, IkConstraintData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("IkConstraintData"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BoneData item : obj.getBones()) { + writeBoneData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("target"); + writeBoneData(json, obj.getTarget()); + + json.writeName("uniform"); + json.writeValue(obj.getUniform()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writeIkConstraintPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writeIkConstraintPose(JsonWriter json, IkConstraintPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("IkConstraintPose"); + + json.writeName("mix"); + json.writeValue(obj.getMix()); + + json.writeName("softness"); + json.writeValue(obj.getSoftness()); + + json.writeName("bendDirection"); + json.writeValue(obj.getBendDirection()); + + json.writeName("compress"); + json.writeValue(obj.getCompress()); + + json.writeName("stretch"); + json.writeValue(obj.getStretch()); + + json.writeObjectEnd(); + } + + private void writeMeshAttachment(JsonWriter json, MeshAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("MeshAttachment"); + + json.writeName("region"); + writeTextureRegion(json, obj.getRegion()); + + json.writeName("triangles"); + if (obj.getTriangles() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (short item : obj.getTriangles()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("regionUVs"); + if (obj.getRegionUVs() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getRegionUVs()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("uVs"); + if (obj.getUVs() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getUVs()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("path"); + json.writeValue(obj.getPath()); + + json.writeName("hullLength"); + json.writeValue(obj.getHullLength()); + + json.writeName("edges"); + if (obj.getEdges() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (short item : obj.getEdges()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("width"); + json.writeValue(obj.getWidth()); + + json.writeName("height"); + json.writeValue(obj.getHeight()); + + json.writeName("sequence"); + if (obj.getSequence() == null) { + json.writeNull(); + } else { + writeSequence(json, obj.getSequence()); + } + + json.writeName("parentMesh"); + if (obj.getParentMesh() == null) { + json.writeNull(); + } else { + writeMeshAttachment(json, obj.getParentMesh()); + } + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int item : obj.getBones()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("vertices"); + if (obj.getVertices() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getVertices()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("worldVerticesLength"); + json.writeValue(obj.getWorldVerticesLength()); + + json.writeName("timelineAttachment"); + if (obj.getTimelineAttachment() == null) { + json.writeNull(); + } else { + writeAttachment(json, obj.getTimelineAttachment()); + } + + json.writeName("id"); + json.writeValue(obj.getId()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writePathAttachment(JsonWriter json, PathAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathAttachment"); + + json.writeName("closed"); + json.writeValue(obj.getClosed()); + + json.writeName("constantSpeed"); + json.writeValue(obj.getConstantSpeed()); + + json.writeName("lengths"); + if (obj.getLengths() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getLengths()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int item : obj.getBones()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("vertices"); + if (obj.getVertices() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getVertices()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("worldVerticesLength"); + json.writeValue(obj.getWorldVerticesLength()); + + json.writeName("timelineAttachment"); + if (obj.getTimelineAttachment() == null) { + json.writeNull(); + } else { + writeAttachment(json, obj.getTimelineAttachment()); + } + + json.writeName("id"); + json.writeValue(obj.getId()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writePathConstraint(JsonWriter json, PathConstraint obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraint"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BonePose item : obj.getBones()) { + writeBonePose(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("slot"); + writeSlot(json, obj.getSlot()); + + json.writeName("data"); + writePathConstraintData(json, obj.getData()); + + json.writeName("pose"); + writePathConstraintPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writePathConstraintPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writePathConstraintData(JsonWriter json, PathConstraintData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraintData"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BoneData item : obj.getBones()) { + writeBoneData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("slot"); + writeSlotData(json, obj.getSlot()); + + json.writeName("positionMode"); + json.writeValue(obj.getPositionMode().name()); + + json.writeName("spacingMode"); + json.writeValue(obj.getSpacingMode().name()); + + json.writeName("rotateMode"); + json.writeValue(obj.getRotateMode().name()); + + json.writeName("offsetRotation"); + json.writeValue(obj.getOffsetRotation()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writePathConstraintPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writePathConstraintPose(JsonWriter json, PathConstraintPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PathConstraintPose"); + + json.writeName("position"); + json.writeValue(obj.getPosition()); + + json.writeName("spacing"); + json.writeValue(obj.getSpacing()); + + json.writeName("mixRotate"); + json.writeValue(obj.getMixRotate()); + + json.writeName("mixX"); + json.writeValue(obj.getMixX()); + + json.writeName("mixY"); + json.writeValue(obj.getMixY()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraint(JsonWriter json, PhysicsConstraint obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraint"); + + json.writeName("bone"); + writeBonePose(json, obj.getBone()); + + json.writeName("data"); + writePhysicsConstraintData(json, obj.getData()); + + json.writeName("pose"); + writePhysicsConstraintPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writePhysicsConstraintPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintData(JsonWriter json, PhysicsConstraintData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintData"); + + json.writeName("bone"); + writeBoneData(json, obj.getBone()); + + json.writeName("step"); + json.writeValue(obj.getStep()); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("rotate"); + json.writeValue(obj.getRotate()); + + json.writeName("scaleX"); + json.writeValue(obj.getScaleX()); + + json.writeName("shearX"); + json.writeValue(obj.getShearX()); + + json.writeName("limit"); + json.writeValue(obj.getLimit()); + + json.writeName("inertiaGlobal"); + json.writeValue(obj.getInertiaGlobal()); + + json.writeName("strengthGlobal"); + json.writeValue(obj.getStrengthGlobal()); + + json.writeName("dampingGlobal"); + json.writeValue(obj.getDampingGlobal()); + + json.writeName("massGlobal"); + json.writeValue(obj.getMassGlobal()); + + json.writeName("windGlobal"); + json.writeValue(obj.getWindGlobal()); + + json.writeName("gravityGlobal"); + json.writeValue(obj.getGravityGlobal()); + + json.writeName("mixGlobal"); + json.writeValue(obj.getMixGlobal()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writePhysicsConstraintPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writePhysicsConstraintPose(JsonWriter json, PhysicsConstraintPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PhysicsConstraintPose"); + + json.writeName("inertia"); + json.writeValue(obj.getInertia()); + + json.writeName("strength"); + json.writeValue(obj.getStrength()); + + json.writeName("damping"); + json.writeValue(obj.getDamping()); + + json.writeName("massInverse"); + json.writeValue(obj.getMassInverse()); + + json.writeName("wind"); + json.writeValue(obj.getWind()); + + json.writeName("gravity"); + json.writeValue(obj.getGravity()); + + json.writeName("mix"); + json.writeValue(obj.getMix()); + + json.writeObjectEnd(); + } + + private void writePointAttachment(JsonWriter json, PointAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("PointAttachment"); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("rotation"); + json.writeValue(obj.getRotation()); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeRegionAttachment(JsonWriter json, RegionAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("RegionAttachment"); + + json.writeName("region"); + writeTextureRegion(json, obj.getRegion()); + + json.writeName("offset"); + if (obj.getOffset() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getOffset()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("uVs"); + if (obj.getUVs() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (float item : obj.getUVs()) { + json.writeValue(item); + } + json.writeArrayEnd(); + } + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("scaleX"); + json.writeValue(obj.getScaleX()); + + json.writeName("scaleY"); + json.writeValue(obj.getScaleY()); + + json.writeName("rotation"); + json.writeValue(obj.getRotation()); + + json.writeName("width"); + json.writeValue(obj.getWidth()); + + json.writeName("height"); + json.writeValue(obj.getHeight()); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("path"); + json.writeValue(obj.getPath()); + + json.writeName("sequence"); + if (obj.getSequence() == null) { + json.writeNull(); + } else { + writeSequence(json, obj.getSequence()); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeSequence(JsonWriter json, Sequence obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Sequence"); + + json.writeName("start"); + json.writeValue(obj.getStart()); + + json.writeName("digits"); + json.writeValue(obj.getDigits()); + + json.writeName("setupIndex"); + json.writeValue(obj.getSetupIndex()); + + json.writeName("regions"); + if (obj.getRegions() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (TextureRegion item : obj.getRegions()) { + writeTextureRegion(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("id"); + json.writeValue(obj.getId()); + + json.writeObjectEnd(); + } + + private void writeSkeleton(JsonWriter json, Skeleton obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Skeleton"); + + json.writeName("data"); + writeSkeletonData(json, obj.getData()); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Bone item : obj.getBones()) { + writeBone(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("updateCache"); + if (obj.getUpdateCache() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Update item : obj.getUpdateCache()) { + writeUpdate(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("rootBone"); + writeBone(json, obj.getRootBone()); + + json.writeName("slots"); + if (obj.getSlots() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Slot item : obj.getSlots()) { + writeSlot(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("drawOrder"); + if (obj.getDrawOrder() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Slot item : obj.getDrawOrder()) { + writeSlot(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("skin"); + if (obj.getSkin() == null) { + json.writeNull(); + } else { + writeSkin(json, obj.getSkin()); + } + + json.writeName("constraints"); + if (obj.getConstraints() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Constraint item : obj.getConstraints()) { + writeConstraint(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("physicsConstraints"); + if (obj.getPhysicsConstraints() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (PhysicsConstraint item : obj.getPhysicsConstraints()) { + writePhysicsConstraint(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("scaleX"); + json.writeValue(obj.getScaleX()); + + json.writeName("scaleY"); + json.writeValue(obj.getScaleY()); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("windX"); + json.writeValue(obj.getWindX()); + + json.writeName("windY"); + json.writeValue(obj.getWindY()); + + json.writeName("gravityX"); + json.writeValue(obj.getGravityX()); + + json.writeName("gravityY"); + json.writeValue(obj.getGravityY()); + + json.writeName("time"); + json.writeValue(obj.getTime()); + + json.writeObjectEnd(); + } + + private void writeSkeletonAttachment(JsonWriter json, SkeletonAttachment obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SkeletonAttachment"); + + json.writeName("skeleton"); + if (obj.getSkeleton() == null) { + json.writeNull(); + } else { + writeSkeleton(json, obj.getSkeleton()); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeObjectEnd(); + } + + private void writeSkeletonData(JsonWriter json, SkeletonData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SkeletonData"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BoneData item : obj.getBones()) { + writeBoneData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("slots"); + if (obj.getSlots() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (SlotData item : obj.getSlots()) { + writeSlotData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("defaultSkin"); + if (obj.getDefaultSkin() == null) { + json.writeNull(); + } else { + writeSkin(json, obj.getDefaultSkin()); + } + + json.writeName("skins"); + if (obj.getSkins() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Skin item : obj.getSkins()) { + writeSkin(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("events"); + if (obj.getEvents() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (EventData item : obj.getEvents()) { + writeEventData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("animations"); + if (obj.getAnimations() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (Animation item : obj.getAnimations()) { + writeAnimation(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("constraints"); + if (obj.getConstraints() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ConstraintData item : obj.getConstraints()) { + writeConstraintData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("x"); + json.writeValue(obj.getX()); + + json.writeName("y"); + json.writeValue(obj.getY()); + + json.writeName("width"); + json.writeValue(obj.getWidth()); + + json.writeName("height"); + json.writeValue(obj.getHeight()); + + json.writeName("referenceScale"); + json.writeValue(obj.getReferenceScale()); + + json.writeName("version"); + json.writeValue(obj.getVersion()); + + json.writeName("hash"); + json.writeValue(obj.getHash()); + + json.writeName("imagesPath"); + json.writeValue(obj.getImagesPath()); + + json.writeName("audioPath"); + json.writeValue(obj.getAudioPath()); + + json.writeName("fps"); + json.writeValue(obj.getFps()); + + json.writeObjectEnd(); + } + + private void writeSkin(JsonWriter json, Skin obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Skin"); + + json.writeName("attachments"); + if (obj.getAttachments() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (SkinEntry item : obj.getAttachments()) { + writeSkinEntry(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BoneData item : obj.getBones()) { + writeBoneData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("constraints"); + if (obj.getConstraints() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ConstraintData item : obj.getConstraints()) { + writeConstraintData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeObjectEnd(); + } + + private void writeSkinEntry(JsonWriter json, Skin.SkinEntry obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SkinEntry"); + + json.writeName("slotIndex"); + json.writeValue(obj.getSlotIndex()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("attachment"); + writeAttachment(json, obj.getAttachment()); + + json.writeObjectEnd(); + } + + private void writeSlider(JsonWriter json, Slider obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Slider"); + + json.writeName("bone"); + writeBone(json, obj.getBone()); + + json.writeName("data"); + writeSliderData(json, obj.getData()); + + json.writeName("pose"); + writeSliderPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writeSliderPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writeSliderData(JsonWriter json, SliderData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SliderData"); + + json.writeName("animation"); + writeAnimation(json, obj.getAnimation()); + + json.writeName("additive"); + json.writeValue(obj.getAdditive()); + + json.writeName("loop"); + json.writeValue(obj.getLoop()); + + json.writeName("bone"); + if (obj.getBone() == null) { + json.writeNull(); + } else { + writeBoneData(json, obj.getBone()); + } + + json.writeName("property"); + if (obj.getProperty() == null) { + json.writeNull(); + } else { + writeFromProperty(json, obj.getProperty()); + } + + json.writeName("offset"); + json.writeValue(obj.getOffset()); + + json.writeName("scale"); + json.writeValue(obj.getScale()); + + json.writeName("local"); + json.writeValue(obj.getLocal()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writeSliderPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writeSliderPose(JsonWriter json, SliderPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SliderPose"); + + json.writeName("time"); + json.writeValue(obj.getTime()); + + json.writeName("mix"); + json.writeValue(obj.getMix()); + + json.writeObjectEnd(); + } + + private void writeSlot(JsonWriter json, Slot obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("Slot"); + + json.writeName("bone"); + writeBone(json, obj.getBone()); + + json.writeName("data"); + writeSlotData(json, obj.getData()); + + json.writeName("pose"); + writeSlotPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writeSlotPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writeSlotData(JsonWriter json, SlotData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SlotData"); + + json.writeName("index"); + json.writeValue(obj.getIndex()); + + json.writeName("boneData"); + writeBoneData(json, obj.getBoneData()); + + json.writeName("attachmentName"); + json.writeValue(obj.getAttachmentName()); + + json.writeName("blendMode"); + json.writeValue(obj.getBlendMode().name()); + + json.writeName("visible"); + json.writeValue(obj.getVisible()); + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writeSlotPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writeSlotPose(JsonWriter json, SlotPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("SlotPose"); + + json.writeName("color"); + writeColor(json, obj.getColor()); + + json.writeName("darkColor"); + writeColor(json, obj.getDarkColor()); + + json.writeName("attachment"); + if (obj.getAttachment() == null) { + json.writeNull(); + } else { + writeAttachment(json, obj.getAttachment()); + } + + json.writeName("sequenceIndex"); + json.writeValue(obj.getSequenceIndex()); + + json.writeName("deform"); + if (obj.getDeform() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (int i = 0; i < obj.getDeform().size; i++) { + json.writeValue(obj.getDeform().get(i)); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeTransformConstraint(JsonWriter json, TransformConstraint obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TransformConstraint"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BonePose item : obj.getBones()) { + writeBonePose(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("source"); + writeBone(json, obj.getSource()); + + json.writeName("data"); + writeTransformConstraintData(json, obj.getData()); + + json.writeName("pose"); + writeTransformConstraintPose(json, obj.getPose()); + + json.writeName("appliedPose"); + writeTransformConstraintPose(json, obj.getAppliedPose()); + + json.writeObjectEnd(); + } + + private void writeTransformConstraintData(JsonWriter json, TransformConstraintData obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TransformConstraintData"); + + json.writeName("bones"); + if (obj.getBones() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (BoneData item : obj.getBones()) { + writeBoneData(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("source"); + writeBoneData(json, obj.getSource()); + + json.writeName("offsetRotation"); + json.writeValue(obj.getOffsetRotation()); + + json.writeName("offsetX"); + json.writeValue(obj.getOffsetX()); + + json.writeName("offsetY"); + json.writeValue(obj.getOffsetY()); + + json.writeName("offsetScaleX"); + json.writeValue(obj.getOffsetScaleX()); + + json.writeName("offsetScaleY"); + json.writeValue(obj.getOffsetScaleY()); + + json.writeName("offsetShearY"); + json.writeValue(obj.getOffsetShearY()); + + json.writeName("localSource"); + json.writeValue(obj.getLocalSource()); + + json.writeName("localTarget"); + json.writeValue(obj.getLocalTarget()); + + json.writeName("additive"); + json.writeValue(obj.getAdditive()); + + json.writeName("clamp"); + json.writeValue(obj.getClamp()); + + json.writeName("properties"); + if (obj.getProperties() == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (FromProperty item : obj.getProperties()) { + writeFromProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeName("name"); + json.writeValue(obj.getName()); + + json.writeName("setupPose"); + writeTransformConstraintPose(json, obj.getSetupPose()); + + json.writeName("skinRequired"); + json.writeValue(obj.getSkinRequired()); + + json.writeObjectEnd(); + } + + private void writeFromProperty(JsonWriter json, TransformConstraintData.FromProperty obj) throws IOException { + if (obj instanceof TransformConstraintData.FromRotate) { + writeFromRotate(json, (TransformConstraintData.FromRotate) obj); + } else if (obj instanceof TransformConstraintData.FromScaleX) { + writeFromScaleX(json, (TransformConstraintData.FromScaleX) obj); + } else if (obj instanceof TransformConstraintData.FromScaleY) { + writeFromScaleY(json, (TransformConstraintData.FromScaleY) obj); + } else if (obj instanceof TransformConstraintData.FromShearY) { + writeFromShearY(json, (TransformConstraintData.FromShearY) obj); + } else if (obj instanceof TransformConstraintData.FromX) { + writeFromX(json, (TransformConstraintData.FromX) obj); + } else if (obj instanceof TransformConstraintData.FromY) { + writeFromY(json, (TransformConstraintData.FromY) obj); + } else { + throw new RuntimeException("Unknown FromProperty type: " + obj.getClass().getName()); + } + } + + private void writeFromRotate(JsonWriter json, TransformConstraintData.FromRotate obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromRotate"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeFromScaleX(JsonWriter json, TransformConstraintData.FromScaleX obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromScaleX"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeFromScaleY(JsonWriter json, TransformConstraintData.FromScaleY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromScaleY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeFromShearY(JsonWriter json, TransformConstraintData.FromShearY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromShearY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeFromX(JsonWriter json, TransformConstraintData.FromX obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromX"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeFromY(JsonWriter json, TransformConstraintData.FromY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("FromY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("to"); + if (obj.to == null) { + json.writeNull(); + } else { + json.writeArrayStart(); + for (ToProperty item : obj.to) { + writeToProperty(json, item); + } + json.writeArrayEnd(); + } + + json.writeObjectEnd(); + } + + private void writeToProperty(JsonWriter json, TransformConstraintData.ToProperty obj) throws IOException { + if (obj instanceof TransformConstraintData.ToRotate) { + writeToRotate(json, (TransformConstraintData.ToRotate) obj); + } else if (obj instanceof TransformConstraintData.ToScaleX) { + writeToScaleX(json, (TransformConstraintData.ToScaleX) obj); + } else if (obj instanceof TransformConstraintData.ToScaleY) { + writeToScaleY(json, (TransformConstraintData.ToScaleY) obj); + } else if (obj instanceof TransformConstraintData.ToShearY) { + writeToShearY(json, (TransformConstraintData.ToShearY) obj); + } else if (obj instanceof TransformConstraintData.ToX) { + writeToX(json, (TransformConstraintData.ToX) obj); + } else if (obj instanceof TransformConstraintData.ToY) { + writeToY(json, (TransformConstraintData.ToY) obj); + } else { + throw new RuntimeException("Unknown ToProperty type: " + obj.getClass().getName()); + } + } + + private void writeToRotate(JsonWriter json, TransformConstraintData.ToRotate obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToRotate"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeToScaleX(JsonWriter json, TransformConstraintData.ToScaleX obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToScaleX"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeToScaleY(JsonWriter json, TransformConstraintData.ToScaleY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToScaleY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeToShearY(JsonWriter json, TransformConstraintData.ToShearY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToShearY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeToX(JsonWriter json, TransformConstraintData.ToX obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToX"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeToY(JsonWriter json, TransformConstraintData.ToY obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("ToY"); + + json.writeName("offset"); + json.writeValue(obj.offset); + + json.writeName("max"); + json.writeValue(obj.max); + + json.writeName("scale"); + json.writeValue(obj.scale); + + json.writeObjectEnd(); + } + + private void writeTransformConstraintPose(JsonWriter json, TransformConstraintPose obj) throws IOException { + if (visitedObjects.contains(obj)) { + json.writeValue(""); + return; + } + visitedObjects.add(obj); + + json.writeObjectStart(); + json.writeName("type"); + json.writeValue("TransformConstraintPose"); + + json.writeName("mixRotate"); + json.writeValue(obj.getMixRotate()); + + json.writeName("mixX"); + json.writeValue(obj.getMixX()); + + json.writeName("mixY"); + json.writeValue(obj.getMixY()); + + json.writeName("mixScaleX"); + json.writeValue(obj.getMixScaleX()); + + json.writeName("mixScaleY"); + json.writeValue(obj.getMixScaleY()); + + json.writeName("mixShearY"); + json.writeValue(obj.getMixShearY()); + + json.writeObjectEnd(); + } + + private void writeUpdate(JsonWriter json, Update obj) throws IOException { + if (obj instanceof BonePose) { + writeBonePose(json, (BonePose) obj); + } else if (obj instanceof IkConstraint) { + writeIkConstraint(json, (IkConstraint) obj); + } else if (obj instanceof PathConstraint) { + writePathConstraint(json, (PathConstraint) obj); + } else if (obj instanceof PhysicsConstraint) { + writePhysicsConstraint(json, (PhysicsConstraint) obj); + } else if (obj instanceof Slider) { + writeSlider(json, (Slider) obj); + } else if (obj instanceof TransformConstraint) { + writeTransformConstraint(json, (TransformConstraint) obj); + } else { + throw new RuntimeException("Unknown Update type: " + obj.getClass().getName()); + } + } + + private void writeVertexAttachment(JsonWriter json, VertexAttachment obj) throws IOException { + if (obj instanceof BoundingBoxAttachment) { + writeBoundingBoxAttachment(json, (BoundingBoxAttachment) obj); + } else if (obj instanceof ClippingAttachment) { + writeClippingAttachment(json, (ClippingAttachment) obj); + } else if (obj instanceof MeshAttachment) { + writeMeshAttachment(json, (MeshAttachment) obj); + } else if (obj instanceof PathAttachment) { + writePathAttachment(json, (PathAttachment) obj); + } else { + throw new RuntimeException("Unknown VertexAttachment type: " + obj.getClass().getName()); + } + } + + private void writeColor(JsonWriter json, Color color) throws IOException { + if (color == null) { + json.writeNull(); + } else { + json.writeObjectStart(); + json.writeName("r"); + json.writeValue(color.r); + json.writeName("g"); + json.writeValue(color.g); + json.writeName("b"); + json.writeValue(color.b); + json.writeName("a"); + json.writeValue(color.a); + json.writeObjectEnd(); + } + } + + private void writeTextureRegion(JsonWriter json, TextureRegion region) throws IOException { + if (region == null) { + json.writeNull(); + } else { + json.writeObjectStart(); + json.writeName("u"); + json.writeValue(region.getU()); + json.writeName("v"); + json.writeValue(region.getV()); + json.writeName("u2"); + json.writeValue(region.getU2()); + json.writeName("v2"); + json.writeValue(region.getV2()); + json.writeName("width"); + json.writeValue(region.getRegionWidth()); + json.writeName("height"); + json.writeValue(region.getRegionHeight()); + json.writeObjectEnd(); + } + } + + private static class JsonWriter { + private final Writer writer; + private int depth = 0; + private boolean needsComma = false; + + JsonWriter(Writer writer) { + this.writer = writer; + } + + void writeObjectStart() throws IOException { + writeCommaIfNeeded(); + writer.write("{"); + depth++; + needsComma = false; + } + + void writeObjectEnd() throws IOException { + depth--; + if (needsComma) { + writer.write("\n"); + writeIndent(); + } + writer.write("}"); + needsComma = true; + } + + void writeArrayStart() throws IOException { + writeCommaIfNeeded(); + writer.write("["); + depth++; + needsComma = false; + } + + void writeArrayEnd() throws IOException { + depth--; + if (needsComma) { + writer.write("\n"); + writeIndent(); + } + writer.write("]"); + needsComma = true; + } + + void writeName(String name) throws IOException { + writeCommaIfNeeded(); + writer.write("\n"); + writeIndent(); + writer.write("\"" + name + "\": "); + needsComma = false; + } + + void writeValue(String value) throws IOException { + writeCommaIfNeeded(); + if (value == null) { + writer.write("null"); + } else { + writer.write("\"" + escapeString(value) + "\""); + } + needsComma = true; + } + + void writeValue(float value) throws IOException { + writeCommaIfNeeded(); + writer.write(String.format(Locale.US, "%.6f", value).replaceAll("0+$", "").replaceAll("\\.$", "")); + needsComma = true; + } + + void writeValue(int value) throws IOException { + writeCommaIfNeeded(); + writer.write(String.valueOf(value)); + needsComma = true; + } + + void writeValue(boolean value) throws IOException { + writeCommaIfNeeded(); + writer.write(String.valueOf(value)); + needsComma = true; + } + + void writeNull() throws IOException { + writeCommaIfNeeded(); + writer.write("null"); + needsComma = true; + } + + void close() throws IOException { + writer.write("\n"); + writer.flush(); + } + + private void writeCommaIfNeeded() throws IOException { + if (needsComma) { + writer.write(","); + } + } + + private void writeIndent() throws IOException { + for (int i = 0; i < depth; i++) { + writer.write(" "); + } + } + + private String escapeString(String str) { + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\b", "\\b") + .replace("\f", "\\f") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } +} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md index b80a171e0..757938499 100644 --- a/tests/README.md +++ b/tests/README.md @@ -25,6 +25,9 @@ Each runtime has a HeadlessTest program that outputs skeleton data in a standard ```bash cd spine-libgdx ./gradlew :spine-libgdx-tests:runHeadlessTest -Pargs=" [animation-name]" + +# Example with spineboy: +./gradlew :spine-libgdx-tests:runHeadlessTest -Pargs="../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk" ``` ### C++ (spine-cpp) @@ -32,6 +35,9 @@ cd spine-libgdx cd spine-cpp ./build.sh # Build if needed ./build/headless-test [animation-name] + +# Example with spineboy: +./build/headless-test ../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk ``` ### C (spine-c) @@ -39,12 +45,18 @@ cd spine-cpp cd spine-c ./build.sh # Build if needed ./build/headless-test [animation-name] + +# Example with spineboy: +./build/headless-test ../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk ``` ### TypeScript (spine-ts) ```bash cd spine-ts/spine-core npx tsx tests/HeadlessTest.ts [animation-name] + +# Example with spineboy: +npx tsx tests/HeadlessTest.ts ../../examples/spineboy/export/spineboy-pro.json ../../examples/spineboy/export/spineboy.atlas walk ``` ## Running the Comparison Test @@ -82,12 +94,41 @@ This script will: Each HeadlessTest outputs: - **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata) - **SKELETON STATE**: Runtime state after applying animations +- **ANIMATION STATE**: Current animation state with tracks and mixing information -The output uses consistent formatting: +The output uses consistent JSON formatting: - Hierarchical structure with 2-space indentation - Float values formatted to 6 decimal places - Strings quoted, nulls explicitly shown - Locale-independent number formatting (always uses `.` for decimals) +- Circular references marked as `""` to prevent infinite recursion +- Each object includes a `"type"` field for easy identification + +## Development Tools + +### API Analyzer (Java) +Analyzes the spine-libgdx API to discover all types and their properties: +```bash +cd tests +npx tsx analyze-java-api.ts +# Output: output/analysis-result.json +``` + +### Serializer Generator (Java) +Generates SkeletonSerializer.java from the analysis: +```bash +cd tests +npx tsx generate-java-serializer.ts +# Output: ../spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java +``` + +### Claude Prompt Generator +Generates a prompt for Claude to help port the serializer to other runtimes: +```bash +cd tests +npx tsx generate-claude-prompt.ts +# Output: output/port-serializer-prompt.txt +``` ## Troubleshooting @@ -99,13 +140,3 @@ If outputs differ between runtimes: - Missing or extra fields in data structures - Different default values - Rounding differences - -## Future Expansion - -The current implementation prints basic skeleton data. Future expansions will include: -- Full bone and slot hierarchies -- All attachment types -- Animation timelines -- Constraint data -- Physics settings -- Complete runtime state after animation \ No newline at end of file diff --git a/tests/analyze-java-api.ts b/tests/analyze-java-api.ts new file mode 100755 index 000000000..34d406e23 --- /dev/null +++ b/tests/analyze-java-api.ts @@ -0,0 +1,665 @@ +#!/usr/bin/env tsx + +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { Symbol, LspOutput, ClassInfo, PropertyInfo, AnalysisResult } from './types'; + +function ensureOutputDir(): string { + const outputDir = path.join(process.cwd(), 'output'); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + return outputDir; +} + +function generateLspData(outputDir: string): string { + const outputFile = path.join(outputDir, 'spine-libgdx-symbols.json'); + const projectDir = '/Users/badlogic/workspaces/spine-runtimes/spine-libgdx'; + const srcDir = path.join(projectDir, 'spine-libgdx/src'); + + // Check if we need to regenerate + let needsRegeneration = true; + if (fs.existsSync(outputFile)) { + const outputStats = fs.statSync(outputFile); + const outputTime = outputStats.mtime.getTime(); + + // Find the newest source file + const newestSourceTime = execSync( + `find "${srcDir}" -name "*.java" -type f ! -name "SkeletonSerializer.java" -exec stat -f "%m" {} \\; | sort -nr | head -1`, + { encoding: 'utf8' } + ).trim(); + + if (newestSourceTime) { + const sourceTime = parseInt(newestSourceTime) * 1000; // Convert to milliseconds + needsRegeneration = sourceTime > outputTime; + } + } + + if (needsRegeneration) { + console.error('Generating LSP data for spine-libgdx...'); + try { + execSync(`npx lsp-cli "${projectDir}" java "${outputFile}"`, { + encoding: 'utf8', + stdio: ['ignore', 'ignore', 'pipe'] // Hide stdout but show stderr + }); + console.error('LSP data generated successfully'); + } catch (error: any) { + console.error('Error generating LSP data:', error.message); + throw error; + } + } else { + console.error('Using existing LSP data (up to date)'); + } + + return outputFile; +} + +function analyzeClasses(symbols: Symbol[]): Map { + const classMap = new Map(); + const srcPath = '/Users/badlogic/workspaces/spine-runtimes/spine-libgdx/spine-libgdx/src/'; + + function processSymbol(symbol: Symbol, parentName?: string) { + if (symbol.kind !== 'class' && symbol.kind !== 'enum' && symbol.kind !== 'interface') return; + + // Filter: only process symbols in spine-libgdx/src, excluding SkeletonSerializer + if (!symbol.file.startsWith(srcPath)) return; + if (symbol.file.endsWith('SkeletonSerializer.java')) return; + + const className = parentName ? `${parentName}.${symbol.name}` : symbol.name; + + const classInfo: ClassInfo = { + className: className, + superTypes: (symbol.supertypes || []).map(st => st.name.replace('$', '.')), + superTypeDetails: symbol.supertypes, + file: symbol.file, + getters: [], + fields: [], + isAbstract: false, + isInterface: symbol.kind === 'interface', + isEnum: symbol.kind === 'enum', + typeParameters: symbol.typeParameters || [] + }; + + // No need to parse superTypes from preview anymore - lsp-cli handles this properly now + + // Check if abstract class + if (symbol.preview && symbol.preview.includes('abstract ')) { + classInfo.isAbstract = true; + } + + // Log type parameter information if available + if (symbol.typeParameters && symbol.typeParameters.length > 0) { + console.error(`Class ${className} has type parameters: ${symbol.typeParameters.join(', ')}`); + } + if (symbol.supertypes) { + for (const supertype of symbol.supertypes) { + if (supertype.typeArguments && supertype.typeArguments.length > 0) { + console.error(` extends ${supertype.name}<${supertype.typeArguments.join(', ')}>`); + } + } + } + + // Find all getter methods, public fields, inner classes, and enum values + if (symbol.children) { + for (const child of symbol.children) { + if (child.kind === 'class' || child.kind === 'enum' || child.kind === 'interface') { + // Process inner class + processSymbol(child, className); + } else if (child.kind === 'enumMember') { + // Collect enum values + if (!classInfo.enumValues) { + classInfo.enumValues = []; + } + classInfo.enumValues.push(child.name); + } else if (child.kind === 'field' && child.preview) { + // Check if it's a public field + if (child.preview.includes('public ')) { + // Extract field type from preview + // Examples: "public float offset;", "public final Array to = ..." + const fieldMatch = child.preview.match(/public\s+(final\s+)?(.+?)\s+(\w+)\s*[;=]/); + if (fieldMatch) { + const isFinal = !!fieldMatch[1]; + const fieldType = fieldMatch[2].trim(); + const fieldName = fieldMatch[3]; + classInfo.fields.push({ fieldName, fieldType, isFinal }); + } + } + } else if (child.kind === 'method' && + child.name.startsWith('get') && + child.name !== 'getClass()' && + child.name.endsWith('()')) { // Only parameterless getters + + const methodName = child.name.slice(0, -2); // Remove () + + if (methodName.length > 3 && methodName[3] === methodName[3].toUpperCase()) { + // Extract return type from preview + let returnType = 'unknown'; + if (child.preview) { + const returnMatch = child.preview.match(/(?:public|protected|private)?\s*(.+?)\s+\w+\s*\(\s*\)/); + if (returnMatch) { + returnType = returnMatch[1].trim(); + } + } + + classInfo.getters.push({ methodName, returnType }); + } + } + } + } + + classMap.set(className, classInfo); + } + + for (const symbol of symbols) { + processSymbol(symbol); + } + + return classMap; +} + +function findAccessibleTypes( + classMap: Map, + startingTypes: string[] +): Set { + const accessible = new Set(); + const toVisit = [...startingTypes]; + const visited = new Set(); + + // Helper to find all concrete subclasses of a type + function findConcreteSubclasses(typeName: string, addToQueue: boolean = true): string[] { + const concreteClasses: string[] = []; + + if (!classMap.has(typeName)) return concreteClasses; + + const classInfo = classMap.get(typeName)!; + + // Add the type itself if it's concrete + if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) { + concreteClasses.push(typeName); + } + + // Find all subclasses recursively + for (const [className, info] of classMap) { + // Check if this class extends our target (handle both qualified and unqualified names) + const extendsTarget = info.superTypes.some(st => + st === typeName || + st === typeName.split('.').pop() || + (typeName.includes('.') && className.startsWith(typeName.split('.')[0] + '.') && st === typeName.split('.').pop()) + ); + + if (extendsTarget) { + // Recursively find concrete subclasses + const subclasses = findConcreteSubclasses(className, false); + concreteClasses.push(...subclasses); + + if (addToQueue && !visited.has(className)) { + toVisit.push(className); + } + } + } + + return concreteClasses; + } + + while (toVisit.length > 0) { + const typeName = toVisit.pop()!; + + if (visited.has(typeName)) continue; + visited.add(typeName); + + if (!classMap.has(typeName)) { + console.error(`Type ${typeName} not found in classMap`); + continue; + } + + const classInfo = classMap.get(typeName)!; + + // Add the type itself if it's concrete + if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) { + accessible.add(typeName); + console.error(`Added concrete type: ${typeName}`); + } + + // Find all concrete subclasses of this type + const concreteClasses = findConcreteSubclasses(typeName); + concreteClasses.forEach(c => accessible.add(c)); + + // Add types from getter return types and field types + const allTypes = [ + ...classInfo.getters.map(g => g.returnType), + ...classInfo.fields.map(f => f.fieldType) + ]; + + for (const type of allTypes) { + const returnType = type + .replace(/@Null\s+/g, '') // Remove @Null annotations + .replace(/\s+/g, ' '); // Normalize whitespace + + // Extract types from Array, IntArray, FloatArray, etc. + const arrayMatch = returnType.match(/Array<(.+?)>/); + if (arrayMatch) { + const innerType = arrayMatch[1].trim(); + // Handle inner classes like AnimationState.TrackEntry + if (innerType.includes('.')) { + if (classMap.has(innerType) && !visited.has(innerType)) { + toVisit.push(innerType); + } + } else { + // Try both plain type and as inner class of current type + if (classMap.has(innerType) && !visited.has(innerType)) { + toVisit.push(innerType); + } + // Also try as inner class of the declaring type + const parts = typeName.split('.'); + for (let i = parts.length; i >= 1; i--) { + const parentPath = parts.slice(0, i).join('.'); + const innerClassPath = `${parentPath}.${innerType}`; + if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) { + toVisit.push(innerClassPath); + break; + } + } + } + } + + // Extract all capitalized type names + const typeMatches = returnType.match(/\b([A-Z]\w+(?:\.[A-Z]\w+)*)\b/g); + if (typeMatches) { + for (const match of typeMatches) { + if (match === 'BoneLocal') { + console.error(`Found BoneLocal in return type of ${typeName}`); + } + if (classMap.has(match) && !visited.has(match)) { + toVisit.push(match); + if (match === 'BoneLocal') { + console.error(`Added BoneLocal to toVisit`); + } + } + // For non-qualified names, also try as inner class + if (!match.includes('.')) { + // Try as inner class of current type and its parents + const parts = typeName.split('.'); + for (let i = parts.length; i >= 1; i--) { + const parentPath = parts.slice(0, i).join('.'); + const innerClassPath = `${parentPath}.${match}`; + if (classMap.has(innerClassPath) && !visited.has(innerClassPath)) { + toVisit.push(innerClassPath); + break; + } + } + } + } + } + } + } + + console.error(`Found ${accessible.size} accessible types`); + return accessible; +} + +function getAllProperties(classMap: Map, className: string, symbolsFile: string): PropertyInfo[] { + const allProperties: PropertyInfo[] = []; + const visited = new Set(); + const classInfo = classMap.get(className); + if (!classInfo) return []; + + // Build type parameter mapping based on supertype details + const typeParamMap = new Map(); + + // Helper to build parameter mappings for a specific supertype + function buildTypeParamMapping(currentClass: string, targetSupertype: string): Map { + const mapping = new Map(); + const currentInfo = classMap.get(currentClass); + if (!currentInfo || !currentInfo.superTypeDetails) return mapping; + + // Find the matching supertype + for (const supertype of currentInfo.superTypeDetails) { + if (supertype.name === targetSupertype && supertype.typeArguments) { + // Get the supertype's class info to know its type parameters + const supertypeInfo = classMap.get(targetSupertype); + if (supertypeInfo && supertypeInfo.typeParameters) { + // Map type parameters to arguments + for (let i = 0; i < Math.min(supertypeInfo.typeParameters.length, supertype.typeArguments.length); i++) { + mapping.set(supertypeInfo.typeParameters[i], supertype.typeArguments[i]); + } + } + break; + } + } + return mapping; + } + + function resolveType(type: string, typeMap: Map = new Map()): string { + // Resolve generic type parameters + if (typeMap.has(type)) { + return typeMap.get(type)!; + } + // TODO: Handle complex types like Array, Map, etc. + return type; + } + + // Collect properties in inheritance order (most specific first) + function collectProperties(currentClass: string, inheritanceLevel: number = 0, currentTypeMap: Map = new Map()) { + if (visited.has(currentClass)) return; + visited.add(currentClass); + + const classInfo = classMap.get(currentClass); + if (!classInfo) return; + + // Add this class's getters with resolved types + for (const getter of classInfo.getters) { + allProperties.push({ + name: getter.methodName + '()', + type: resolveType(getter.returnType, currentTypeMap), + isGetter: true, + inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass + }); + } + + // Add this class's public fields + for (const field of classInfo.fields) { + allProperties.push({ + name: field.fieldName, + type: resolveType(field.fieldType, currentTypeMap), + isGetter: false, + inheritedFrom: inheritanceLevel === 0 ? undefined : currentClass + }); + } + + // Recursively collect from supertypes + for (const superType of classInfo.superTypes) { + // Build type parameter mapping for this supertype + const supertypeMapping = buildTypeParamMapping(currentClass, superType); + + // Compose mappings - resolve type arguments through current mapping + const composedMapping = new Map(); + for (const [param, arg] of supertypeMapping) { + composedMapping.set(param, resolveType(arg, currentTypeMap)); + } + + // Try to find the supertype - it might be unqualified + let superClassInfo = classMap.get(superType); + + // If not found and it's unqualified, try to find it as an inner class + if (!superClassInfo && !superType.includes('.')) { + // Try as inner class of the same parent + if (currentClass.includes('.')) { + const parentPrefix = currentClass.substring(0, currentClass.lastIndexOf('.')); + const qualifiedSuper = `${parentPrefix}.${superType}`; + superClassInfo = classMap.get(qualifiedSuper); + if (superClassInfo) { + collectProperties(qualifiedSuper, inheritanceLevel + 1, composedMapping); + continue; + } + } + + // Try as top-level class + for (const [name, info] of classMap) { + if (name === superType || name.endsWith(`.${superType}`)) { + collectProperties(name, inheritanceLevel + 1, composedMapping); + break; + } + } + } else if (superClassInfo) { + collectProperties(superType, inheritanceLevel + 1, composedMapping); + } + } + } + + collectProperties(className); + + // Remove duplicates (overridden methods/shadowed fields), keeping the most specific one + const seen = new Map(); + for (const prop of allProperties) { + const key = prop.isGetter ? prop.name : `field:${prop.name}`; + if (!seen.has(key)) { + seen.set(key, prop); + } + } + + return Array.from(seen.values()); +} + +// Helper to find all implementations of a type (both concrete and abstract) +function findAllImplementations(classMap: Map, typeName: string, concreteOnly: boolean = false): string[] { + const implementations: string[] = []; + const visited = new Set(); + + function findImplementations(currentType: string) { + if (visited.has(currentType)) return; + visited.add(currentType); + + // Get the short name for comparison + const currentShortName = currentType.split('.').pop()!; + const currentPrefix = currentType.includes('.') ? currentType.split('.')[0] : ''; + + for (const [className, classInfo] of classMap) { + // Check if this class extends/implements the current type + let extendsType = false; + + // For inner classes, we need to check if they're in the same outer class + if (currentPrefix && className.startsWith(currentPrefix + '.')) { + // Both are inner classes of the same outer class + extendsType = classInfo.superTypes.some(st => + st === currentShortName || st === currentType + ); + } else { + // Standard inheritance check + extendsType = classInfo.superTypes.some(st => + st === currentType || st === currentShortName + ); + } + + if (extendsType) { + if (!classInfo.isAbstract && !classInfo.isInterface && !classInfo.isEnum) { + // This is a concrete implementation + implementations.push(className); + } else { + // This is abstract/interface + if (!concreteOnly) { + // Include abstract types when getting all implementations + implementations.push(className); + } + // Always recurse to find further implementations + findImplementations(className); + } + } + } + } + + findImplementations(typeName); + return [...new Set(implementations)].sort(); // Remove duplicates and sort +} + +function analyzeForSerialization(classMap: Map, symbolsFile: string): AnalysisResult { + const startingTypes = ['SkeletonData', 'Skeleton', 'AnimationState']; + const accessibleTypes = findAccessibleTypes(classMap, startingTypes); + + // First pass: populate implementations for all abstract types + for (const [className, classInfo] of classMap) { + if (classInfo.isAbstract || classInfo.isInterface) { + // Get only concrete implementations + const concreteImplementations = findAllImplementations(classMap, className, true); + classInfo.concreteImplementations = concreteImplementations; + + // Get all implementations (including intermediate abstract types) + const allImplementations = findAllImplementations(classMap, className, false); + classInfo.allImplementations = allImplementations; + } + } + + // Collect abstract types and their implementations + const abstractTypes = new Map(); + const allTypesToGenerate = new Set(accessibleTypes); + + // Find all abstract types referenced by accessible types + for (const typeName of accessibleTypes) { + const classInfo = classMap.get(typeName); + if (!classInfo) continue; + + // Check return types and field types for abstract classes + const allTypes = [ + ...classInfo.getters.map(g => g.returnType), + ...classInfo.fields.map(f => f.fieldType) + ]; + + for (const type of allTypes) { + const returnType = type + .replace(/@Null\s+/g, '') + .replace(/\s+/g, ' '); + + // Extract types from Array + let checkTypes: string[] = []; + const arrayMatch = returnType.match(/Array<(.+?)>/); + if (arrayMatch) { + checkTypes.push(arrayMatch[1].trim()); + } else if (returnType.match(/^[A-Z]\w+$/)) { + checkTypes.push(returnType); + } + + // Also check for type names that might be inner classes + const typeMatches = returnType.match(/\b([A-Z]\w+)\b/g); + if (typeMatches) { + for (const match of typeMatches) { + // Try as inner class of current type + const parts = typeName.split('.'); + for (let i = parts.length; i >= 1; i--) { + const parentPath = parts.slice(0, i).join('.'); + const innerClassPath = `${parentPath}.${match}`; + if (classMap.has(innerClassPath)) { + checkTypes.push(innerClassPath); + break; + } + } + } + } + + for (const checkType of checkTypes) { + if (checkType && classMap.has(checkType)) { + const typeInfo = classMap.get(checkType)!; + if (typeInfo.isAbstract || typeInfo.isInterface) { + // Use the already populated concreteImplementations + const implementations = typeInfo.concreteImplementations || []; + abstractTypes.set(checkType, implementations); + + // Add all concrete implementations to types to generate + implementations.forEach(impl => allTypesToGenerate.add(impl)); + } + } + } + } + } + + // Collect all properties for each type (including inherited ones) + const typeProperties = new Map(); + for (const typeName of allTypesToGenerate) { + const props = getAllProperties(classMap, typeName, symbolsFile); + typeProperties.set(typeName, props); + } + + // Also collect properties for abstract types (so we know what properties their implementations should have) + for (const abstractType of abstractTypes.keys()) { + if (!typeProperties.has(abstractType)) { + const props = getAllProperties(classMap, abstractType, symbolsFile); + typeProperties.set(abstractType, props); + } + } + + // Second pass: find additional concrete types referenced in properties + const additionalTypes = new Set(); + for (const [typeName, props] of typeProperties) { + for (const prop of props) { + const propType = prop.type.replace(/@Null\s+/g, '').trim(); + + // Check if it's a simple type name + const typeMatch = propType.match(/^([A-Z]\w+)$/); + if (typeMatch) { + const type = typeMatch[1]; + if (classMap.has(type)) { + const typeInfo = classMap.get(type)!; + if (!typeInfo.isAbstract && !typeInfo.isInterface && !typeInfo.isEnum) { + if (!allTypesToGenerate.has(type)) { + additionalTypes.add(type); + console.error(`Found additional type ${type} from property ${prop.name} of ${typeName}`); + } + } + } + } + } + } + + // Add the additional types + additionalTypes.forEach(type => allTypesToGenerate.add(type)); + + // Get properties for the additional types too + for (const typeName of additionalTypes) { + const props = getAllProperties(classMap, typeName, symbolsFile); + typeProperties.set(typeName, props); + } + + return { + classMap, + accessibleTypes, + abstractTypes, + allTypesToGenerate, + typeProperties + }; +} + +async function main() { + try { + // Ensure output directory exists + const outputDir = ensureOutputDir(); + + // Generate LSP data + const jsonFile = generateLspData(outputDir); + + // Read and parse the JSON + const jsonContent = fs.readFileSync(jsonFile, 'utf8'); + const lspData: LspOutput = JSON.parse(jsonContent); + + console.error(`Analyzing ${lspData.symbols.length} symbols...`); + + // Analyze all classes + const classMap = analyzeClasses(lspData.symbols); + console.error(`Found ${classMap.size} classes`); + + // Perform serialization analysis + const analysisResult = analyzeForSerialization(classMap, jsonFile); + console.error(`Found ${analysisResult.accessibleTypes.size} accessible types`); + console.error(`Found ${analysisResult.allTypesToGenerate.size} types to generate`); + + // Save analysis result to file + const analysisFile = path.join(outputDir, 'analysis-result.json'); + + // Convert Maps to arrays and handle nested Maps in ClassInfo + const classMapArray: [string, any][] = []; + for (const [name, info] of analysisResult.classMap) { + const serializedInfo = { + ...info, + typeParameters: info.typeParameters ? Array.from(info.typeParameters.entries()) : undefined + }; + classMapArray.push([name, serializedInfo]); + } + + const resultToSave = { + ...analysisResult, + // Convert Maps and Sets to arrays for JSON serialization + classMap: classMapArray, + accessibleTypes: Array.from(analysisResult.accessibleTypes), + abstractTypes: Array.from(analysisResult.abstractTypes.entries()), + allTypesToGenerate: Array.from(analysisResult.allTypesToGenerate), + typeProperties: Array.from(analysisResult.typeProperties.entries()) + }; + + fs.writeFileSync(analysisFile, JSON.stringify(resultToSave, null, 2)); + console.log(`Analysis result written to: ${analysisFile}`); + + } catch (error: any) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/tests/generate-claude-prompt.ts b/tests/generate-claude-prompt.ts new file mode 100644 index 000000000..59d3d5b1e --- /dev/null +++ b/tests/generate-claude-prompt.ts @@ -0,0 +1,165 @@ +#!/usr/bin/env tsx + +import * as fs from 'fs'; +import * as path from 'path'; +import type { ClassInfo, PropertyInfo } from './types'; + +interface SerializedAnalysisResult { + classMap: [string, ClassInfo][]; + accessibleTypes: string[]; + abstractTypes: [string, string[]][]; + allTypesToGenerate: string[]; + typeProperties: [string, PropertyInfo[]][]; +} + +function generateClaudePrompt(analysisData: SerializedAnalysisResult): string { + const output: string[] = []; + + // Convert arrays back to Maps + const classMap = new Map(analysisData.classMap); + const abstractTypes = new Map(analysisData.abstractTypes); + const typeProperties = new Map(analysisData.typeProperties); + + output.push('# Spine Java Serialization Methods to Generate'); + output.push(''); + output.push('You need to generate writeXXX() methods for the SkeletonSerializer class.'); + output.push('Each method should serialize all properties accessible via getter methods.'); + output.push(''); + output.push('## Task: Port the Java SkeletonSerializer to Other Runtimes'); + output.push(''); + output.push('A complete Java SkeletonSerializer has been generated at:'); + output.push('`spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java`'); + output.push(''); + output.push('Port this serializer to:'); + output.push('- **C++**: Create `SkeletonSerializer.h/cpp` in `spine-cpp/src/spine/`'); + output.push('- **C**: Create `spine_skeleton_serializer.h/c` in `spine-c/src/spine/`'); + output.push('- **TypeScript**: Create `SkeletonSerializer.ts` in `spine-ts/spine-core/src/`'); + output.push(''); + output.push('## Important Porting Notes'); + output.push(''); + output.push('1. **Language Differences**:'); + output.push(' - Java uses getter methods (`getName()`), TypeScript may use properties (`.name`)'); + output.push(' - C uses function calls (`spBoneData_getName()`)'); + output.push(' - Adapt to each language\'s idioms'); + output.push('2. **Type Checking**:'); + output.push(' - Java uses `instanceof`'); + output.push(' - C++ uses custom RTTI (`object->getRTTI().instanceOf()`)'); + output.push(' - C uses type fields or function pointers'); + output.push(' - TypeScript uses `instanceof`'); + output.push('3. **Collections**:'); + output.push(' - Java uses `Array`, `IntArray`, `FloatArray`'); + output.push(' - C++ uses `Vector`'); + output.push(' - C uses arrays with size fields'); + output.push(' - TypeScript uses arrays `T[]`'); + output.push(''); + output.push('## Types Reference'); + output.push(''); + + // First emit abstract types that need instanceof delegation + for (const [className, classInfo] of classMap) { + if ((classInfo.isAbstract || classInfo.isInterface) && classInfo.concreteImplementations && classInfo.concreteImplementations.length > 0) { + output.push(`### ${className} (${classInfo.isInterface ? 'interface' : 'abstract'})`); + if (classInfo.superTypes.length > 0) { + output.push(`Extends: ${classInfo.superTypes.join(', ')}`); + } + output.push(''); + output.push('This is an abstract class. Generate a write' + className.split('.').pop() + '() method that checks instanceof for these concrete implementations:'); + for (const impl of classInfo.concreteImplementations.sort()) { + output.push(`- ${impl}`); + } + output.push(''); + output.push('Example implementation:'); + output.push('```java'); + output.push(`private void write${className.split('.').pop()}(JsonWriter json, ${className.split('.').pop()} obj) throws IOException {`); + const first = classInfo.concreteImplementations[0]; + if (first) { + const shortName = first.split('.').pop()!; + output.push(` if (obj instanceof ${shortName}) {`); + output.push(` write${shortName}(json, (${shortName}) obj);`); + output.push(' } // ... etc for all concrete types'); + output.push(' else {'); + output.push(` throw new RuntimeException("Unknown ${className.split('.').pop()} type: " + obj.getClass().getName());`); + output.push(' }'); + } + output.push('}'); + output.push('```'); + output.push(''); + } + } + + // Then emit concrete types + const sortedTypes = Array.from(analysisData.allTypesToGenerate).sort(); + + for (const typeName of sortedTypes) { + const classInfo = classMap.get(typeName)!; + + output.push(`### ${typeName}`); + if (classInfo && classInfo.superTypes.length > 0) { + output.push(`Extends: ${classInfo.superTypes.join(', ')}`); + } + output.push(''); + + const properties = typeProperties.get(typeName) || []; + const getters = properties.filter(p => p.isGetter); + const fields = properties.filter(p => !p.isGetter); + + if (getters.length > 0 || fields.length > 0) { + if (fields.length > 0) { + output.push('Public fields:'); + output.push('```java'); + for (const field of fields) { + output.push(`${field.name} // ${field.type}`); + } + output.push('```'); + output.push(''); + } + + if (getters.length > 0) { + output.push('Getters to serialize:'); + output.push('```java'); + for (const getter of getters) { + output.push(`${getter.name} // returns ${getter.type}`); + } + output.push('```'); + } + } else { + output.push('*No properties found*'); + } + output.push(''); + } + + return output.join('\n'); +} + +async function main() { + try { + // Read analysis result + const analysisFile = path.join(process.cwd(), 'output', 'analysis-result.json'); + if (!fs.existsSync(analysisFile)) { + console.error('Analysis result not found. Run analyze-java-api.ts first.'); + process.exit(1); + } + + const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8')); + + // Generate Claude prompt + const prompt = generateClaudePrompt(analysisData); + + // Write the prompt file + const outputFile = path.join(process.cwd(), 'output', 'port-serializer-to-other-runtimes.md'); + fs.writeFileSync(outputFile, prompt); + + console.log(`Claude prompt written to: ${outputFile}`); + + } catch (error: any) { + console.error('Error:', error.message); + process.exit(1); + } +} + +// Allow running as a script or importing the function +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { generateClaudePrompt }; \ No newline at end of file diff --git a/tests/generate-java-serializer.ts b/tests/generate-java-serializer.ts new file mode 100644 index 000000000..69aa74fe4 --- /dev/null +++ b/tests/generate-java-serializer.ts @@ -0,0 +1,549 @@ +#!/usr/bin/env tsx + +import * as fs from 'fs'; +import * as path from 'path'; +import type { ClassInfo, PropertyInfo } from './types'; + +interface SerializedAnalysisResult { + classMap: [string, ClassInfo][]; + accessibleTypes: string[]; + abstractTypes: [string, string[]][]; + allTypesToGenerate: string[]; + typeProperties: [string, PropertyInfo[]][]; +} + +function generateWriteValue(output: string[], expression: string, type: string, indent: string, abstractTypes: Map, classMap: Map) { + // Handle null annotations + const isNullable = type.includes('@Null'); + type = type.replace(/@Null\s+/g, '').trim(); + + // Primitive types + if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(type)) { + output.push(`${indent}json.writeValue(${expression});`); + return; + } + + // Check if it's an enum - need to handle both short and full names + let classInfo = classMap.get(type); + if (!classInfo && !type.includes('.')) { + // Try to find by short name + for (const [fullName, info] of classMap) { + if (fullName.split('.').pop() === type) { + classInfo = info; + break; + } + } + } + + if (classInfo?.isEnum) { + if (isNullable) { + output.push(`${indent}if (${expression} == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent}} else {`); + output.push(`${indent} json.writeValue(${expression}.name());`); + output.push(`${indent}}`); + } else { + output.push(`${indent}json.writeValue(${expression}.name());`); + } + return; + } + + // Arrays + if (type.startsWith('Array<')) { + const innerType = type.match(/Array<(.+?)>/)![1].trim(); + output.push(`${indent}if (${expression} == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent}} else {`); + output.push(`${indent} json.writeArrayStart();`); + output.push(`${indent} for (${innerType} item : ${expression}) {`); + generateWriteValue(output, 'item', innerType, indent + ' ', abstractTypes, classMap); + output.push(`${indent} }`); + output.push(`${indent} json.writeArrayEnd();`); + output.push(`${indent}}`); + return; + } + + if (type === 'IntArray' || type === 'FloatArray') { + output.push(`${indent}if (${expression} == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent}} else {`); + output.push(`${indent} json.writeArrayStart();`); + output.push(`${indent} for (int i = 0; i < ${expression}.size; i++) {`); + output.push(`${indent} json.writeValue(${expression}.get(i));`); + output.push(`${indent} }`); + output.push(`${indent} json.writeArrayEnd();`); + output.push(`${indent}}`); + return; + } + + if (type.endsWith('[]')) { + const elemType = type.slice(0, -2); + output.push(`${indent}if (${expression} == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent}} else {`); + output.push(`${indent} json.writeArrayStart();`); + // Handle nested arrays (like float[][]) + if (elemType.endsWith('[]')) { + const nestedType = elemType.slice(0, -2); + output.push(`${indent} for (${elemType} nestedArray : ${expression}) {`); + output.push(`${indent} if (nestedArray == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent} } else {`); + output.push(`${indent} json.writeArrayStart();`); + output.push(`${indent} for (${nestedType} elem : nestedArray) {`); + output.push(`${indent} json.writeValue(elem);`); + output.push(`${indent} }`); + output.push(`${indent} json.writeArrayEnd();`); + output.push(`${indent} }`); + output.push(`${indent} }`); + } else { + output.push(`${indent} for (${elemType} item : ${expression}) {`); + generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap); + output.push(`${indent} }`); + } + output.push(`${indent} json.writeArrayEnd();`); + output.push(`${indent}}`); + return; + } + + // Special cases for libGDX types + if (type === 'Color') { + output.push(`${indent}writeColor(json, ${expression});`); + return; + } + + if (type === 'TextureRegion') { + output.push(`${indent}writeTextureRegion(json, ${expression});`); + return; + } + + // Handle objects + const shortType = type.split('.').pop()!; + + // Check if this type exists in classMap (for abstract types that might not be in generated methods) + let foundInClassMap = classMap.has(type); + if (!foundInClassMap && !type.includes('.')) { + // Try to find by short name + for (const [fullName, info] of classMap) { + if (fullName.split('.').pop() === type) { + foundInClassMap = true; + // If it's abstract/interface, we need the instanceof chain + if (info.isAbstract || info.isInterface) { + type = fullName; // Use full name for abstract types + } + break; + } + } + } + + if (isNullable) { + output.push(`${indent}if (${expression} == null) {`); + output.push(`${indent} json.writeNull();`); + output.push(`${indent}} else {`); + output.push(`${indent} write${shortType}(json, ${expression});`); + output.push(`${indent}}`); + } else { + output.push(`${indent}write${shortType}(json, ${expression});`); + } +} + +function generateJavaSerializer(analysisData: SerializedAnalysisResult): string { + const javaOutput: string[] = []; + + // Convert arrays back to Maps + const classMap = new Map(analysisData.classMap); + const abstractTypes = new Map(analysisData.abstractTypes); + const typeProperties = new Map(analysisData.typeProperties); + + // Collect all types that need write methods + const typesNeedingMethods = new Set(); + + // Add all types from allTypesToGenerate + for (const type of analysisData.allTypesToGenerate) { + typesNeedingMethods.add(type); + } + + // Add all abstract types that are referenced + for (const [abstractType] of abstractTypes) { + typesNeedingMethods.add(abstractType); + } + + // Add types referenced in properties + for (const [typeName, props] of typeProperties) { + if (!typesNeedingMethods.has(typeName)) continue; + + for (const prop of props) { + let propType = prop.type.replace(/@Null\s+/g, '').trim(); + + // Extract type from Array + const arrayMatch = propType.match(/Array<(.+?)>/); + if (arrayMatch) { + propType = arrayMatch[1].trim(); + } + + // Extract type from Type[] + if (propType.endsWith('[]')) { + propType = propType.slice(0, -2); + } + + // Skip primitives and special types + if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long', + 'Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) { + continue; + } + + // Add the type if it's a class + if (propType.match(/^[A-Z]/)) { + typesNeedingMethods.add(propType); + + // Also check if it's an abstract type in classMap + let found = false; + for (const [fullName, info] of classMap) { + if (fullName === propType || fullName.split('.').pop() === propType) { + if (info.isAbstract || info.isInterface) { + typesNeedingMethods.add(fullName); + } + found = true; + break; + } + } + } + } + } + + // Generate Java file header + javaOutput.push('package com.esotericsoftware.spine.utils;'); + javaOutput.push(''); + javaOutput.push('import com.esotericsoftware.spine.*;'); + javaOutput.push('import com.esotericsoftware.spine.Animation.*;'); + javaOutput.push('import com.esotericsoftware.spine.AnimationState.*;'); + javaOutput.push('import com.esotericsoftware.spine.BoneData.Inherit;'); + javaOutput.push('import com.esotericsoftware.spine.Skin.SkinEntry;'); + javaOutput.push('import com.esotericsoftware.spine.PathConstraintData.*;'); + javaOutput.push('import com.esotericsoftware.spine.TransformConstraintData.*;'); + javaOutput.push('import com.esotericsoftware.spine.attachments.*;'); + javaOutput.push('import com.badlogic.gdx.graphics.Color;'); + javaOutput.push('import com.badlogic.gdx.graphics.g2d.TextureRegion;'); + javaOutput.push('import com.badlogic.gdx.utils.Array;'); + javaOutput.push('import com.badlogic.gdx.utils.IntArray;'); + javaOutput.push('import com.badlogic.gdx.utils.FloatArray;'); + javaOutput.push(''); + javaOutput.push('import java.io.Writer;'); + javaOutput.push('import java.io.IOException;'); + javaOutput.push('import java.util.Locale;'); + javaOutput.push('import java.util.Set;'); + javaOutput.push('import java.util.HashSet;'); + javaOutput.push(''); + javaOutput.push('public class SkeletonSerializer {'); + javaOutput.push(' private final Set visitedObjects = new HashSet<>();'); + javaOutput.push(''); + + // Generate main entry methods + javaOutput.push(' public void serializeSkeletonData(SkeletonData data, Writer writer) throws IOException {'); + javaOutput.push(' visitedObjects.clear();'); + javaOutput.push(' JsonWriter json = new JsonWriter(writer);'); + javaOutput.push(' writeSkeletonData(json, data);'); + javaOutput.push(' json.close();'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' public void serializeSkeleton(Skeleton skeleton, Writer writer) throws IOException {'); + javaOutput.push(' visitedObjects.clear();'); + javaOutput.push(' JsonWriter json = new JsonWriter(writer);'); + javaOutput.push(' writeSkeleton(json, skeleton);'); + javaOutput.push(' json.close();'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' public void serializeAnimationState(AnimationState state, Writer writer) throws IOException {'); + javaOutput.push(' visitedObjects.clear();'); + javaOutput.push(' JsonWriter json = new JsonWriter(writer);'); + javaOutput.push(' writeAnimationState(json, state);'); + javaOutput.push(' json.close();'); + javaOutput.push(' }'); + javaOutput.push(''); + + // Generate write methods for all types + const generatedMethods = new Set(); + + for (const typeName of Array.from(typesNeedingMethods).sort()) { + const classInfo = classMap.get(typeName); + if (!classInfo) continue; + + const shortName = typeName.split('.').pop()!; + + // Skip if already generated (handle name collisions) + if (generatedMethods.has(shortName)) continue; + generatedMethods.add(shortName); + + // Use full class name for inner classes + const className = typeName.includes('.') ? typeName : shortName; + + javaOutput.push(` private void write${shortName}(JsonWriter json, ${className} obj) throws IOException {`); + + if (classInfo.isEnum) { + // Handle enums + javaOutput.push(' json.writeValue(obj.name());'); + } else if (classInfo.isAbstract || classInfo.isInterface) { + // Handle abstract types with instanceof chain + const implementations = classInfo.concreteImplementations || []; + if (implementations.length === 0) { + javaOutput.push(' json.writeNull(); // No concrete implementations'); + } else { + let first = true; + for (const impl of implementations) { + const implShortName = impl.split('.').pop()!; + const implClassName = impl.includes('.') ? impl : implShortName; + + if (first) { + javaOutput.push(` if (obj instanceof ${implClassName}) {`); + first = false; + } else { + javaOutput.push(` } else if (obj instanceof ${implClassName}) {`); + } + javaOutput.push(` write${implShortName}(json, (${implClassName}) obj);`); + } + javaOutput.push(' } else {'); + javaOutput.push(` throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`); + javaOutput.push(' }'); + } + } else { + // Handle concrete types + const properties = typeProperties.get(typeName) || []; + + // Add cycle detection + javaOutput.push(' if (visitedObjects.contains(obj)) {'); + javaOutput.push(' json.writeValue("");'); + javaOutput.push(' return;'); + javaOutput.push(' }'); + javaOutput.push(' visitedObjects.add(obj);'); + javaOutput.push(''); + + javaOutput.push(' json.writeObjectStart();'); + + // Write type field + javaOutput.push(' json.writeName("type");'); + javaOutput.push(` json.writeValue("${shortName}");`); + + // Write properties + for (const prop of properties) { + const propName = prop.isGetter ? + prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() + + prop.name.replace('get', '').replace('()', '').slice(1) : + prop.name; + + javaOutput.push(''); + javaOutput.push(` json.writeName("${propName}");`); + const accessor = prop.isGetter ? `obj.${prop.name}` : `obj.${prop.name}`; + generateWriteValue(javaOutput, accessor, prop.type, ' ', abstractTypes, classMap); + } + + javaOutput.push(''); + javaOutput.push(' json.writeObjectEnd();'); + } + + javaOutput.push(' }'); + javaOutput.push(''); + } + + // Add helper methods + javaOutput.push(' private void writeColor(JsonWriter json, Color color) throws IOException {'); + javaOutput.push(' if (color == null) {'); + javaOutput.push(' json.writeNull();'); + javaOutput.push(' } else {'); + javaOutput.push(' json.writeObjectStart();'); + javaOutput.push(' json.writeName("r");'); + javaOutput.push(' json.writeValue(color.r);'); + javaOutput.push(' json.writeName("g");'); + javaOutput.push(' json.writeValue(color.g);'); + javaOutput.push(' json.writeName("b");'); + javaOutput.push(' json.writeValue(color.b);'); + javaOutput.push(' json.writeName("a");'); + javaOutput.push(' json.writeValue(color.a);'); + javaOutput.push(' json.writeObjectEnd();'); + javaOutput.push(' }'); + javaOutput.push(' }'); + javaOutput.push(''); + + javaOutput.push(' private void writeTextureRegion(JsonWriter json, TextureRegion region) throws IOException {'); + javaOutput.push(' if (region == null) {'); + javaOutput.push(' json.writeNull();'); + javaOutput.push(' } else {'); + javaOutput.push(' json.writeObjectStart();'); + javaOutput.push(' json.writeName("u");'); + javaOutput.push(' json.writeValue(region.getU());'); + javaOutput.push(' json.writeName("v");'); + javaOutput.push(' json.writeValue(region.getV());'); + javaOutput.push(' json.writeName("u2");'); + javaOutput.push(' json.writeValue(region.getU2());'); + javaOutput.push(' json.writeName("v2");'); + javaOutput.push(' json.writeValue(region.getV2());'); + javaOutput.push(' json.writeName("width");'); + javaOutput.push(' json.writeValue(region.getRegionWidth());'); + javaOutput.push(' json.writeName("height");'); + javaOutput.push(' json.writeValue(region.getRegionHeight());'); + javaOutput.push(' json.writeObjectEnd();'); + javaOutput.push(' }'); + javaOutput.push(' }'); + javaOutput.push(''); + + // Add JsonWriter inner class + javaOutput.push(' private static class JsonWriter {'); + javaOutput.push(' private final Writer writer;'); + javaOutput.push(' private int depth = 0;'); + javaOutput.push(' private boolean needsComma = false;'); + javaOutput.push(''); + javaOutput.push(' JsonWriter(Writer writer) {'); + javaOutput.push(' this.writer = writer;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeObjectStart() throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write("{");'); + javaOutput.push(' depth++;'); + javaOutput.push(' needsComma = false;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeObjectEnd() throws IOException {'); + javaOutput.push(' depth--;'); + javaOutput.push(' if (needsComma) {'); + javaOutput.push(' writer.write("\\n");'); + javaOutput.push(' writeIndent();'); + javaOutput.push(' }'); + javaOutput.push(' writer.write("}");'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeArrayStart() throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write("[");'); + javaOutput.push(' depth++;'); + javaOutput.push(' needsComma = false;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeArrayEnd() throws IOException {'); + javaOutput.push(' depth--;'); + javaOutput.push(' if (needsComma) {'); + javaOutput.push(' writer.write("\\n");'); + javaOutput.push(' writeIndent();'); + javaOutput.push(' }'); + javaOutput.push(' writer.write("]");'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeName(String name) throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write("\\n");'); + javaOutput.push(' writeIndent();'); + javaOutput.push(' writer.write("\\"" + name + "\\": ");'); + javaOutput.push(' needsComma = false;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeValue(String value) throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' if (value == null) {'); + javaOutput.push(' writer.write("null");'); + javaOutput.push(' } else {'); + javaOutput.push(' writer.write("\\"" + escapeString(value) + "\\"");'); + javaOutput.push(' }'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeValue(float value) throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write(String.format(Locale.US, "%.6f", value).replaceAll("0+$", "").replaceAll("\\\\.$", ""));'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeValue(int value) throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write(String.valueOf(value));'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeValue(boolean value) throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write(String.valueOf(value));'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void writeNull() throws IOException {'); + javaOutput.push(' writeCommaIfNeeded();'); + javaOutput.push(' writer.write("null");'); + javaOutput.push(' needsComma = true;'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' void close() throws IOException {'); + javaOutput.push(' writer.write("\\n");'); + javaOutput.push(' writer.flush();'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' private void writeCommaIfNeeded() throws IOException {'); + javaOutput.push(' if (needsComma) {'); + javaOutput.push(' writer.write(",");'); + javaOutput.push(' }'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' private void writeIndent() throws IOException {'); + javaOutput.push(' for (int i = 0; i < depth; i++) {'); + javaOutput.push(' writer.write(" ");'); + javaOutput.push(' }'); + javaOutput.push(' }'); + javaOutput.push(''); + javaOutput.push(' private String escapeString(String str) {'); + javaOutput.push(' return str.replace("\\\\", "\\\\\\\\")'); + javaOutput.push(' .replace("\\"", "\\\\\\"")'); + javaOutput.push(' .replace("\\b", "\\\\b")'); + javaOutput.push(' .replace("\\f", "\\\\f")'); + javaOutput.push(' .replace("\\n", "\\\\n")'); + javaOutput.push(' .replace("\\r", "\\\\r")'); + javaOutput.push(' .replace("\\t", "\\\\t");'); + javaOutput.push(' }'); + javaOutput.push(' }'); + javaOutput.push('}'); + + return javaOutput.join('\n'); +} + +async function main() { + try { + // Read analysis result + const analysisFile = path.join(process.cwd(), 'output', 'analysis-result.json'); + if (!fs.existsSync(analysisFile)) { + console.error('Analysis result not found. Run analyze-java-api.ts first.'); + process.exit(1); + } + + const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8')); + + // Generate Java serializer + const javaCode = generateJavaSerializer(analysisData); + + // Write the Java file + const javaFile = path.join( + path.dirname(process.cwd()), + 'spine-libgdx', + 'spine-libgdx', + 'src', + 'com', + 'esotericsoftware', + 'spine', + 'utils', + 'SkeletonSerializer.java' + ); + + fs.mkdirSync(path.dirname(javaFile), { recursive: true }); + fs.writeFileSync(javaFile, javaCode); + + console.log(`Generated Java serializer: ${javaFile}`); + + } catch (error: any) { + console.error('Error:', error.message); + process.exit(1); + } +} + +// Allow running as a script or importing the function +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { generateJavaSerializer }; \ No newline at end of file diff --git a/tests/package-lock.json b/tests/package-lock.json index 289e80c7e..3e467242c 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "spine-tests", + "dependencies": { + "@mariozechner/lsp-cli": "^0.1.1" + }, "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.0.0" @@ -452,6 +455,22 @@ "node": ">=18" } }, + "node_modules/@mariozechner/lsp-cli": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mariozechner/lsp-cli/-/lsp-cli-0.1.1.tgz", + "integrity": "sha512-/5HF/PoYhQKFMhXLQiH1ZHTBJfs7rB8xcXa4YXSq6aTR5G6tWOrE6fwFbK7pwtkry6ZFeCTE2HI4BRWv5La9Qg==", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^11.0.0", + "node-stream-zip": "^1.15.0", + "tar": "^6.2.0", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageserver-protocol": "^3.17.0" + }, + "bin": { + "lsp-cli": "dist/index.js" + } + }, "node_modules/@types/node": { "version": "20.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.7.tgz", @@ -462,6 +481,36 @@ "undici-types": "~6.21.0" } }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/esbuild": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", @@ -504,6 +553,30 @@ "@esbuild/win32-x64": "0.25.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -532,6 +605,65 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -542,6 +674,23 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tsx": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", @@ -568,6 +717,46 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/tests/package.json b/tests/package.json index 440cf9f37..ed0e927c6 100644 --- a/tests/package.json +++ b/tests/package.json @@ -8,5 +8,8 @@ "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.0.0" + }, + "dependencies": { + "@mariozechner/lsp-cli": "^0.1.1" } -} \ No newline at end of file +} diff --git a/tests/types.ts b/tests/types.ts new file mode 100644 index 000000000..feafb8b9f --- /dev/null +++ b/tests/types.ts @@ -0,0 +1,68 @@ +// Shared types for the Spine serializer generator + +// Match lsp-cli's Supertype interface +export interface Supertype { + name: string; + typeArguments?: string[]; +} + +// Match lsp-cli's SymbolInfo interface (we call it Symbol for backward compatibility) +export interface Symbol { + name: string; + kind: string; + file: string; + preview: string; + documentation?: string; + typeParameters?: string[]; + supertypes?: Supertype[]; + children?: Symbol[]; + // We don't need range and definition for our use case +} + +export interface LspOutput { + language: string; + directory: string; + symbols: Symbol[]; +} + +export interface ClassInfo { + className: string; + superTypes: string[]; // Just the names for backward compatibility + superTypeDetails?: Supertype[]; // Full details with type arguments + getters: GetterInfo[]; + fields: FieldInfo[]; + file: string; + isAbstract: boolean; + isInterface: boolean; + isEnum: boolean; + typeParameters?: string[]; // The class's own type parameters + enumValues?: string[]; // For enums + concreteImplementations?: string[]; // For abstract classes/interfaces - only leaf concrete types + allImplementations?: string[]; // For abstract classes/interfaces - includes intermediate abstract types +} + +export interface GetterInfo { + methodName: string; + returnType: string; +} + +export interface FieldInfo { + fieldName: string; + fieldType: string; + isFinal: boolean; +} + +export interface PropertyInfo { + name: string; + type: string; + isGetter: boolean; + inheritedFrom?: string; // Which class this property was inherited from +} + +export interface AnalysisResult { + classMap: Map; + accessibleTypes: Set; + abstractTypes: Map; // abstract type -> concrete implementations + allTypesToGenerate: Set; // all types that need write methods + typeProperties: Map; // type -> all properties (including inherited) +} \ No newline at end of file