[haxe] Plan for serializer generator

This commit is contained in:
Mario Zechner 2025-07-26 09:19:10 +02:00
parent 3e67c82a22
commit 108f9bf355
3 changed files with 1014 additions and 12 deletions

View File

@ -20,6 +20,7 @@
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz",
"integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
@ -49,6 +50,7 @@
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
@ -65,6 +67,7 @@
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
@ -81,6 +84,7 @@
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
@ -97,6 +101,7 @@
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
@ -113,6 +118,7 @@
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
@ -129,6 +135,7 @@
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
@ -145,6 +152,7 @@
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
@ -161,6 +169,7 @@
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
@ -177,6 +186,7 @@
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
@ -193,6 +203,7 @@
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
@ -209,6 +220,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
@ -225,6 +237,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
@ -241,6 +254,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -257,6 +271,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -273,6 +288,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
@ -289,6 +305,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
@ -305,6 +322,7 @@
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -321,6 +339,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -337,6 +356,7 @@
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -353,6 +373,7 @@
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -369,6 +390,7 @@
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -385,6 +407,7 @@
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -401,6 +424,7 @@
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -417,6 +441,7 @@
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -433,6 +458,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -449,6 +475,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
@ -465,6 +492,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
@ -481,6 +509,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
@ -497,6 +526,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
@ -513,6 +543,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
@ -529,6 +560,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
@ -545,6 +577,7 @@
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -561,6 +594,7 @@
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -577,6 +611,7 @@
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -606,6 +641,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
"integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
@ -614,6 +650,7 @@
"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"
},
@ -625,6 +662,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
@ -633,6 +671,7 @@
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"engines": {
"node": ">=16"
}
@ -641,13 +680,15 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz",
"integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/editorconfig": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
"integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^2.19.0",
"lru-cache": "^4.1.5",
@ -662,7 +703,8 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.8",
@ -670,6 +712,7 @@
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@ -709,6 +752,7 @@
"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"
},
@ -720,6 +764,7 @@
"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"
},
@ -733,6 +778,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -746,6 +792,7 @@
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
@ -758,6 +805,7 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"license": "ISC",
"dependencies": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
@ -767,12 +815,14 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true
"dev": true,
"license": "ISC"
},
"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"
}
@ -781,6 +831,7 @@
"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"
@ -793,6 +844,7 @@
"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"
},
@ -804,6 +856,7 @@
"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"
},
@ -815,6 +868,7 @@
"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"
},
@ -827,13 +881,15 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
"dev": true
"dev": true,
"license": "ISC"
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
@ -843,6 +899,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
@ -851,12 +908,14 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
"dev": true
"dev": true,
"license": "ISC"
},
"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",
@ -874,6 +933,7 @@
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@ -893,6 +953,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
@ -907,6 +968,7 @@
"resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz",
"integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commandpost": "^1.0.0",
"editorconfig": "^0.15.0"
@ -925,12 +987,14 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
"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"
}
@ -939,6 +1003,7 @@
"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"
@ -948,6 +1013,7 @@
"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"
}
@ -955,12 +1021,14 @@
"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=="
"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=="
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
}
}
}

927
tests/plan-haxe.md Normal file
View File

