导出数据转spine
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user