导出数据转spine

This commit is contained in:
tianmo
2026-09-07 21:29:15 +08:00
parent d258d4f435
commit 3a7c0b4092
65 changed files with 6000 additions and 0 deletions
+350
View File
@@ -0,0 +1,350 @@
#include "atlas/AtlasParser.hpp"
#include "domain/SpineVersion.hpp"
#include "editor/SpineEditorLocator.hpp"
#include "editor/SpineOutputParser.hpp"
#include "editor/SpineVersionAvailability.hpp"
#include "platform/ProcessRunner.hpp"
#include "preview/RuntimePlugin.hpp"
#include "scan/SpineDataVersionDetector.hpp"
#include <chrono>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace {
int failures = 0;
void expect(bool condition, const std::string& message) {
if (!condition) {
std::cerr << "FAILED: " << message << '\n';
++failures;
}
}
void testVersions() {
const auto supported = dts::SpineVersion::parse("3.8.99");
expect(supported.has_value(), "3.8.99 parses");
expect(supported && supported->line() == "3.8", "version line is 3.8");
expect(supported && supported->isSupported(), "3.8.99 is supported");
const auto early = dts::SpineVersion::parse("3.8.19");
expect(early && !early->isSupported(), "3.8.19 is outside formal support");
const auto current = dts::SpineVersion::parse("4.3.6");
expect(current && current->isSupported(), "4.3 is supported");
const auto beta = dts::SpineVersion::parse("4.3.75-beta");
expect(beta && beta->toString() == "4.3.75", "version suffix is accepted");
const auto old = dts::SpineVersion::parse("3.7.94");
expect(old && !old->isSupported(), "3.7 is unsupported");
expect(!dts::SpineVersion::parse("four.two"), "invalid version is rejected");
}
void testLauncherOutput() {
const std::string output =
"Spine Launcher 4.3.06 (macOS Apple Silicon)\n"
"Launching: Spine 3.8.99 Professional\n"
"Starting: Spine 3.8.99\n";
dts::ProcessResult process{true, false, 0, output, {}};
const auto result = dts::SpineOutputParser{}.parse("/Applications/Spine", process);
expect(result.status == dts::EditorProbeStatus::Available, "probe is available");
expect(result.launcherVersion && result.launcherVersion->toString() == "4.3.6",
"launcher version is parsed separately");
expect(result.editorVersion && result.editorVersion->toString() == "3.8.99",
"editor version is parsed separately");
expect(result.edition == "Professional", "edition is parsed");
}
void testTimedOutOutput() {
dts::ProcessResult process;
process.started = true;
process.timedOut = true;
process.exitCode = 143;
process.output = "Spine Launcher 4.3.06\nLaunching: Spine 4.2.83 Professional\n";
const auto result = dts::SpineOutputParser{}.parse("Spine", process);
expect(result.status == dts::EditorProbeStatus::ObservedButTimedOut,
"recognized timeout is distinguished from missing Spine");
}
void testMacBundleNormalization() {
#if defined(__APPLE__)
const auto path = dts::SpineEditorLocator{}.normalize("/Applications/Spine.app");
expect(path.has_value(), "macOS .app is normalized");
expect(path && path->filename() == "Spine", "normalized executable is Spine");
#endif
}
void testProcessTimeout() {
#if !defined(_WIN32)
auto runner = dts::createDefaultProcessRunner();
dts::ProcessRequest request;
request.program = "/bin/sleep";
request.arguments = {"5"};
request.timeout = std::chrono::milliseconds(50);
const auto result = runner->run(request);
expect(result.started, "timeout test process starts");
expect(result.timedOut, "long-running process is timed out");
#endif
}
void writeVarint(std::ofstream& output, std::size_t value) {
while (true) {
auto byte = static_cast<unsigned char>(value & 0x7f);
value >>= 7;
if (value != 0) {
byte |= 0x80;
}
output.put(static_cast<char>(byte));
if (value == 0) {
return;
}
}
}
void writeBinaryString(std::ofstream& output, const std::string& value) {
writeVarint(output, value.size() + 1);
output.write(value.data(), static_cast<std::streamsize>(value.size()));
}
void testDataVersionDetection() {
const auto directory = std::filesystem::temp_directory_path() / "dts-core-tests";
std::filesystem::create_directories(directory);
const auto jsonPath = directory / "skeleton.json";
{
std::ofstream output(jsonPath);
output << R"({"skeleton":{"spine":"4.2.17"},"bones":[{"name":"root"}]})";
}
const auto json = dts::SpineDataVersionDetector{}.inspect(jsonPath);
expect(json.status == dts::VersionDetectionStatus::Detected, "Spine JSON is detected");
expect(json.version && json.version->toString() == "4.2.17", "JSON version is parsed");
const auto binaryPath = directory / "skeleton.skel";
{
std::ofstream output(binaryPath, std::ios::binary);
writeBinaryString(output, "fixture-hash");
writeBinaryString(output, "3.8.99");
}
const auto binary = dts::SpineDataVersionDetector{}.inspect(binaryPath);
expect(binary.status == dts::VersionDetectionStatus::Detected, "SKEL is detected");
expect(binary.version && binary.version->toString() == "3.8.99", "SKEL version is parsed");
const auto genericPath = directory / "generic.json";
{
std::ofstream output(genericPath);
output << R"({"spine":"4.2.0","items":[]})";
}
const auto generic = dts::SpineDataVersionDetector{}.inspect(genericPath);
expect(generic.status == dts::VersionDetectionStatus::InvalidData,
"ordinary JSON is not accepted as Spine data");
std::filesystem::remove_all(directory);
}
void testAtlasParsing() {
const std::string atlas =
"hero.webp\n"
"size: 1024, 512\n"
"filter: Linear, Linear\n"
"scale: 0.5\n"
"pma: true\n"
"body/arm\n"
"bounds: 10, 20, 30, 40\n"
"offsets: 1, 2, 32, 44\n"
"rotate: 90\n"
"\n"
"hero-2.jpg\n"
"size: 256, 256\n"
"face\n"
"xy: 4, 5\n"
"size: 20, 30\n"
"orig: 22, 34\n";
const auto result = dts::AtlasParser{}.parseText(atlas);
expect(result.document.has_value(), "multi-page Atlas parses");
expect(result.document && result.document->pages.size() == 2, "two pages are found");
const auto& first = result.document->pages.front();
expect(first.imagePath == "hero.webp", "WebP page path is retained");
expect(first.premultipliedAlpha == true, "PMA is parsed");
expect(first.scale && std::abs(*first.scale - 0.5) < 0.000001,
"Atlas page scale is parsed");
expect(first.regions.size() == 1 && first.regions.front().rotated,
"rotated region is parsed");
const auto& second = result.document->pages.back();
expect(second.regions.size() == 1 && second.regions.front().size.size() == 2,
"legacy region size is parsed");
}
void testInstalledVersionDiscovery() {
const auto directory = std::filesystem::temp_directory_path() / "dts-version-cache-tests";
std::filesystem::create_directories(directory);
for (const auto* name : {"4.3.18", "4.3.23", "4.2.43", "not-a-version"}) {
std::ofstream output(directory / name, std::ios::binary);
output.put('x');
}
dts::SpineVersionAvailability availability;
const auto selected = availability.newestForLine("4.3", {directory});
expect(selected && selected->toString() == "4.3.23",
"newest locally cached patch is selected for a version line");
expect(selected && availability.isCached(*selected, {directory}),
"selected exact editor version is confirmed in local cache");
expect(!availability.newestForLine("4.1", {directory}),
"missing cached version line is not reported as available");
std::filesystem::remove_all(directory);
}
void expectRuntimePlugin(const std::string& path, const std::string& versionLine) {
dts::RuntimePlugin plugin;
std::string error;
expect(plugin.load(path, error), versionLine + " Runtime plugin loads: " + error);
const auto metadata = plugin.metadata(error);
expect(metadata.has_value(), "Runtime metadata is available: " + error);
expect(metadata && metadata->apiVersion == DTS_RUNTIME_API_VERSION,
"Runtime API version matches");
expect(metadata && metadata->spineVersionLine == versionLine,
"Runtime line is " + versionLine);
}
void testRuntimePlugins() {
#if defined(DTS_RUNTIME_38_PATH)
expectRuntimePlugin(DTS_RUNTIME_38_PATH, "3.8");
expectRuntimePlugin(DTS_RUNTIME_40_PATH, "4.0");
expectRuntimePlugin(DTS_RUNTIME_41_PATH, "4.1");
expectRuntimePlugin(DTS_RUNTIME_42_PATH, "4.2");
expectRuntimePlugin(DTS_RUNTIME_43_PATH, "4.3");
#endif
}
void testRuntime43OfficialAssets() {
#if defined(DTS_TEST_SPINEBOY_JSON)
const auto jsonVersion = dts::SpineDataVersionDetector{}.inspect(DTS_TEST_SPINEBOY_JSON);
expect(jsonVersion.status == dts::VersionDetectionStatus::Detected,
"official 4.3 beta JSON version is detected");
const auto binaryVersion = dts::SpineDataVersionDetector{}.inspect(DTS_TEST_SPINEBOY_SKEL);
expect(binaryVersion.status == dts::VersionDetectionStatus::Detected,
"official 4.3 binary header version is detected");
dts::RuntimePlugin plugin;
std::string error;
expect(plugin.load(DTS_RUNTIME_43_PATH, error), "4.3 Runtime loads for official assets");
const auto json = plugin.loadSkeleton(
DTS_TEST_SPINEBOY_JSON, DTS_TEST_SPINEBOY_ATLAS, 1.0F, error);
expect(json.has_value(), "official Spineboy JSON loads: " + error);
expect(json && json->boneCount > 0, "Spineboy JSON contains bones");
expect(json && !json->animations.empty(), "Spineboy JSON contains animations");
if (json && !json->animations.empty()) {
const auto idle = std::find_if(json->animations.begin(), json->animations.end(),
[](const auto& animation) { return animation.name == "idle"; });
const auto& animated = idle == json->animations.end()
? json->animations.front() : *idle;
expect(plugin.setAnimation(animated.name, true, error),
"Spineboy animation can be selected: " + error);
const auto frame = plugin.updatePreview(1.0F / 60.0F, error);
expect(frame.has_value(), "Spineboy preview frame is sampled: " + error);
expect(frame && !frame->empty(), "Spineboy preview contains draw commands");
if (frame && !frame->empty()) {
expect(!frame->front().vertices.empty(), "draw command contains vertices");
expect(!frame->front().indices.empty(), "draw command contains triangles");
expect(std::filesystem::exists(frame->front().texturePath),
"draw command texture exists");
}
const auto laterFrame = plugin.updatePreview(0.25F, error);
bool moved = false;
if (frame && laterFrame && frame->size() == laterFrame->size()) {
for (std::size_t command = 0; command < frame->size() && !moved; ++command) {
const auto& before = (*frame)[command].vertices;
const auto& after = (*laterFrame)[command].vertices;
for (std::size_t vertex = 0; vertex < std::min(before.size(), after.size()); ++vertex) {
if (std::abs(before[vertex].x - after[vertex].x) > 0.001F
|| std::abs(before[vertex].y - after[vertex].y) > 0.001F) {
moved = true;
break;
}
}
}
}
expect(moved, "animation sampling changes vertex positions over time");
}
error.clear();
const auto binary = plugin.loadSkeleton(
DTS_TEST_SPINEBOY_SKEL, DTS_TEST_SPINEBOY_ATLAS, 1.0F, error);
expect(binary.has_value(), "official Spineboy SKEL loads: " + error);
expect(binary && binary->boneCount > 0, "Spineboy SKEL contains bones");
expect(binary && !binary->animations.empty(), "Spineboy SKEL contains animations");
#endif
}
void testRuntime42OfficialAssets() {
#if defined(DTS_TEST_42_JSON)
dts::RuntimePlugin plugin;
std::string error;
expect(plugin.load(DTS_RUNTIME_42_PATH, error), "4.2 Runtime loads for official assets");
const auto json = plugin.loadSkeleton(DTS_TEST_42_JSON, DTS_TEST_42_ATLAS, 1.0F, error);
expect(json.has_value(), "official 4.2 Spineboy JSON loads: " + error);
expect(json && !json->animations.empty(), "official 4.2 JSON contains animations");
if (json && !json->animations.empty()) {
expect(plugin.setAnimation(json->animations.front().name, true, error),
"4.2 animation can be selected: " + error);
const auto frame = plugin.updatePreview(1.0F / 60.0F, error);
expect(frame && !frame->empty(), "4.2 preview produces draw commands: " + error);
}
error.clear();
const auto binary = plugin.loadSkeleton(DTS_TEST_42_SKEL, DTS_TEST_42_ATLAS, 1.0F, error);
expect(binary.has_value(), "official 4.2 Spineboy SKEL loads: " + error);
#endif
}
void testLegacyOfficialAsset(const char* line, const char* runtimePath,
const char* jsonPath, const char* binaryPath, const char* atlasPath) {
dts::RuntimePlugin plugin;
std::string error;
expect(plugin.load(runtimePath, error), std::string(line) + " Runtime loads for assets");
const auto json = plugin.loadSkeleton(jsonPath, atlasPath, 1.0F, error);
expect(json.has_value(), std::string("official ") + line + " JSON loads: " + error);
expect(json && !json->animations.empty(), std::string(line) + " JSON contains animations");
if (json && !json->animations.empty()) {
expect(plugin.setAnimation(json->animations.front().name, true, error),
std::string(line) + " animation can be selected: " + error);
const auto first = plugin.updatePreview(1.0F / 60.0F, error);
expect(first && !first->empty(), std::string(line) + " preview draws: " + error);
const auto later = plugin.updatePreview(0.25F, error);
expect(later && !later->empty(), std::string(line) + " later frame draws: " + error);
}
error.clear();
const auto binary = plugin.loadSkeleton(binaryPath, atlasPath, 1.0F, error);
expect(binary.has_value(), std::string("official ") + line + " SKEL loads: " + error);
}
void testLegacyOfficialAssets() {
#if defined(DTS_TEST_38_JSON)
testLegacyOfficialAsset("3.8", DTS_RUNTIME_38_PATH,
DTS_TEST_38_JSON, DTS_TEST_38_SKEL, DTS_TEST_38_ATLAS);
testLegacyOfficialAsset("4.0", DTS_RUNTIME_40_PATH,
DTS_TEST_40_JSON, DTS_TEST_40_SKEL, DTS_TEST_40_ATLAS);
testLegacyOfficialAsset("4.1", DTS_RUNTIME_41_PATH,
DTS_TEST_41_JSON, DTS_TEST_41_SKEL, DTS_TEST_41_ATLAS);
#endif
}
} // namespace
int main() {
testVersions();
testLauncherOutput();
testTimedOutOutput();
testMacBundleNormalization();
testProcessTimeout();
testDataVersionDetection();
testAtlasParsing();
testInstalledVersionDiscovery();
testRuntimePlugins();
testLegacyOfficialAssets();
testRuntime42OfficialAssets();
testRuntime43OfficialAssets();
if (failures == 0) {
std::cout << "All core tests passed.\n";
}
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}