From 9ee4d6c40e51aaa8e4724a1fdee919a1ef367f3a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 11 Jul 2025 11:03:04 +0200 Subject: [PATCH] [libgdx] Add DebugPrinter program, add runDebugPrinter task to build.gradle --- spine-libgdx/build.gradle | 11 + .../esotericsoftware/spine/DebugPrinter.java | 283 ++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java diff --git a/spine-libgdx/build.gradle b/spine-libgdx/build.gradle index b74c943d2..4c4b490ae 100644 --- a/spine-libgdx/build.gradle +++ b/spine-libgdx/build.gradle @@ -138,11 +138,22 @@ configure(subprojects - project("spine-libgdx")) { implementation "com.badlogicgames.gdx:gdx:$libgdxVersion" implementation "com.badlogicgames.gdx:gdx-platform:$libgdxVersion:natives-desktop" implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$libgdxVersion" + implementation "com.badlogicgames.gdx:gdx-backend-headless:$libgdxVersion" implementation "com.badlogicgames.gdx:gdx-box2d:$libgdxVersion" implementation "com.badlogicgames.gdx:gdx-box2d-platform:$libgdxVersion:natives-desktop" } } +project("spine-libgdx-tests") { + task runDebugPrinter(type: JavaExec) { + main = 'com.esotericsoftware.spine.DebugPrinter' + classpath = sourceSets.main.runtimeClasspath + if (project.hasProperty('args')) { + args project.getProperty('args').split(' ') + } + } +} + tasks.withType(JavaCompile).configureEach { println "Building with sourceCompatibility = ${sourceCompatibility}, targetCompatibility = ${targetCompatibility}" } diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java new file mode 100644 index 000000000..abde2e401 --- /dev/null +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java @@ -0,0 +1,283 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +package com.esotericsoftware.spine; + +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.backends.headless.HeadlessApplication; +import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration; +import com.badlogic.gdx.files.FileHandle; +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; + +public class DebugPrinter implements ApplicationListener { + private String skeletonPath; + private String atlasPath; + private String animationName; + + public DebugPrinter (String skeletonPath, String atlasPath, String animationName) { + this.skeletonPath = skeletonPath; + this.atlasPath = atlasPath; + this.animationName = animationName; + } + + static class Printer { + private int indentLevel = 0; + private static final String INDENT = " "; + + private void print (String text) { + for (int i = 0; i < indentLevel; i++) { + System.out.print(INDENT); + } + System.out.println(text); + } + + private void printValue (String name, Object value) { + if (value == null) { + print(name + ": null"); + } else if (value instanceof String) { + print(name + ": \"" + value + "\""); + } else if (value instanceof Float) { + // Format floats to 6 decimal places to match other runtimes + print(name + ": " + String.format("%.6f", value)); + } else { + print(name + ": " + value); + } + } + + private void indent () { + indentLevel++; + } + + private void unindent () { + indentLevel--; + } + + public void printSkeletonData (SkeletonData data) { + print("SkeletonData {"); + indent(); + + // Basic properties + printValue("name", data.getName()); + printValue("version", data.getVersion()); + printValue("hash", data.getHash()); + printValue("x", data.getX()); + printValue("y", data.getY()); + printValue("width", data.getWidth()); + printValue("height", data.getHeight()); + printValue("referenceScale", data.getReferenceScale()); + printValue("fps", data.getFps()); + printValue("imagesPath", data.getImagesPath()); + printValue("audioPath", data.getAudioPath()); + + // TODO: Add bones, slots, skins, animations, etc. in future expansion + + unindent(); + print("}"); + } + + public void printSkeleton (Skeleton skeleton) { + print("Skeleton {"); + indent(); + + // Basic properties + printValue("x", skeleton.getX()); + printValue("y", skeleton.getY()); + printValue("scaleX", skeleton.getScaleX()); + printValue("scaleY", skeleton.getScaleY()); + printValue("time", skeleton.getTime()); + + // TODO: Add runtime state (bones, slots, etc.) in future expansion + + unindent(); + print("}"); + } + } + + // Mock texture that doesn't require OpenGL - similar to AndroidTexture + static class MockTexture extends Texture { + private int width, height; + + public MockTexture (int width, int height) { + super(); + this.width = width; + this.height = height; + } + + @Override + public int getWidth () { + return width; + } + + @Override + public int getHeight () { + return height; + } + + @Override + public void bind () { + // Do nothing + } + + @Override + public void bind (int unit) { + // Do nothing + } + + @Override + public void dispose () { + // Do nothing + } + } + + // Custom TextureAtlas that doesn't load actual textures - similar to AndroidTextureAtlas + static class HeadlessTextureAtlas extends TextureAtlas { + public HeadlessTextureAtlas (FileHandle packFile) { + // Load atlas data without creating real textures + TextureAtlasData data = new TextureAtlasData(packFile, packFile.parent(), false); + + // Create mock textures for each page + for (TextureAtlasData.Page page : data.getPages()) { + // Create a mock texture - we use 1024x1024 as a default size + page.texture = new MockTexture(1024, 1024); + } + + // Create regions from the data + for (TextureAtlasData.Region region : data.getRegions()) { + AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, + region.rotate ? region.height : region.width, region.rotate ? region.width : region.height); + atlasRegion.index = region.index; + atlasRegion.name = region.name; + atlasRegion.offsetX = region.offsetX; + atlasRegion.offsetY = region.offsetY; + atlasRegion.originalHeight = region.originalHeight; + atlasRegion.originalWidth = region.originalWidth; + atlasRegion.rotate = region.rotate; + atlasRegion.degrees = region.degrees; + atlasRegion.names = region.names; + atlasRegion.values = region.values; + if (region.flip) atlasRegion.flip(false, true); + getRegions().add(atlasRegion); + } + } + } + + @Override + public void create () { + try { + // Load atlas without textures + FileHandle atlasFile = Gdx.files.absolute(atlasPath); + TextureAtlas atlas = new HeadlessTextureAtlas(atlasFile); + + // Load skeleton data + FileHandle skeletonFile = Gdx.files.absolute(skeletonPath); + SkeletonData skeletonData; + + if (skeletonPath.endsWith(".json")) { + SkeletonJson json = new SkeletonJson(atlas); + skeletonData = json.readSkeletonData(skeletonFile); + } else { + SkeletonBinary binary = new SkeletonBinary(atlas); + skeletonData = binary.readSkeletonData(skeletonFile); + } + + // Print skeleton data + System.out.println("=== SKELETON DATA ==="); + Printer printer = new Printer(); + printer.printSkeletonData(skeletonData); + + // Create skeleton instance + Skeleton skeleton = new Skeleton(skeletonData); + + // Create animation state + AnimationStateData stateData = new AnimationStateData(skeletonData); + AnimationState state = new AnimationState(stateData); + + // Find and set animation + Animation animation = skeletonData.findAnimation(animationName); + if (animation == null) { + System.err.println("Animation not found: " + animationName); + System.exit(1); + } + + state.setAnimation(0, animation, true); + + // Update and apply + state.update(0.016f); + state.apply(skeleton); + skeleton.update(0.016f); + skeleton.updateWorldTransform(Physics.update); + + // Print skeleton state + System.out.println("\n=== SKELETON STATE ==="); + printer.printSkeleton(skeleton); + + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + + // Exit after processing + Gdx.app.exit(); + } + + @Override + public void resize (int width, int height) { + } + + @Override + public void render () { + } + + @Override + public void pause () { + } + + @Override + public void resume () { + } + + @Override + public void dispose () { + } + + public static void main (String[] args) { + if (args.length < 3) { + System.err.println("Usage: DebugPrinter "); + System.exit(1); + } + + HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); + config.updatesPerSecond = 60; + new HeadlessApplication(new DebugPrinter(args[0], args[1], args[2]), config); + } +}