@ -0,0 +1,927 @@
# Haxe Serializer Generator Implementation Plan
## Overview
This document outlines the complete implementation plan for adding Haxe support to the Spine runtime testing infrastructure. The goal is to generate a Haxe serializer that produces identical JSON output to the existing Java and C++ serializers, enabling cross-runtime compatibility testing.
## Current System Architecture
The existing system consists of three layers:
1. **SerializerIR Generation** (`tests/src/generate-serializer-ir.ts`)
- Analyzes Java API to create intermediate representation
- Outputs `tests/output/serializer-ir.json` with type and property metadata
2. **Language-Specific Generators**
- `tests/src/generate-java-serializer.ts` - Java implementation
- `tests/src/generate-cpp-serializer.ts` - C++ implementation
- **Missing**: `tests/src/generate-haxe-serializer.ts`
3. **HeadlessTest Applications**
- `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
- `spine-cpp/tests/HeadlessTest.cpp`
- **Missing**: `spine-haxe/tests/HeadlessTest.hx`
4. **Test Runner** (`tests/src/headless-test-runner.ts`)
- Orchestrates building and running tests
- Compares outputs for consistency
- Currently supports: Java, C++
- **Needs**: Haxe support
## SerializerIR Structure Reference
Based on `tests/src/generate-serializer-ir.ts:10-80`:
```typescript
interface SerializerIR {
publicMethods: PublicMethod[]; // Entry point methods
writeMethods: WriteMethod[]; // Type-specific serializers
enumMappings: { [enumName: string]: { [javaValue: string]: string } };
}
interface WriteMethod {
name: string; // writeSkeletonData, writeBone, etc.
paramType: string; // Full Java class name
properties: Property[]; // Fields to serialize
isAbstractType: boolean; // Needs instanceof chain
subtypeChecks?: SubtypeCheck[]; // For abstract types
}
type Property = Primitive | Object | Enum | Array | NestedArray;
```
## Implementation Plan
### 1. Generate Haxe Serializer (`tests/src/generate-haxe-serializer.ts`)
Create the generator following the pattern from existing generators:
```typescript
#!/usr/bin/env tsx
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import type { Property, SerializerIR } from './generate-serializer-ir';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function transformType(javaType: string): string {
// Java → Haxe type mappings
const primitiveMap: Record<string, string> = {
'String': 'String',
'int': 'Int',
'float': 'Float',
'boolean': 'Bool',
'short': 'Int',
'byte': 'Int',
'double': 'Float',
'long': 'Int'
};
// Remove package prefixes and map primitives
const simpleName = javaType.includes('.') ? javaType.split('.').pop()! : javaType;
if (primitiveMap[simpleName]) {
return primitiveMap[simpleName];
}
// Handle arrays: Java T[] → Haxe Array<T>
if (simpleName.endsWith('[]')) {
const baseType = simpleName.slice(0, -2);
return `Array<${transformType(baseType)}>`;
}
// Java Array<T> stays Array<T> in Haxe
if (simpleName.startsWith('Array<')) {
return simpleName;
}
// Object types: keep class name, remove package
return simpleName;
}
function mapJavaGetterToHaxeField(javaGetter: string, objName: string): string {
// Map Java getter methods to Haxe field access
// Based on analysis of existing Haxe classes in spine-haxe/spine-haxe/spine/
if (javaGetter.endsWith('()')) {
const methodName = javaGetter.slice(0, -2);
// Remove get/is prefix and convert to camelCase field
if (methodName.startsWith('get')) {
const fieldName = methodName.slice(3);
const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1);
return `${objName}.${haxeField}`;
}
if (methodName.startsWith('is')) {
const fieldName = methodName.slice(2);
const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1);
return `${objName}.${haxeField}`;
}
// Some methods might be direct field names
return `${objName}.${methodName}`;
}
// Direct field access (already in correct format)
return `${objName}.${javaGetter}`;
}
function generatePropertyCode(property: Property, indent: string, enumMappings: { [enumName: string]: { [javaValue: string]: string } }): string[] {
const lines: string[] = [];
const accessor = mapJavaGetterToHaxeField(property.getter, 'obj');
switch (property.kind) {
case "primitive":
lines.push(`${indent}json.writeValue(${accessor});`);
break;
case "object":
if (property.isNullable) {
lines.push(`${indent}if (${accessor} == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent}} else {`);
lines.push(`${indent} ${property.writeMethodCall}(${accessor});`);
lines.push(`${indent}}`);
} else {
lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
}
break;
case "enum": {
const enumName = property.enumName;
const enumMap = enumMappings[enumName];
if (property.isNullable) {
lines.push(`${indent}if (${accessor} == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent}} else {`);
}
if (enumMap && Object.keys(enumMap).length > 0) {
// Generate switch statement for enum mapping
lines.push(`${indent}${property.isNullable ? ' ' : ''}switch (${accessor}) {`);
for (const [javaValue, haxeValue] of Object.entries(enumMap)) {
lines.push(`${indent}${property.isNullable ? ' ' : ''} case ${haxeValue}: json.writeValue("${javaValue}");`);
}
lines.push(`${indent}${property.isNullable ? ' ' : ''} default: json.writeValue("unknown");`);
lines.push(`${indent}${property.isNullable ? ' ' : ''}}`);
} else {
// Fallback using Type.enumConstructor or similar
lines.push(`${indent}${property.isNullable ? ' ' : ''}json.writeValue(Type.enumConstructor(${accessor}));`);
}
if (property.isNullable) {
lines.push(`${indent}}`);
}
break;
}
case "array": {
if (property.isNullable) {
lines.push(`${indent}if (${accessor} == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent}} else {`);
lines.push(`${indent} json.writeArrayStart();`);
lines.push(`${indent} for (item in ${accessor}) {`);
} else {
lines.push(`${indent}json.writeArrayStart();`);
lines.push(`${indent}for (item in ${accessor}) {`);
}
const itemIndent = property.isNullable ? `${indent} ` : `${indent} `;
if (property.elementKind === "primitive") {
lines.push(`${itemIndent}json.writeValue(item);`);
} else {
lines.push(`${itemIndent}${property.writeMethodCall}(item);`);
}
if (property.isNullable) {
lines.push(`${indent} }`);
lines.push(`${indent} json.writeArrayEnd();`);
lines.push(`${indent}}`);
} else {
lines.push(`${indent}}`);
lines.push(`${indent}json.writeArrayEnd();`);
}
break;
}
case "nestedArray": {
if (property.isNullable) {
lines.push(`${indent}if (${accessor} == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent}} else {`);
}
const outerIndent = property.isNullable ? `${indent} ` : indent;
lines.push(`${outerIndent}json.writeArrayStart();`);
lines.push(`${outerIndent}for (nestedArray in ${accessor}) {`);
lines.push(`${outerIndent} if (nestedArray == null) {`);
lines.push(`${outerIndent} json.writeNull();`);
lines.push(`${outerIndent} } else {`);
lines.push(`${outerIndent} json.writeArrayStart();`);
lines.push(`${outerIndent} for (elem in nestedArray) {`);
lines.push(`${outerIndent} json.writeValue(elem);`);
lines.push(`${outerIndent} }`);
lines.push(`${outerIndent} json.writeArrayEnd();`);
lines.push(`${outerIndent} }`);
lines.push(`${outerIndent}}`);
lines.push(`${outerIndent}json.writeArrayEnd();`);
if (property.isNullable) {
lines.push(`${indent}}`);
}
break;
}
}
return lines;
}
function generateHaxeFromIR(ir: SerializerIR): string {
const haxeOutput: string[] = [];
// Generate Haxe file header
haxeOutput.push('package spine.utils;');
haxeOutput.push('');
haxeOutput.push('import haxe.ds.StringMap;');
haxeOutput.push('import spine.*;');
haxeOutput.push('import spine.animation.*;');
haxeOutput.push('import spine.attachments.*;');
haxeOutput.push('');
haxeOutput.push('class SkeletonSerializer {');
haxeOutput.push(' private var visitedObjects:StringMap<String> = new StringMap();');
haxeOutput.push(' private var nextId:Int = 1;');
haxeOutput.push(' private var json:JsonWriter;');
haxeOutput.push('');
haxeOutput.push(' public function new() {}');
haxeOutput.push('');
// Generate public methods
for (const method of ir.publicMethods) {
const haxeParamType = transformType(method.paramType);
haxeOutput.push(` public function ${method.name}(${method.paramName}:${haxeParamType}):String {`);
haxeOutput.push(' visitedObjects = new StringMap();');
haxeOutput.push(' nextId = 1;');
haxeOutput.push(' json = new JsonWriter();');
haxeOutput.push(` ${method.writeMethodCall}(${method.paramName});`);
haxeOutput.push(' return json.getString();');
haxeOutput.push(' }');
haxeOutput.push('');
}
// Generate write methods
for (const method of ir.writeMethods) {
const shortName = method.paramType.split('.').pop();
const haxeType = transformType(method.paramType);
haxeOutput.push(` private function ${method.name}(obj:${haxeType}):Void {`);
if (method.isAbstractType) {
// Handle abstract types with Std.isOfType chain (Haxe equivalent of instanceof)
if (method.subtypeChecks && method.subtypeChecks.length > 0) {
let first = true;
for (const subtype of method.subtypeChecks) {
const subtypeHaxeName = transformType(subtype.typeName);
if (first) {
haxeOutput.push(` if (Std.isOfType(obj, ${subtypeHaxeName})) {`);
first = false;
} else {
haxeOutput.push(` } else if (Std.isOfType(obj, ${subtypeHaxeName})) {`);
}
haxeOutput.push(` ${subtype.writeMethodCall}(cast(obj, ${subtypeHaxeName}));`);
}
haxeOutput.push(' } else {');
haxeOutput.push(` throw new spine.SpineException("Unknown ${shortName} type");`);
haxeOutput.push(' }');
} else {
haxeOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions');
}
} else {
// Handle concrete types - add cycle detection
haxeOutput.push(' if (visitedObjects.exists(obj)) {');
haxeOutput.push(' json.writeValue(visitedObjects.get(obj));');
haxeOutput.push(' return;');
haxeOutput.push(' }');
// Generate reference string
const nameGetter = method.properties.find(p =>
(p.kind === 'object' || p.kind === "primitive") &&
p.getter === 'getName()' &&
p.valueType === 'String'
);
if (nameGetter) {
const nameAccessor = mapJavaGetterToHaxeField('getName()', 'obj');
haxeOutput.push(` var refString = ${nameAccessor} != null ? "<${shortName}-" + ${nameAccessor} + ">" : "<${shortName}-" + (nextId++) + ">";`);
} else {
haxeOutput.push(` var refString = "<${shortName}-" + (nextId++) + ">";`);
}
haxeOutput.push(' visitedObjects.set(obj, refString);');
haxeOutput.push('');
haxeOutput.push(' json.writeObjectStart();');
// Write reference string and type
haxeOutput.push(' json.writeName("refString");');
haxeOutput.push(' json.writeValue(refString);');
haxeOutput.push(' json.writeName("type");');
haxeOutput.push(` json.writeValue("${shortName}");`);
// Write properties
for (const property of method.properties) {
haxeOutput.push('');
haxeOutput.push(` json.writeName("${property.name}");`);
const propertyLines = generatePropertyCode(property, ' ', ir.enumMappings);
haxeOutput.push(...propertyLines);
}
haxeOutput.push('');
haxeOutput.push(' json.writeObjectEnd();');
}
haxeOutput.push(' }');
haxeOutput.push('');
}
// Add helper methods for special types (following C++ pattern)
haxeOutput.push(' // Helper methods for special types');
haxeOutput.push(' private function writeColor(obj:spine.Color):Void {');
haxeOutput.push(' if (obj == null) {');
haxeOutput.push(' json.writeNull();');
haxeOutput.push(' } else {');
haxeOutput.push(' json.writeObjectStart();');
haxeOutput.push(' json.writeName("r");');
haxeOutput.push(' json.writeValue(obj.r);');
haxeOutput.push(' json.writeName("g");');
haxeOutput.push(' json.writeValue(obj.g);');
haxeOutput.push(' json.writeName("b");');
haxeOutput.push(' json.writeValue(obj.b);');
haxeOutput.push(' json.writeName("a");');
haxeOutput.push(' json.writeValue(obj.a);');
haxeOutput.push(' json.writeObjectEnd();');
haxeOutput.push(' }');
haxeOutput.push(' }');
haxeOutput.push('');
haxeOutput.push('}');
return haxeOutput.join('\n');
}
async function main(): Promise<void> {
try {
// Read the IR file
const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
if (!fs.existsSync(irFile)) {
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
process.exit(1);
}
const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
// Generate Haxe serializer from IR
const haxeCode = generateHaxeFromIR(ir);
// Write the Haxe file
const haxeFile = path.resolve(
__dirname,
'../../spine-haxe/spine-haxe/spine/utils/SkeletonSerializer.hx'
);
fs.mkdirSync(path.dirname(haxeFile), { recursive: true });
fs.writeFileSync(haxeFile, haxeCode);
console.log(`Generated Haxe serializer from IR: ${haxeFile}`);
console.log(`- ${ir.publicMethods.length} public methods`);
console.log(`- ${ir.writeMethods.length} write methods`);
console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
} catch (error: any) {
console.error('Error:', error.message);
console.error('Stack:', error.stack);
process.exit(1);
}
}
// Allow running as a script or importing the function
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { generateHaxeFromIR };
```
### 2. JsonWriter Helper Class (`spine-haxe/spine-haxe/spine/utils/JsonWriter.hx`)
Based on the pattern from `spine-cpp/tests/JsonWriter.h`, create a Haxe equivalent:
```haxe
package spine.utils;
enum JsonContext {
Object;
Array;
}
class JsonWriter {
private var buffer:StringBuf = new StringBuf();
private var needsComma:Bool = false;
private var contexts:Array<JsonContext> = [];
public function new() {
buffer = new StringBuf();
needsComma = false;
contexts = [];
}
public function writeObjectStart():Void {
writeCommaIfNeeded();
buffer.add("{");
contexts.push(Object);
needsComma = false;
}
public function writeObjectEnd():Void {
buffer.add("}");
contexts.pop();
needsComma = true;
}
public function writeArrayStart():Void {
writeCommaIfNeeded();
buffer.add("[");
contexts.push(Array);
needsComma = false;
}
public function writeArrayEnd():Void {
buffer.add("]");
contexts.pop();
needsComma = true;
}
public function writeName(name:String):Void {
writeCommaIfNeeded();
buffer.add('"${escapeString(name)}":');
needsComma = false;
}
public function writeValue(value:Dynamic):Void {
writeCommaIfNeeded();
if (value == null) {
buffer.add("null");
} else if (Std.isOfType(value, String)) {
buffer.add('"${escapeString(cast(value, String))}"');
} else if (Std.isOfType(value, Bool)) {
buffer.add(value ? "true" : "false");
} else if (Std.isOfType(value, Float) || Std.isOfType(value, Int)) {
// Ensure consistent float formatting (C locale style)
buffer.add(Std.string(value));
} else {
buffer.add(Std.string(value));
}
needsComma = true;
}
public function writeNull():Void {
writeCommaIfNeeded();
buffer.add("null");
needsComma = true;
}
public function getString():String {
return buffer.toString();
}
private function writeCommaIfNeeded():Void {
if (needsComma) {
buffer.add(",");
}
}
private function escapeString(str:String):String {
// Escape special characters for JSON
str = StringTools.replace(str, "\\", "\\\\");
str = StringTools.replace(str, '"', '\\"');
str = StringTools.replace(str, "\n", "\\n");
str = StringTools.replace(str, "\r", "\\r");
str = StringTools.replace(str, "\t", "\\t");
return str;
}
}
```
### 3. Haxe HeadlessTest Application (`spine-haxe/tests/HeadlessTest.hx`)
Following the pattern from existing HeadlessTest implementations:
```haxe
package;
import spine.*;
import spine.atlas.TextureAtlas;
import spine.atlas.TextureAtlasPage;
import spine.atlas.TextureLoader;
import spine.attachments.AtlasAttachmentLoader;
import spine.animation.*;
import spine.utils.SkeletonSerializer;
import sys.io.File;
import haxe.io.Bytes;
// Mock texture loader that doesn't require actual texture loading
class MockTextureLoader implements TextureLoader {
public function new() {}
public function load(page:TextureAtlasPage, path:String):Void {
// Set mock dimensions - no actual texture loading needed
page.width = 1024;
page.height = 1024;
page.texture = {}; // Empty object as mock texture
}
public function unload(texture:Dynamic):Void {
// Nothing to unload in headless mode
}
}
class HeadlessTest {
static function main():Void {
var args = Sys.args();
if (args.length < 2) {
Sys.stderr().writeString("Usage: HeadlessTest <skeleton-path> <atlas-path> [animation-name]\n");
Sys.exit(1);
}
var skeletonPath = args[0];
var atlasPath = args[1];
var animationName = args.length >= 3 ? args[2] : null;
try {
// Load atlas with mock texture loader
var textureLoader = new MockTextureLoader();
var atlasContent = File.getContent(atlasPath);
var atlas = new TextureAtlas(atlasContent, textureLoader);
// Load skeleton data
var skeletonData:SkeletonData;
var attachmentLoader = new AtlasAttachmentLoader(atlas);
if (StringTools.endsWith(skeletonPath, ".json")) {
var loader = new SkeletonJson(attachmentLoader);
var jsonContent = File.getContent(skeletonPath);
skeletonData = loader.readSkeletonData(jsonContent);
} else {
var loader = new SkeletonBinary(attachmentLoader);
var binaryContent = File.getBytes(skeletonPath);
skeletonData = loader.readSkeletonData(binaryContent);
}
// Create serializer
var serializer = new SkeletonSerializer();
// Print skeleton data
Sys.println("=== SKELETON DATA ===");
Sys.println(serializer.serializeSkeletonData(skeletonData));
// Create skeleton instance
var skeleton = new Skeleton(skeletonData);
// Handle animation if provided
var state:AnimationState = null;
if (animationName != null) {
var stateData = new AnimationStateData(skeletonData);
state = new AnimationState(stateData);
var animation = skeletonData.findAnimation(animationName);
if (animation == null) {
Sys.stderr().writeString('Animation not found: $animationName\n');
Sys.exit(1);
}
state.setAnimation(0, animation, true);
state.update(0.016);
state.apply(skeleton);
}
// Update world transforms (following the pattern from other HeadlessTests)
skeleton.updateWorldTransform(Physics.update);
// Print skeleton state
Sys.println("\n=== SKELETON STATE ===");
Sys.println(serializer.serializeSkeleton(skeleton));
// Print animation state if present
if (state != null) {
Sys.println("\n=== ANIMATION STATE ===");
Sys.println(serializer.serializeAnimationState(state));
}
} catch (e:Dynamic) {
Sys.stderr().writeString('Error: $e\n');
Sys.exit(1);
}
}
}
```
### 4. Build Script (`spine-haxe/build-headless-test.sh`)
```bash
#!/bin/bash
# Build Haxe HeadlessTest for cross-platform execution
# Following pattern from spine-cpp/build.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "Building Haxe HeadlessTest..."
# Clean previous build
rm -rf build/headless-test
# Create build directory
mkdir -p build
# Compile HeadlessTest to C++ for performance and consistency with other runtimes
haxe \
-cp spine-haxe \
-cp tests \
-main HeadlessTest \
-cpp build/headless-test \
-D HXCPP_QUIET
# Make executable
chmod +x build/headless-test/HeadlessTest
echo "Build complete: build/headless-test/HeadlessTest"
```
### 5. Test Runner Integration (`tests/src/headless-test-runner.ts`)
Add Haxe support to the existing test runner. Key changes needed:
```typescript
// Line 207: Update supported languages
if (!['cpp', 'haxe'].includes(language)) {
log_detail(`Invalid target language: ${language}. Must be cpp or haxe`);
process.exit(1);
}
// Add needsHaxeBuild function (similar to needsCppBuild at line 96)
function needsHaxeBuild(): boolean {
const haxeDir = join(SPINE_ROOT, 'spine-haxe');
const buildDir = join(haxeDir, 'build');
const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest');
try {
// Check if executable exists
if (!existsSync(headlessTest)) return true;
// Get executable modification time
const execTime = statSync(headlessTest).mtime.getTime();
// Check Haxe source files
const haxeSourceTime = getNewestFileTime(join(haxeDir, 'spine-haxe'), '*.hx');
const testSourceTime = getNewestFileTime(join(haxeDir, 'tests'), '*.hx');
const buildScriptTime = getNewestFileTime(haxeDir, 'build-headless-test.sh');
const newestSourceTime = Math.max(haxeSourceTime, testSourceTime, buildScriptTime);
return newestSourceTime > execTime;
} catch {
return true;
}
}
// Add executeHaxe function (similar to executeCpp at line 321)
function executeHaxe(args: TestArgs): string {
const haxeDir = join(SPINE_ROOT, 'spine-haxe');
const testsDir = join(haxeDir, 'tests');
if (!existsSync(testsDir)) {
log_detail(`Haxe tests directory not found: ${testsDir}`);
process.exit(1);
}
// Check if we need to build
if (needsHaxeBuild()) {
log_action('Building Haxe HeadlessTest');
try {
execSync('./build-headless-test.sh', {
cwd: haxeDir,
stdio: ['inherit', 'pipe', 'inherit']
});
log_ok();
} catch (error: any) {
log_fail();
log_detail(`Haxe build failed: ${error.message}`);
process.exit(1);
}
}
// Run the headless test
const testArgs = [args.skeletonPath, args.atlasPath];
if (args.animationName) {
testArgs.push(args.animationName);
}
const buildDir = join(haxeDir, 'build');
const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest');
if (!existsSync(headlessTest)) {
log_detail(`Haxe headless-test executable not found: ${headlessTest}`);
process.exit(1);
}
log_action('Running Haxe HeadlessTest');
try {
const output = execSync(`${headlessTest} ${testArgs.join(' ')}`, {
encoding: 'utf8',
maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large output
});
log_ok();
return output;
} catch (error: any) {
log_fail();
log_detail(`Haxe execution failed: ${error.message}`);
process.exit(1);
}
}
// Update runTestsForFiles function around line 525 to handle Haxe
if (language === 'cpp') {
targetOutput = executeCpp(testArgs);
} else if (language === 'haxe') {
targetOutput = executeHaxe(testArgs);
} else {
log_detail(`Unsupported target language: ${language}`);
process.exit(1);
}
```
### 6. Build Integration (`tests/generate-serializers.sh`)
Update the serializer generation script to include Haxe:
```bash
# Add after C++ generation
echo "Generating Haxe serializer..."
tsx tests/src/generate-haxe-serializer.ts
echo "Type checking Haxe serializer..."
cd spine-haxe && haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer
cd ..
```
### 7. File Structure Summary
```
spine-haxe/
├── spine-haxe/spine/utils/
│ ├── SkeletonSerializer.hx (generated)
│ └── JsonWriter.hx (helper class)
├── tests/
│ └── HeadlessTest.hx (console application)
├── build-headless-test.sh (build script)
└── build/headless-test/ (compiled executable)
└── HeadlessTest
tests/src/
├── generate-haxe-serializer.ts (new generator)
└── headless-test-runner.ts (updated with Haxe support)
```
## Type Checking and Validation
### Compilation Validation
Add type checking to the generator to ensure generated code compiles:
```typescript
import { execSync } from 'child_process';
async function validateGeneratedHaxeCode(haxeCode: string, outputPath: string): Promise<void> {
// Write code to file
fs.writeFileSync(outputPath, haxeCode);
try {
// Attempt compilation without output (type check only)
execSync('haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer', {
cwd: path.resolve(__dirname, '../../spine-haxe'),
stdio: 'pipe'
});
console.log('✓ Generated Haxe serializer compiles successfully');
} catch (error: any) {
fs.unlinkSync(outputPath);
throw new Error(`Generated Haxe serializer failed to compile:\n${error.message}`);
}
}
// Call in main() after generating code
await validateGeneratedHaxeCode(haxeCode, haxeFile);
```
## Key Implementation Notes
### Java → Haxe Property Mapping
Based on analysis of `spine-haxe/spine-haxe/spine/` classes:
- `obj.getName()``obj.name`
- `obj.getBones()``obj.bones`
- `obj.isActive()``obj.active`
- `obj.getColor()``obj.color`
### Enum Handling
Haxe enums are different from Java enums. Use `Type.enumConstructor()` to get string representation:
```haxe
// For enum serialization
json.writeValue(Type.enumConstructor(obj.blendMode));
```
### Array Handling
Haxe uses `Array<T>` syntax similar to Java, but iteration is different:
```haxe
// Haxe iteration
for (item in obj.bones) {
writeBone(item);
}
```
### Null Safety
Haxe has explicit null checking:
```haxe
if (obj.skin == null) {
json.writeNull();
} else {
writeSkin(obj.skin);
}
```
## Testing and Verification
### Cross-Runtime Consistency
The test runner will automatically:
1. Build all three runtimes (Java, C++, Haxe)
2. Run identical test cases on same skeleton files
3. Compare JSON outputs for exact matches
4. Report any differences
### Manual Testing
```bash
# Generate all serializers
./tests/generate-serializers.sh
# Test specific skeleton with all runtimes
tsx tests/src/headless-test-runner.ts cpp -s spineboy idle
tsx tests/src/headless-test-runner.ts haxe -s spineboy idle
# Compare outputs
diff tests/output/skeleton-data-cpp-json.json tests/output/skeleton-data-haxe-json.json
```
## Implementation Checklist
- [ ] Create `tests/src/generate-haxe-serializer.ts`
- [ ] Create `spine-haxe/spine-haxe/spine/utils/JsonWriter.hx`
- [ ] Create `spine-haxe/tests/HeadlessTest.hx`
- [ ] Create `spine-haxe/build-headless-test.sh`
- [ ] Update `tests/src/headless-test-runner.ts` with Haxe support
- [ ] Update `tests/generate-serializers.sh`
- [ ] Test with existing skeleton examples
- [ ] Verify JSON output matches Java/C++ exactly
- [ ] Add to CI pipeline
## Expected Benefits
1. **Cross-Runtime Testing**: Verify Haxe runtime behavior matches Java/C++
2. **Debugging Support**: Unified JSON format for inspection across all runtimes
3. **API Consistency**: Ensure Haxe API changes don't break compatibility
4. **Quality Assurance**: Automated verification of serialization correctness
5. **Development Velocity**: Fast detection of runtime-specific issues
This implementation follows the established patterns while adapting to Haxe's specific language features and build system.

View File

@ -1,8 +1,15 @@
- Port C++ SkeletonRenderer and RenderCommands to all runtimes
- Will be used to snapshottesting via HeadlessTest, see also tests/
- Can go into main package in all core runtimes, except for spine-libgdx, where it must go next to SkeletonSerializer in spine-libgdx-tests
- Generate language bindings in spine-c/codegen
- Use CClassOrStruct, CEnum that get generated from spine-cpp-types.json and generate
- Swift
- Dart
- Fix Dart NativeArray wrt to resize/add/remove. Current impl is wonky. Either make it read-only or support full mutabiliy (prefer latter)
- Generate bindings for Swift from spine-c generate() like dart-writer.ts
- Generate Godot wrappers from C++ types and/or spine-c generate() (unlike dart-writer.ts)?
- headless-test improvements
- should take cli args for ad-hoc testing
- if none are given, should execute a set of (regression) tests and output individual test snapshots one after the other as jsonl
- All headless tests must have the same test suite
- test runner must know how to deal with this mode
- Add serializer generator for Haxe (see tests/plan-haxe.md for a full plan)
- Add serializer generator for C#
- Add serializer generator for TypeScript
- 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