加入5套spine官方运行时
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
/******************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
#include <spine/spine.h>
|
||||
#include <spine/InheritTimeline.h>
|
||||
#include "SkeletonSerializer.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <locale.h>
|
||||
#include <math.h>
|
||||
|
||||
static void printBonePoseJson(spine::BonePose &pose) {
|
||||
printf("{");
|
||||
printf("\"x\":%.6f,\"y\":%.6f,\"rotation\":%.6f,\"scaleX\":%.6f,\"scaleY\":%.6f,\"shearX\":%.6f,\"shearY\":%.6f,", pose.getX(), pose.getY(),
|
||||
pose.getRotation(), pose.getScaleX(), pose.getScaleY(), pose.getShearX(), pose.getShearY());
|
||||
printf("\"a\":%.6f,\"b\":%.6f,\"c\":%.6f,\"d\":%.6f,", pose.getA(), pose.getB(), pose.getC(), pose.getD());
|
||||
printf("\"worldX\":%.6f,\"worldY\":%.6f,\"worldRotationX\":%.6f,\"worldRotationY\":%.6f,\"worldScaleX\":%.6f,\"worldScaleY\":%.6f",
|
||||
pose.getWorldX(), pose.getWorldY(), pose.getWorldRotationX(), pose.getWorldRotationY(), pose.getWorldScaleX(), pose.getWorldScaleY());
|
||||
printf("}");
|
||||
}
|
||||
|
||||
static void printSlotPoseJson(spine::SlotPose &pose) {
|
||||
printf("{");
|
||||
printf("\"attachment\":");
|
||||
spine::Attachment *attachment = pose.getAttachment();
|
||||
if (attachment)
|
||||
printf("\"%s\"", attachment->getName().buffer());
|
||||
else
|
||||
printf("null");
|
||||
printf(",\"sequenceIndex\":%d,", pose.getSequenceIndex());
|
||||
spine::Color &color = pose.getColor();
|
||||
printf("\"color\":{\"r\":%.6f,\"g\":%.6f,\"b\":%.6f,\"a\":%.6f}", color.r, color.g, color.b, color.a);
|
||||
printf("}");
|
||||
}
|
||||
|
||||
static void printSkeletonFrameJson(spine::Skeleton &skeleton, int frame) {
|
||||
printf("\n=== ANIMATION FRAME %d ===\n", frame);
|
||||
printf("{");
|
||||
printf("\"frame\":%d,", frame);
|
||||
printf("\"time\":%.6f,", skeleton.getTime());
|
||||
printf("\"bones\":[");
|
||||
spine::Array<spine::Bone *> &bones = skeleton.getBones();
|
||||
for (size_t i = 0; i < bones.size(); i++) {
|
||||
if (i) printf(",");
|
||||
spine::Bone *bone = bones[i];
|
||||
printf("{");
|
||||
printf("\"name\":\"%s\",", bone->getData().getName().buffer());
|
||||
printf("\"pose\":");
|
||||
printBonePoseJson(bone->getPose());
|
||||
printf(",\"appliedPose\":");
|
||||
printBonePoseJson(bone->getAppliedPose());
|
||||
printf("}");
|
||||
}
|
||||
printf("],\"slots\":[");
|
||||
spine::Array<spine::Slot *> &slots = skeleton.getSlots();
|
||||
for (size_t i = 0; i < slots.size(); i++) {
|
||||
if (i) printf(",");
|
||||
spine::Slot *slot = slots[i];
|
||||
printf("{");
|
||||
printf("\"name\":\"%s\",", slot->getData().getName().buffer());
|
||||
printf("\"pose\":");
|
||||
printSlotPoseJson(slot->getPose());
|
||||
printf(",\"appliedPose\":");
|
||||
printSlotPoseJson(slot->getAppliedPose());
|
||||
printf("}");
|
||||
}
|
||||
printf("]}");
|
||||
}
|
||||
|
||||
using namespace spine;
|
||||
|
||||
// Provide the default extension implementation
|
||||
namespace spine {
|
||||
SpineExtension *getDefaultExtension() {
|
||||
return new DefaultSpineExtension();
|
||||
}
|
||||
}// namespace spine
|
||||
|
||||
// Mock texture that doesn't require OpenGL
|
||||
class MockTexture {
|
||||
public:
|
||||
int width = 1024;
|
||||
int height = 1024;
|
||||
};
|
||||
|
||||
// Custom texture loader that doesn't load actual textures
|
||||
class HeadlessTextureLoader : public TextureLoader {
|
||||
public:
|
||||
virtual void load(AtlasPage &page, const String &path) override {
|
||||
// Don't load actual texture, just set dimensions
|
||||
page.texture = new MockTexture();
|
||||
page.width = 1024;
|
||||
page.height = 1024;
|
||||
}
|
||||
|
||||
virtual void unload(void *texture) override {
|
||||
delete static_cast<MockTexture *>(texture);
|
||||
}
|
||||
};
|
||||
|
||||
static bool testInheritTimelineBoneIndex() {
|
||||
InheritTimeline timeline(1, 123);
|
||||
if (timeline.getBoneIndex() != 123) {
|
||||
fprintf(stderr, "InheritTimeline bone index was not initialized correctly\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Set locale to ensure consistent number formatting
|
||||
setlocale(LC_ALL, "C");
|
||||
|
||||
if (!testInheritTimelineBoneIndex()) return 1;
|
||||
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: HeadlessTest <skeleton-path> <atlas-path> [animation-name] [animation-name-2]\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Bone::setYDown(false);
|
||||
|
||||
const char *skeletonPath = argv[1];
|
||||
const char *atlasPath = argv[2];
|
||||
const char *animationName = argc >= 4 ? argv[3] : nullptr;
|
||||
const char *animationName2 = argc >= 5 ? argv[4] : nullptr;
|
||||
|
||||
// Load atlas with headless texture loader
|
||||
HeadlessTextureLoader textureLoader;
|
||||
Atlas *atlas = new Atlas(atlasPath, &textureLoader);
|
||||
|
||||
// Load skeleton data
|
||||
SkeletonData *skeletonData = nullptr;
|
||||
|
||||
if (strstr(skeletonPath, ".json") != nullptr) {
|
||||
SkeletonJson json(*atlas);
|
||||
skeletonData = json.readSkeletonDataFile(skeletonPath);
|
||||
} else {
|
||||
SkeletonBinary binary(*atlas);
|
||||
skeletonData = binary.readSkeletonDataFile(skeletonPath);
|
||||
}
|
||||
|
||||
if (!skeletonData) {
|
||||
fprintf(stderr, "Failed to load skeleton data\n");
|
||||
delete atlas;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create skeleton instance
|
||||
Skeleton skeleton(*skeletonData);
|
||||
|
||||
// Set animation if provided
|
||||
AnimationState *state = nullptr;
|
||||
AnimationStateData *stateData = nullptr;
|
||||
if (animationName != nullptr) {
|
||||
// Create animation state only when needed
|
||||
stateData = new AnimationStateData(*skeletonData);
|
||||
state = new AnimationState(*stateData);
|
||||
|
||||
// Find and set animation
|
||||
Animation *animation = skeletonData->findAnimation(animationName);
|
||||
if (!animation) {
|
||||
fprintf(stderr, "Animation not found: %s\n", animationName);
|
||||
delete skeletonData;
|
||||
delete atlas;
|
||||
return 1;
|
||||
}
|
||||
state->setAnimation(0, *animation, true);
|
||||
// Update and apply
|
||||
state->update(0.016f);
|
||||
state->apply(skeleton);
|
||||
}
|
||||
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
|
||||
// Use SkeletonSerializer for JSON output
|
||||
SkeletonSerializer serializer;
|
||||
|
||||
// Print skeleton data
|
||||
printf("=== SKELETON DATA ===\n");
|
||||
printf("%s", serializer.serializeSkeletonData(skeletonData).buffer());
|
||||
|
||||
// Print skeleton state
|
||||
printf("\n=== SKELETON STATE ===\n");
|
||||
printf("%s", serializer.serializeSkeleton(&skeleton).buffer());
|
||||
|
||||
// Print animation state only if animation was loaded
|
||||
if (state != nullptr) {
|
||||
printf("\n=== ANIMATION STATE ===\n");
|
||||
printf("%s", serializer.serializeAnimationState(state).buffer());
|
||||
}
|
||||
|
||||
// Full animation sampling: if a single animation is provided, sample skeleton state for every
|
||||
// frame across the full non-looping animation duration at 60 FPS.
|
||||
if (state != nullptr && animationName2 == nullptr) {
|
||||
Animation *animation = skeletonData->findAnimation(animationName);
|
||||
if (!animation) {
|
||||
fprintf(stderr, "Animation not found: %s\n", animationName);
|
||||
delete state;
|
||||
delete stateData;
|
||||
delete skeletonData;
|
||||
delete atlas;
|
||||
return 1;
|
||||
}
|
||||
|
||||
skeleton.setupPose();
|
||||
state->clearTracks();
|
||||
state->setAnimation(0, *animation, false);
|
||||
state->apply(skeleton);
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
|
||||
int frameCount = (int) ceilf(animation->getDuration() * 60.0f);
|
||||
for (int i = 0; i <= frameCount; i++) {
|
||||
if (i > 0) {
|
||||
state->update(1 / 60.0f);
|
||||
state->apply(skeleton);
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
}
|
||||
printSkeletonFrameJson(skeleton, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Transition test: if a second animation is provided, play A for 10 frames, transition to B,
|
||||
// then sample skeleton state at frames 5, 10, 15, 20 during the mix.
|
||||
if (state != nullptr && animationName2 != nullptr) {
|
||||
Animation *animation2 = skeletonData->findAnimation(animationName2);
|
||||
if (!animation2) {
|
||||
fprintf(stderr, "Animation not found: %s\n", animationName2);
|
||||
delete state;
|
||||
delete stateData;
|
||||
delete skeletonData;
|
||||
delete atlas;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Reset skeleton and state
|
||||
skeleton.setupPose();
|
||||
state->clearTracks();
|
||||
state->setAnimation(0, *skeletonData->findAnimation(animationName), true);
|
||||
|
||||
// Run 10 frames of animation A
|
||||
for (int i = 0; i < 10; i++) {
|
||||
state->update(1 / 60.0f);
|
||||
state->apply(skeleton);
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
}
|
||||
|
||||
// Transition to animation B
|
||||
state->setAnimation(0, *animation2, true);
|
||||
|
||||
// Run 20 frames through the mix, serializing at frames 5, 10, 15, 20
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
state->update(1 / 60.0f);
|
||||
state->apply(skeleton);
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
if (i == 5 || i == 10 || i == 15 || i == 20) {
|
||||
SkeletonSerializer transSerializer;
|
||||
printf("\n=== TRANSITION FRAME %d ===\n", i);
|
||||
printf("%s", transSerializer.serializeSkeleton(&skeleton).buffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (state != nullptr) {
|
||||
delete state;
|
||||
}
|
||||
if (stateData != nullptr) {
|
||||
delete stateData;
|
||||
}
|
||||
delete skeletonData;
|
||||
delete atlas;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#ifndef Spine_JsonWriter_h
|
||||
#define Spine_JsonWriter_h
|
||||
|
||||
#include <spine/SpineString.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace spine {
|
||||
|
||||
class JsonWriter {
|
||||
private:
|
||||
String buffer;
|
||||
int depth;
|
||||
bool needsComma;
|
||||
|
||||
public:
|
||||
JsonWriter() : depth(0), needsComma(false) {
|
||||
}
|
||||
|
||||
void writeObjectStart() {
|
||||
writeCommaIfNeeded();
|
||||
buffer.append("{");
|
||||
depth++;
|
||||
needsComma = false;
|
||||
}
|
||||
|
||||
void writeObjectEnd() {
|
||||
depth--;
|
||||
if (needsComma) {
|
||||
buffer.append("\n");
|
||||
writeIndent();
|
||||
}
|
||||
buffer.append("}");
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeArrayStart() {
|
||||
writeCommaIfNeeded();
|
||||
buffer.append("[");
|
||||
depth++;
|
||||
needsComma = false;
|
||||
}
|
||||
|
||||
void writeArrayEnd() {
|
||||
depth--;
|
||||
if (needsComma) {
|
||||
buffer.append("\n");
|
||||
writeIndent();
|
||||
}
|
||||
buffer.append("]");
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeName(const char *name) {
|
||||
writeCommaIfNeeded();
|
||||
buffer.append("\n");
|
||||
writeIndent();
|
||||
buffer.append("\"");
|
||||
buffer.append(name);
|
||||
buffer.append("\": ");
|
||||
needsComma = false;
|
||||
}
|
||||
|
||||
void writeValue(const String &value) {
|
||||
writeCommaIfNeeded();
|
||||
if (value.buffer() == nullptr) {
|
||||
buffer.append("null");
|
||||
} else {
|
||||
buffer.append("\"");
|
||||
buffer.append(escapeString(value));
|
||||
buffer.append("\"");
|
||||
}
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(const char *value) {
|
||||
writeCommaIfNeeded();
|
||||
if (value == nullptr) {
|
||||
buffer.append("null");
|
||||
} else {
|
||||
buffer.append("\"");
|
||||
buffer.append(escapeString(String(value)));
|
||||
buffer.append("\"");
|
||||
}
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(float value) {
|
||||
writeCommaIfNeeded();
|
||||
|
||||
// Format float with 6 decimal places
|
||||
char temp[32];
|
||||
snprintf(temp, sizeof(temp), "%.6f", value);
|
||||
|
||||
// Remove trailing zeros
|
||||
char *end = temp + strlen(temp) - 1;
|
||||
while (end > temp && *end == '0') {
|
||||
end--;
|
||||
}
|
||||
if (*end == '.') {
|
||||
end--;
|
||||
}
|
||||
*(end + 1) = '\0';
|
||||
|
||||
buffer.append(temp);
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(int value) {
|
||||
writeCommaIfNeeded();
|
||||
char temp[32];
|
||||
snprintf(temp, sizeof(temp), "%d", value);
|
||||
buffer.append(temp);
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(bool value) {
|
||||
writeCommaIfNeeded();
|
||||
buffer.append(value ? "true" : "false");
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(size_t value) {
|
||||
writeCommaIfNeeded();
|
||||
char temp[32];
|
||||
snprintf(temp, sizeof(temp), "%zu", value);
|
||||
buffer.append(temp);
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeValue(PropertyId value) {
|
||||
writeCommaIfNeeded();
|
||||
char temp[32];
|
||||
snprintf(temp, sizeof(temp), "%lld", (long long) value);
|
||||
buffer.append(temp);
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void writeNull() {
|
||||
writeCommaIfNeeded();
|
||||
buffer.append("null");
|
||||
needsComma = true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
buffer.append("\n");
|
||||
}
|
||||
|
||||
String getString() const {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private:
|
||||
void writeCommaIfNeeded() {
|
||||
if (needsComma) {
|
||||
buffer.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
void writeIndent() {
|
||||
for (int i = 0; i < depth; i++) {
|
||||
buffer.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
String escapeString(const String &str) {
|
||||
String result("");
|
||||
const char *chars = str.buffer();
|
||||
if (chars) {
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
char c = chars[i];
|
||||
switch (c) {
|
||||
case '"':
|
||||
result.append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
result.append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
result.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
result.append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
result.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
result.append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
result.append("\\t");
|
||||
break;
|
||||
default:
|
||||
result.append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
}// namespace spine
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,296 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*****************************************************************************/
|
||||
|
||||
#include <spine/Extension.h>
|
||||
#include <spine/Map.h>
|
||||
#include <spine/SpineString.h>
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace spine;
|
||||
|
||||
namespace spine {
|
||||
SpineExtension *getDefaultExtension() {
|
||||
return new DefaultSpineExtension();
|
||||
}
|
||||
}
|
||||
|
||||
class TrackingExtension : public SpineExtension {
|
||||
public:
|
||||
TrackingExtension() : SpineExtension() {
|
||||
}
|
||||
|
||||
size_t activeAllocations() const {
|
||||
return _allocations.size();
|
||||
}
|
||||
|
||||
size_t activeBytes() const {
|
||||
size_t total = 0;
|
||||
for (std::unordered_map<void *, size_t>::const_iterator i = _allocations.begin(); i != _allocations.end(); ++i) total += i->second;
|
||||
return total;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void *_alloc(size_t size, const char *file, int line) override {
|
||||
SP_UNUSED(file);
|
||||
SP_UNUSED(line);
|
||||
if (size == 0) return NULL;
|
||||
void *ptr = malloc(size);
|
||||
assert(ptr);
|
||||
_allocations[ptr] = size;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
virtual void *_calloc(size_t size, const char *file, int line) override {
|
||||
void *ptr = _alloc(size, file, line);
|
||||
if (ptr) memset(ptr, 0, size);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
virtual void *_realloc(void *ptr, size_t size, const char *file, int line) override {
|
||||
SP_UNUSED(file);
|
||||
SP_UNUSED(line);
|
||||
if (ptr == NULL) return _alloc(size, file, line);
|
||||
std::unordered_map<void *, size_t>::iterator existing = _allocations.find(ptr);
|
||||
assert(existing != _allocations.end());
|
||||
_allocations.erase(existing);
|
||||
if (size == 0) {
|
||||
::free(ptr);
|
||||
return NULL;
|
||||
}
|
||||
void *newPtr = ::realloc(ptr, size);
|
||||
assert(newPtr);
|
||||
_allocations[newPtr] = size;
|
||||
return newPtr;
|
||||
}
|
||||
|
||||
virtual void _free(void *mem, const char *file, int line) override {
|
||||
SP_UNUSED(file);
|
||||
SP_UNUSED(line);
|
||||
if (!mem) return;
|
||||
std::unordered_map<void *, size_t>::iterator existing = _allocations.find(mem);
|
||||
assert(existing != _allocations.end());
|
||||
_allocations.erase(existing);
|
||||
::free(mem);
|
||||
}
|
||||
|
||||
virtual char *_readFile(const String &path, int *length) override {
|
||||
SP_UNUSED(path);
|
||||
SP_UNUSED(length);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<void *, size_t> _allocations;
|
||||
};
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
#define CHECK(condition) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
class BadIntHash {
|
||||
public:
|
||||
size_t operator()(const int &key) const {
|
||||
return (size_t) (key & 7);
|
||||
}
|
||||
};
|
||||
|
||||
static String makeString(const char *prefix, int value) {
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "%s%d", prefix, value);
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
static void testBasicOperations() {
|
||||
Map<int, int, BadIntHash> map;
|
||||
CHECK(map.isEmpty());
|
||||
CHECK(map.size() == 0);
|
||||
CHECK(map.get(123) == NULL);
|
||||
CHECK(!map.containsKey(123));
|
||||
CHECK(!map.remove(123));
|
||||
|
||||
for (int i = 0; i < 1000; i++) map.put(i, i * 3);
|
||||
CHECK(map.size() == 1000);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int *value = map.get(i);
|
||||
CHECK(value && *value == i * 3);
|
||||
CHECK(map.containsKey(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 1000; i += 3) map.put(i, -i);
|
||||
CHECK(map.size() == 1000);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int expected = i % 3 == 0 ? -i : i * 3;
|
||||
CHECK(map[i] == expected);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 1000; i += 2) CHECK(map.remove(i));
|
||||
CHECK(map.size() == 500);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int *value = map.get(i);
|
||||
if (i % 2 == 0)
|
||||
CHECK(value == NULL);
|
||||
else
|
||||
CHECK(value && *value == (i % 3 == 0 ? -i : i * 3));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 1000; i += 2) map.put(i, i + 7);
|
||||
CHECK(map.size() == 1000);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int expected = i % 2 == 0 ? i + 7 : (i % 3 == 0 ? -i : i * 3);
|
||||
CHECK(map[i] == expected);
|
||||
}
|
||||
}
|
||||
|
||||
static void testEntriesAndAddAll() {
|
||||
Map<int, int> map;
|
||||
Array<int> keys;
|
||||
for (int i = 0; i < 100; i++) keys.add(i);
|
||||
CHECK(map.addAll(keys, 42));
|
||||
CHECK(!map.addAll(keys, 42));
|
||||
CHECK(map.size() == 100);
|
||||
|
||||
int count = 0;
|
||||
int sum = 0;
|
||||
Map<int, int>::Entries entries = map.getEntries();
|
||||
while (entries.hasNext()) {
|
||||
Map<int, int>::Pair pair = entries.next();
|
||||
CHECK(pair.value == 42);
|
||||
count++;
|
||||
sum += pair.key;
|
||||
}
|
||||
CHECK(count == 100);
|
||||
CHECK(sum == 4950);
|
||||
}
|
||||
|
||||
static void testPutMissing() {
|
||||
int a = 1, b = 2;
|
||||
Map<int, int *> map;
|
||||
CHECK(map.putMissing(5, &a) == NULL);
|
||||
CHECK(map.size() == 1);
|
||||
CHECK(map.putMissing(5, &b) == &a);
|
||||
CHECK(map.size() == 1);
|
||||
CHECK(*map.get(5) == &a);
|
||||
}
|
||||
|
||||
static void testCopyAndAssignment() {
|
||||
Map<int, String> original;
|
||||
original.put(1, String("one"));
|
||||
original.put(2, String("two"));
|
||||
|
||||
Map<int, String> copy(original);
|
||||
copy.put(1, String("changed"));
|
||||
CHECK(*original.get(1) == String("one"));
|
||||
CHECK(*copy.get(1) == String("changed"));
|
||||
CHECK(*copy.get(2) == String("two"));
|
||||
|
||||
Map<int, String> assigned;
|
||||
assigned.put(99, String("discard"));
|
||||
assigned = original;
|
||||
CHECK(assigned.get(99) == NULL);
|
||||
CHECK(*assigned.get(1) == String("one"));
|
||||
CHECK(*assigned.get(2) == String("two"));
|
||||
}
|
||||
|
||||
static void testStringMemory(TrackingExtension &extension) {
|
||||
size_t baselineAllocations = extension.activeAllocations();
|
||||
{
|
||||
Map<String, String> map;
|
||||
map.ensureCapacity(64);
|
||||
size_t mapOnlyAllocations = extension.activeAllocations();
|
||||
CHECK(mapOnlyAllocations > baselineAllocations);
|
||||
|
||||
for (int i = 0; i < 200; i++) map.put(makeString("key", i), makeString("value", i));
|
||||
CHECK(map.size() == 200);
|
||||
CHECK(extension.activeAllocations() > mapOnlyAllocations);
|
||||
|
||||
for (int i = 0; i < 100; i++) CHECK(map.remove(makeString("key", i)));
|
||||
CHECK(map.size() == 100);
|
||||
for (int i = 100; i < 200; i++) CHECK(*map.get(makeString("key", i)) == makeString("value", i));
|
||||
|
||||
map.clear();
|
||||
CHECK(map.size() == 0);
|
||||
CHECK(extension.activeAllocations() == mapOnlyAllocations);
|
||||
}
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
}
|
||||
|
||||
static void testStress() {
|
||||
Map<int, int, BadIntHash> map;
|
||||
bool present[257];
|
||||
int values[257];
|
||||
for (int i = 0; i < 257; i++) {
|
||||
present[i] = false;
|
||||
values[i] = 0;
|
||||
}
|
||||
|
||||
unsigned int seed = 0x12345678u;
|
||||
for (int step = 0; step < 10000; step++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
int key = (int) ((seed >> 8) % 257);
|
||||
int op = (int) ((seed >> 24) % 3);
|
||||
if (op == 0) {
|
||||
bool removed = map.remove(key);
|
||||
CHECK(removed == present[key]);
|
||||
present[key] = false;
|
||||
} else {
|
||||
int value = (int) seed;
|
||||
map.put(key, value);
|
||||
present[key] = true;
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
if ((step & 127) == 0) {
|
||||
size_t expectedSize = 0;
|
||||
for (int i = 0; i < 257; i++) {
|
||||
if (present[i]) {
|
||||
expectedSize++;
|
||||
CHECK(map.get(i) && *map.get(i) == values[i]);
|
||||
} else
|
||||
CHECK(map.get(i) == NULL);
|
||||
}
|
||||
CHECK(map.size() == expectedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
TrackingExtension extension;
|
||||
SpineExtension::setInstance(&extension);
|
||||
|
||||
size_t baselineAllocations = extension.activeAllocations();
|
||||
testBasicOperations();
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
testEntriesAndAddAll();
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
testPutMissing();
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
testCopyAndAssignment();
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
testStringMemory(extension);
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
testStress();
|
||||
CHECK(extension.activeAllocations() == baselineAllocations);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "MapTest failed: %d failure(s), %zu active allocation(s), %zu active byte(s)\n", failures, extension.activeAllocations(),
|
||||
extension.activeBytes());
|
||||
return 1;
|
||||
}
|
||||
printf("MapTest passed\n");
|
||||
return 0;
|
||||
}
|
||||
+4768
File diff suppressed because it is too large
Load Diff
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# Spine-C++ Docker Test Runner
|
||||
#
|
||||
# Runs the full spine-cpp test suite inside a Linux Docker container.
|
||||
# This ensures consistent testing across different host environments.
|
||||
|
||||
set -e
|
||||
|
||||
# Change to spine-runtimes root directory (parent of spine-cpp)
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
# Source logging utilities
|
||||
source formatters/logging/logging.sh
|
||||
|
||||
log_title "Spine-C++ Docker Test"
|
||||
log_detail "Running tests in Linux container"
|
||||
|
||||
# Detect existing build type
|
||||
BUILD_TYPE=""
|
||||
cd spine-cpp
|
||||
if [ -f "build/CMakeCache.txt" ]; then
|
||||
if grep -q "CMAKE_BUILD_TYPE:STRING=Release" build/CMakeCache.txt 2>/dev/null; then
|
||||
BUILD_TYPE="release"
|
||||
elif grep -q "CMAKE_BUILD_TYPE:STRING=Debug" build/CMakeCache.txt 2>/dev/null; then
|
||||
BUILD_TYPE="debug"
|
||||
fi
|
||||
log_detail "Detected existing build type: ${BUILD_TYPE:-unknown}"
|
||||
fi
|
||||
cd ..
|
||||
|
||||
# Use a lightweight Linux image with build tools
|
||||
DOCKER_IMAGE="gcc:12"
|
||||
|
||||
log_action "Pulling Docker image"
|
||||
if docker pull "$DOCKER_IMAGE" > /dev/null 2>&1; then
|
||||
log_ok
|
||||
else
|
||||
log_fail "Failed to pull Docker image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_action "Running tests in container"
|
||||
if DOCKER_OUTPUT=$(docker run --rm \
|
||||
-v "$(pwd)":/workspace \
|
||||
-w /workspace/spine-cpp \
|
||||
"$DOCKER_IMAGE" \
|
||||
bash -c "apt-get update -qq && apt-get install -y -qq cmake ninja-build && ./tests/test.sh" 2>&1); then
|
||||
log_ok
|
||||
|
||||
log_action "Analyzing executables"
|
||||
if ANALYSIS_OUTPUT=$(docker run --rm \
|
||||
-v "$(pwd)":/workspace \
|
||||
-w /workspace/spine-cpp \
|
||||
"$DOCKER_IMAGE" \
|
||||
bash -c "echo '=== EXECUTABLE ANALYSIS ==='; for exe in build/headless-test*; do if [ -f \"\$exe\" ] && [ -x \"\$exe\" ]; then echo; echo \"--- \$(basename \"\$exe\") ---\"; echo \"Size: \$(du -h \"\$exe\" | cut -f1)\"; echo \"Dependencies:\"; ldd \"\$exe\" 2>/dev/null || echo 'No dynamic dependencies'; fi; done" 2>&1); then
|
||||
log_ok
|
||||
log_detail "$ANALYSIS_OUTPUT"
|
||||
else
|
||||
log_fail
|
||||
log_error_output "$ANALYSIS_OUTPUT"
|
||||
fi
|
||||
|
||||
log_action "Restoring local build directory"
|
||||
cd spine-cpp
|
||||
if rm -rf build; then
|
||||
if [ -n "$BUILD_TYPE" ]; then
|
||||
log_detail "Rebuilding $BUILD_TYPE configuration"
|
||||
if ./build.sh clean "$BUILD_TYPE" > /dev/null 2>&1; then
|
||||
log_ok "Local build directory restored ($BUILD_TYPE)"
|
||||
else
|
||||
log_detail "Warning: Could not restore $BUILD_TYPE build"
|
||||
fi
|
||||
else
|
||||
log_detail "No previous build detected, leaving clean"
|
||||
log_ok "Build directory cleaned"
|
||||
fi
|
||||
else
|
||||
log_detail "Warning: Could not remove build directory"
|
||||
fi
|
||||
else
|
||||
log_fail
|
||||
log_error_output "$DOCKER_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Spine-C++ Smoke Test
|
||||
#
|
||||
# Tests all spine-cpp build variants with spineboy example data:
|
||||
# - headless-test (regular dynamic)
|
||||
# - headless-test-no-cpprt (no-cpprt dynamic)
|
||||
# - headless-test-static (regular static, Linux only)
|
||||
# - headless-test-no-cpprt-static (no-cpprt static, Linux only)
|
||||
|
||||
set -e
|
||||
|
||||
# Change to spine-cpp root directory
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Source logging utilities
|
||||
source ../formatters/logging/logging.sh
|
||||
|
||||
# Test configuration - spineboy example files and animation
|
||||
SPINEBOY_SKEL="../examples/spineboy/export/spineboy-pro.skel"
|
||||
SPINEBOY_ATLAS="../examples/spineboy/export/spineboy-pma.atlas"
|
||||
SPINEBOY_ANIM="idle"
|
||||
|
||||
# Expected output pattern - first 10 lines of skeleton JSON data
|
||||
EXPECTED_OUTPUT="=== SKELETON DATA ===
|
||||
{
|
||||
\"refString\": \"<SkeletonData-spineboy-pro>\",
|
||||
\"type\": \"SkeletonData\",
|
||||
\"bones\": [{
|
||||
\"refString\": \"<BoneData-root>\",
|
||||
\"type\": \"BoneData\",
|
||||
\"index\": 0,
|
||||
\"parent\": null,
|
||||
\"length\": 0,"
|
||||
|
||||
log_title "Spine-C++ Test"
|
||||
log_detail "Platform: $(uname)"
|
||||
|
||||
log_action "Building all variants"
|
||||
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
|
||||
log_ok
|
||||
else
|
||||
log_fail "Build failed"
|
||||
log_detail "$BUILD_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test_count=0
|
||||
pass_count=0
|
||||
|
||||
log_action "Testing map-test"
|
||||
test_count=$((test_count + 1))
|
||||
if OUTPUT=$(build/map-test 2>&1); then
|
||||
log_ok
|
||||
pass_count=$((pass_count + 1))
|
||||
else
|
||||
log_fail "map-test - execution failed"
|
||||
log_detail "$OUTPUT"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
for exe in build/headless-test*; do
|
||||
if [ -f "$exe" ] && [ -x "$exe" ]; then
|
||||
exe_name=$(basename "$exe")
|
||||
log_action "Testing $exe_name"
|
||||
|
||||
test_count=$((test_count + 1))
|
||||
|
||||
if OUTPUT=$("$exe" $SPINEBOY_SKEL $SPINEBOY_ATLAS $SPINEBOY_ANIM 2>&1); then
|
||||
actual_output=$(echo "$OUTPUT" | head -10)
|
||||
|
||||
if [ "$actual_output" = "$EXPECTED_OUTPUT" ]; then
|
||||
log_ok
|
||||
pass_count=$((pass_count + 1))
|
||||
else
|
||||
log_fail "$exe_name - output mismatch"
|
||||
log_detail "Expected:"
|
||||
log_detail "$EXPECTED_OUTPUT"
|
||||
log_detail ""
|
||||
log_detail "Actual:"
|
||||
log_detail "$actual_output"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
log_fail "$exe_name - execution failed"
|
||||
log_detail "$OUTPUT"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $pass_count -eq $test_count ] && [ $test_count -gt 0 ]; then
|
||||
log_summary "✓ All tests passed ($pass_count/$test_count)"
|
||||
exit 0
|
||||
else
|
||||
log_summary "✗ Tests failed ($pass_count/$test_count)"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user