Some animation work
This commit is contained in:
@@ -16,7 +16,9 @@ void animationInit(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
|
||||||
keyframeSetInit(&anim->keyframes, tracks, trackCounts, trackCount);
|
keyframeSetInit(
|
||||||
|
&anim->keyframes, tracks, trackCounts, NULL, NULL, trackCount, NULL
|
||||||
|
);
|
||||||
anim->time = 0.0f;
|
anim->time = 0.0f;
|
||||||
anim->speed = 1.0f;
|
anim->speed = 1.0f;
|
||||||
anim->loop = false;
|
anim->loop = false;
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ void keyframeSetInit(
|
|||||||
keyframeset_t *set,
|
keyframeset_t *set,
|
||||||
keyframe_t **tracks,
|
keyframe_t **tracks,
|
||||||
uint16_t *trackCounts,
|
uint16_t *trackCounts,
|
||||||
uint16_t trackCount
|
keyframesetcallback_t *callbacks,
|
||||||
|
void **users,
|
||||||
|
uint16_t trackCount,
|
||||||
|
void *user
|
||||||
) {
|
) {
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
assertNotNull(tracks, "Tracks pointer cannot be null.");
|
assertNotNull(tracks, "Tracks pointer cannot be null.");
|
||||||
@@ -20,6 +23,9 @@ void keyframeSetInit(
|
|||||||
set->tracks = tracks;
|
set->tracks = tracks;
|
||||||
set->trackCounts = trackCounts;
|
set->trackCounts = trackCounts;
|
||||||
set->trackCount = trackCount;
|
set->trackCount = trackCount;
|
||||||
|
set->callbacks = callbacks;
|
||||||
|
set->users = users;
|
||||||
|
set->user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t keyframeSetGetValue(
|
float_t keyframeSetGetValue(
|
||||||
@@ -58,3 +64,13 @@ float_t keyframeSetGetDuration(keyframeset_t *set) {
|
|||||||
}
|
}
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
|
||||||
|
|
||||||
|
if(!set->callbacks || !set->callbacks[trackIndex]) return;
|
||||||
|
set->callbacks[trackIndex](
|
||||||
|
set, trackIndex, set->users ? set->users[trackIndex] : NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,35 +6,68 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "keyframe.h"
|
#include "keyframe.h"
|
||||||
|
|
||||||
|
typedef struct keyframeset_t keyframeset_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback associated with one track of a keyframe set (see
|
||||||
|
* keyframeSetFireCallback()).
|
||||||
|
*
|
||||||
|
* @param set The keyframe set the track belongs to.
|
||||||
|
* @param trackIndex The track this callback is registered for.
|
||||||
|
* @param user The per-track user pointer passed to keyframeSetInit().
|
||||||
|
*/
|
||||||
|
typedef void (*keyframesetcallback_t)(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const uint16_t trackIndex,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A group of N parallel keyframe tracks (e.g. one per animated channel --
|
* A group of N parallel keyframe tracks (e.g. one per animated channel --
|
||||||
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
|
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
|
||||||
* track is independent: its own keyframe array and count, evaluated at
|
* track is independent: its own keyframe array and count, evaluated at
|
||||||
* whatever time is asked for.
|
* whatever time is asked for.
|
||||||
*/
|
*/
|
||||||
typedef struct {
|
typedef struct keyframeset_t {
|
||||||
/** Caller-owned array of trackCount keyframe_t arrays. */
|
/** Caller-owned array of trackCount keyframe_t arrays. */
|
||||||
keyframe_t **tracks;
|
keyframe_t **tracks;
|
||||||
/** Caller-owned array of trackCount counts, one per entry in tracks. */
|
/** Caller-owned array of trackCount counts, one per entry in tracks. */
|
||||||
uint16_t *trackCounts;
|
uint16_t *trackCounts;
|
||||||
uint16_t trackCount;
|
uint16_t trackCount;
|
||||||
|
|
||||||
|
/** Caller-owned array of trackCount callbacks, one per track. Entries
|
||||||
|
* (or the whole array) may be NULL for tracks with no callback. */
|
||||||
|
keyframesetcallback_t *callbacks;
|
||||||
|
/** Caller-owned array of trackCount user pointers, one per track,
|
||||||
|
* passed to that track's callbacks[] entry. May be NULL. */
|
||||||
|
void **users;
|
||||||
|
/** Extra user data for the set as a whole, not tied to any one track. */
|
||||||
|
void *user;
|
||||||
} keyframeset_t;
|
} keyframeset_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a keyframe set. Neither tracks, trackCounts, nor the
|
* Initializes a keyframe set. None of the passed-in arrays (or the
|
||||||
* keyframe_t arrays they point to are copied -- they must outlive this
|
* keyframe_t arrays tracks points to) are copied -- they must outlive
|
||||||
* keyframeset_t.
|
* this keyframeset_t.
|
||||||
*
|
*
|
||||||
* @param set The keyframe set to initialize.
|
* @param set The keyframe set to initialize.
|
||||||
* @param tracks Array of trackCount keyframe_t arrays.
|
* @param tracks Array of trackCount keyframe_t arrays.
|
||||||
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
||||||
|
* @param callbacks Array of trackCount callbacks, one per track, or NULL
|
||||||
|
* if no track has one.
|
||||||
|
* @param users Array of trackCount user pointers, matching callbacks, or
|
||||||
|
* NULL.
|
||||||
* @param trackCount The number of tracks.
|
* @param trackCount The number of tracks.
|
||||||
|
* @param user Extra user data for the set as a whole; may be NULL.
|
||||||
*/
|
*/
|
||||||
void keyframeSetInit(
|
void keyframeSetInit(
|
||||||
keyframeset_t *set,
|
keyframeset_t *set,
|
||||||
keyframe_t **tracks,
|
keyframe_t **tracks,
|
||||||
uint16_t *trackCounts,
|
uint16_t *trackCounts,
|
||||||
uint16_t trackCount
|
keyframesetcallback_t *callbacks,
|
||||||
|
void **users,
|
||||||
|
uint16_t trackCount,
|
||||||
|
void *user
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,3 +105,12 @@ void keyframeSetGetValues(
|
|||||||
* @return The set's duration, in seconds.
|
* @return The set's duration, in seconds.
|
||||||
*/
|
*/
|
||||||
float_t keyframeSetGetDuration(keyframeset_t *set);
|
float_t keyframeSetGetDuration(keyframeset_t *set);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes a track's callback, if it (and its callbacks array) is set.
|
||||||
|
* No-op otherwise.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set the track belongs to.
|
||||||
|
* @param trackIndex The track whose callback to invoke.
|
||||||
|
*/
|
||||||
|
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex);
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(dmf)
|
add_subdirectory(dmf)
|
||||||
|
add_subdirectory(animation)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
assetanimationloader.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "assetanimationloader.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "asset/loader/assetloading.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderAsync(assetloading_t *loading) {
|
||||||
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
assertNotMainThread("Async loader should not be on main thread.");
|
||||||
|
|
||||||
|
if(
|
||||||
|
loading->loading.animation.state != ASSET_ANIMATION_LOADING_STATE_READ_FILE
|
||||||
|
) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(loading->loading.animation.buffer, "Buffer already defined?");
|
||||||
|
|
||||||
|
assetfile_t *file = &loading->loading.animation.file;
|
||||||
|
assetLoaderErrorChain(
|
||||||
|
loading, assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
if(file->size > ASSET_ANIMATION_FILE_SIZE_MAX) {
|
||||||
|
assetLoaderErrorThrow(
|
||||||
|
loading, "Animation JSON exceeds maximum allowed size"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *buffer;
|
||||||
|
size_t size;
|
||||||
|
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
|
||||||
|
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||||
|
|
||||||
|
loading->loading.animation.buffer = buffer;
|
||||||
|
loading->loading.animation.size = size;
|
||||||
|
loading->loading.animation.state = ASSET_ANIMATION_LOADING_STATE_PARSE;
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderSync(assetloading_t *loading) {
|
||||||
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
assertTrue(loading->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
switch(loading->loading.animation.state) {
|
||||||
|
case ASSET_ANIMATION_LOADING_STATE_INITIAL:
|
||||||
|
loading->loading.animation.state =
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_READ_FILE;
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||||
|
errorOk();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASSET_ANIMATION_LOADING_STATE_PARSE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *buffer = loading->loading.animation.buffer;
|
||||||
|
assertNotNull(buffer, "Animation buffer should have been loaded by now.");
|
||||||
|
|
||||||
|
yyjson_doc *doc = yyjson_read(
|
||||||
|
(char *)buffer,
|
||||||
|
loading->loading.animation.size,
|
||||||
|
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
|
||||||
|
);
|
||||||
|
memoryFree(buffer);
|
||||||
|
loading->loading.animation.buffer = NULL;
|
||||||
|
|
||||||
|
if(!doc) assetLoaderErrorThrow(loading, "Failed to parse animation JSON");
|
||||||
|
|
||||||
|
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||||
|
yyjson_val *channelsVal = yyjson_obj_get(root, "channels");
|
||||||
|
if(!channelsVal || !yyjson_is_arr(channelsVal)) {
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
assetLoaderErrorThrow(loading, "Animation JSON missing 'channels' array");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t channelCount = (uint16_t)yyjson_arr_size(channelsVal);
|
||||||
|
if(channelCount == 0) {
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
assetLoaderErrorThrow(loading, "Animation must have at least one channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframe_t **tracks = memoryAllocate(channelCount * sizeof(keyframe_t *));
|
||||||
|
uint16_t *trackCounts = memoryAllocate(channelCount * sizeof(uint16_t));
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *channelJson;
|
||||||
|
uint16_t parsed = 0;
|
||||||
|
yyjson_arr_foreach(channelsVal, idx, max, channelJson) {
|
||||||
|
keyframe_t *keyframes;
|
||||||
|
uint16_t count;
|
||||||
|
errorret_t ret =
|
||||||
|
assetAnimationParseChannel(channelJson, &keyframes, &count);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
assetAnimationFreeChannels(tracks, parsed);
|
||||||
|
memoryFree(trackCounts);
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks[idx] = keyframes;
|
||||||
|
trackCounts[idx] = count;
|
||||||
|
parsed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationInit(
|
||||||
|
&loading->entry->data.animation, tracks, trackCounts, channelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
yyjson_val *loopVal = yyjson_obj_get(root, "loop");
|
||||||
|
if(loopVal) loading->entry->data.animation.loop = yyjson_get_bool(loopVal);
|
||||||
|
|
||||||
|
yyjson_val *speedVal = yyjson_obj_get(root, "speed");
|
||||||
|
if(speedVal) {
|
||||||
|
loading->entry->data.animation.speed = (float_t)yyjson_get_num(speedVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationDispose(assetentry_t *entry) {
|
||||||
|
assertNotNull(entry, "Asset entry cannot be NULL");
|
||||||
|
assertTrue(entry->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
// A load that failed before animationInit() ran (e.g. a parse error)
|
||||||
|
// never populated these -- nothing to free in that case.
|
||||||
|
keyframeset_t *set = &entry->data.animation.keyframes;
|
||||||
|
if(set->tracks) {
|
||||||
|
assetAnimationFreeChannels(set->tracks, set->trackCount);
|
||||||
|
memoryFree(set->trackCounts);
|
||||||
|
set->tracks = NULL;
|
||||||
|
set->trackCounts = NULL;
|
||||||
|
set->trackCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationParseEasing(
|
||||||
|
const char_t *name,
|
||||||
|
easingtype_t *outEasing
|
||||||
|
) {
|
||||||
|
if(stringEquals(name, "LINEAR")) *outEasing = EASING_LINEAR;
|
||||||
|
else if(stringEquals(name, "IN_SINE")) *outEasing = EASING_IN_SINE;
|
||||||
|
else if(stringEquals(name, "OUT_SINE")) *outEasing = EASING_OUT_SINE;
|
||||||
|
else if(stringEquals(name, "IN_OUT_SINE")) *outEasing = EASING_IN_OUT_SINE;
|
||||||
|
else if(stringEquals(name, "IN_QUAD")) *outEasing = EASING_IN_QUAD;
|
||||||
|
else if(stringEquals(name, "OUT_QUAD")) *outEasing = EASING_OUT_QUAD;
|
||||||
|
else if(stringEquals(name, "IN_OUT_QUAD")) *outEasing = EASING_IN_OUT_QUAD;
|
||||||
|
else if(stringEquals(name, "IN_CUBIC")) *outEasing = EASING_IN_CUBIC;
|
||||||
|
else if(stringEquals(name, "OUT_CUBIC")) *outEasing = EASING_OUT_CUBIC;
|
||||||
|
else if(stringEquals(name, "IN_OUT_CUBIC")) *outEasing = EASING_IN_OUT_CUBIC;
|
||||||
|
else if(stringEquals(name, "IN_QUART")) *outEasing = EASING_IN_QUART;
|
||||||
|
else if(stringEquals(name, "OUT_QUART")) *outEasing = EASING_OUT_QUART;
|
||||||
|
else if(stringEquals(name, "IN_OUT_QUART")) *outEasing = EASING_IN_OUT_QUART;
|
||||||
|
else if(stringEquals(name, "IN_BACK")) *outEasing = EASING_IN_BACK;
|
||||||
|
else if(stringEquals(name, "OUT_BACK")) *outEasing = EASING_OUT_BACK;
|
||||||
|
else if(stringEquals(name, "IN_OUT_BACK")) *outEasing = EASING_IN_OUT_BACK;
|
||||||
|
else errorThrow("Unknown easing type '%s'", name);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationParseChannel(
|
||||||
|
yyjson_val *channelJson,
|
||||||
|
keyframe_t **outKeyframes,
|
||||||
|
uint16_t *outCount
|
||||||
|
) {
|
||||||
|
if(!yyjson_is_arr(channelJson)) {
|
||||||
|
errorThrow("Animation channel must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t count = yyjson_arr_size(channelJson);
|
||||||
|
if(count == 0) {
|
||||||
|
errorThrow("Animation channel must have at least one keyframe");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframe_t *keyframes = memoryAllocate(count * sizeof(keyframe_t));
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *keyframeJson;
|
||||||
|
yyjson_arr_foreach(channelJson, idx, max, keyframeJson) {
|
||||||
|
yyjson_val *timeVal = yyjson_obj_get(keyframeJson, "time");
|
||||||
|
yyjson_val *valueVal = yyjson_obj_get(keyframeJson, "value");
|
||||||
|
if(!timeVal || !valueVal) {
|
||||||
|
memoryFree(keyframes);
|
||||||
|
errorThrow("Animation keyframe missing 'time' or 'value'");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframes[idx].time = (float_t)yyjson_get_num(timeVal);
|
||||||
|
keyframes[idx].value = (float_t)yyjson_get_num(valueVal);
|
||||||
|
|
||||||
|
yyjson_val *easingVal = yyjson_obj_get(keyframeJson, "easing");
|
||||||
|
if(!easingVal) {
|
||||||
|
keyframes[idx].easing = EASING_LINEAR;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t ret = assetAnimationParseEasing(
|
||||||
|
yyjson_get_str(easingVal), &keyframes[idx].easing
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(keyframes);
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*outKeyframes = keyframes;
|
||||||
|
*outCount = (uint16_t)count;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count) {
|
||||||
|
for(uint16_t i = 0; i < count; i++) memoryFree(tracks[i]);
|
||||||
|
memoryFree(tracks);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "asset/assetfile.h"
|
||||||
|
#include "animation/animation.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
|
#define ASSET_ANIMATION_FILE_SIZE_MAX 1024*256
|
||||||
|
|
||||||
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON animation file format:
|
||||||
|
* {
|
||||||
|
* "loop": false,
|
||||||
|
* "speed": 1.0,
|
||||||
|
* "channels": [
|
||||||
|
* [
|
||||||
|
* { "time": 0.0, "value": 0.0, "easing": "LINEAR" },
|
||||||
|
* { "time": 1.0, "value": 10.0 }
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* "loop" and "speed" are optional, defaulting to false and 1.0 (see
|
||||||
|
* animationInit()). "channels" is required and must have at least one
|
||||||
|
* entry; each entry is itself a non-empty array of keyframes, ascending
|
||||||
|
* by "time", sharing one timeline with every other channel (see
|
||||||
|
* keyframeset_t). Each keyframe requires "time" and "value"; "easing" is
|
||||||
|
* optional and defaults to "LINEAR" (see easingtype_t for the full set of
|
||||||
|
* names, e.g. "IN_QUAD", "OUT_BACK", "IN_OUT_CUBIC").
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_INITIAL,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_READ_FILE,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_PARSE,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_DONE
|
||||||
|
} assetanimationloadingstate_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
assetfile_t file;
|
||||||
|
assetanimationloadingstate_t state;
|
||||||
|
uint8_t *buffer;
|
||||||
|
size_t size;
|
||||||
|
} assetanimationloaderloading_t;
|
||||||
|
|
||||||
|
typedef animation_t assetanimationoutput_t;
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderAsync(assetloading_t *loading);
|
||||||
|
errorret_t assetAnimationLoaderSync(assetloading_t *loading);
|
||||||
|
errorret_t assetAnimationDispose(assetentry_t *entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Maps a JSON easing name (e.g. "IN_OUT_QUAD") to its
|
||||||
|
* easingtype_t.
|
||||||
|
*
|
||||||
|
* @param name The easing name to parse.
|
||||||
|
* @param outEasing Destination for the parsed easing type.
|
||||||
|
* @return Error state; fails if name doesn't match any easingtype_t.
|
||||||
|
*/
|
||||||
|
errorret_t assetAnimationParseEasing(
|
||||||
|
const char_t *name,
|
||||||
|
easingtype_t *outEasing
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Parses one "channels" array entry into a heap-allocated
|
||||||
|
* keyframe_t array (memoryAllocate) -- freed later by
|
||||||
|
* assetAnimationFreeChannels() (on a parse failure) or
|
||||||
|
* assetAnimationDispose() (once loaded).
|
||||||
|
*
|
||||||
|
* @param channelJson The channel's JSON array of keyframe objects.
|
||||||
|
* @param outKeyframes Destination for the newly allocated keyframe array.
|
||||||
|
* @param outCount Destination for the number of keyframes parsed.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t assetAnimationParseChannel(
|
||||||
|
yyjson_val *channelJson,
|
||||||
|
keyframe_t **outKeyframes,
|
||||||
|
uint16_t *outCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Frees the first count entries of tracks (each a
|
||||||
|
* memoryAllocate'd keyframe_t array from assetAnimationParseChannel()),
|
||||||
|
* then frees tracks itself. Used both to unwind a partially-parsed
|
||||||
|
* channel list on failure and to free a fully-loaded animation's
|
||||||
|
* channels in assetAnimationDispose().
|
||||||
|
*
|
||||||
|
* @param tracks The channel array to free.
|
||||||
|
* @param count The number of entries in tracks to free.
|
||||||
|
*/
|
||||||
|
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count);
|
||||||
@@ -45,4 +45,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.loadAsync = assetJsonLoaderAsync,
|
.loadAsync = assetJsonLoaderAsync,
|
||||||
.dispose = assetJsonDispose
|
.dispose = assetJsonDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ASSET_LOADER_TYPE_ANIMATION] = {
|
||||||
|
.loadSync = assetAnimationLoaderSync,
|
||||||
|
.loadAsync = assetAnimationLoaderAsync,
|
||||||
|
.dispose = assetAnimationDispose
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "asset/loader/display/assettilesetloader.h"
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "asset/loader/json/assetjsonloader.h"
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
|
#include "asset/loader/animation/assetanimationloader.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
@@ -22,6 +23,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_TILESET,
|
ASSET_LOADER_TYPE_TILESET,
|
||||||
ASSET_LOADER_TYPE_LOCALE,
|
ASSET_LOADER_TYPE_LOCALE,
|
||||||
ASSET_LOADER_TYPE_JSON,
|
ASSET_LOADER_TYPE_JSON,
|
||||||
|
ASSET_LOADER_TYPE_ANIMATION,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
ASSET_LOADER_TYPE_COUNT
|
||||||
} assetloadertype_t;
|
} assetloadertype_t;
|
||||||
@@ -33,6 +35,7 @@ typedef union {
|
|||||||
assettilesetloaderloading_t tileset;
|
assettilesetloaderloading_t tileset;
|
||||||
assetlocaleloaderloading_t locale;
|
assetlocaleloaderloading_t locale;
|
||||||
assetjsonloaderloading_t json;
|
assetjsonloaderloading_t json;
|
||||||
|
assetanimationloaderloading_t animation;
|
||||||
} assetloaderloading_t;
|
} assetloaderloading_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -42,6 +45,7 @@ typedef union {
|
|||||||
assettilesetoutput_t tileset;
|
assettilesetoutput_t tileset;
|
||||||
assetlocaleoutput_t locale;
|
assetlocaleoutput_t locale;
|
||||||
assetjsonoutput_t json;
|
assetjsonoutput_t json;
|
||||||
|
assetanimationoutput_t animation;
|
||||||
} assetloaderoutput_t;
|
} assetloaderoutput_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ static void test_keyframeSetGetValuePerTrack(void **state) {
|
|||||||
uint16_t trackCounts[] = { 2, 2 };
|
uint16_t trackCounts[] = { 2, 2 };
|
||||||
|
|
||||||
keyframeset_t set;
|
keyframeset_t set;
|
||||||
keyframeSetInit(&set, tracks, trackCounts, 2);
|
keyframeSetInit(&set, tracks, trackCounts, NULL, NULL, 2, NULL);
|
||||||
|
|
||||||
assert_float_equal(keyframeSetGetValue(&set, 0, 0.5f), 5.0f, 0.0001f);
|
assert_float_equal(keyframeSetGetValue(&set, 0, 0.5f), 5.0f, 0.0001f);
|
||||||
assert_float_equal(keyframeSetGetValue(&set, 1, 0.5f), 150.0f, 0.0001f);
|
assert_float_equal(keyframeSetGetValue(&set, 1, 0.5f), 150.0f, 0.0001f);
|
||||||
@@ -40,7 +40,7 @@ static void test_keyframeSetGetValuesFillsAllTracks(void **state) {
|
|||||||
uint16_t trackCounts[] = { 2, 2 };
|
uint16_t trackCounts[] = { 2, 2 };
|
||||||
|
|
||||||
keyframeset_t set;
|
keyframeset_t set;
|
||||||
keyframeSetInit(&set, tracks, trackCounts, 2);
|
keyframeSetInit(&set, tracks, trackCounts, NULL, NULL, 2, NULL);
|
||||||
|
|
||||||
float_t values[2];
|
float_t values[2];
|
||||||
keyframeSetGetValues(&set, 0.5f, values);
|
keyframeSetGetValues(&set, 0.5f, values);
|
||||||
@@ -61,16 +61,66 @@ static void test_keyframeSetGetDurationIsLongestTrack(void **state) {
|
|||||||
uint16_t trackCounts[] = { 2, 2 };
|
uint16_t trackCounts[] = { 2, 2 };
|
||||||
|
|
||||||
keyframeset_t set;
|
keyframeset_t set;
|
||||||
keyframeSetInit(&set, tracks, trackCounts, 2);
|
keyframeSetInit(&set, tracks, trackCounts, NULL, NULL, 2, NULL);
|
||||||
|
|
||||||
assert_float_equal(keyframeSetGetDuration(&set), 3.5f, 0.0001f);
|
assert_float_equal(keyframeSetGetDuration(&set), 3.5f, 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t TEST_CALLBACK_FIRE_COUNT;
|
||||||
|
static uint16_t TEST_CALLBACK_LAST_TRACK;
|
||||||
|
static void *TEST_CALLBACK_LAST_USER;
|
||||||
|
|
||||||
|
static void test_onTrackCallback(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const uint16_t trackIndex,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
TEST_CALLBACK_FIRE_COUNT++;
|
||||||
|
TEST_CALLBACK_LAST_TRACK = trackIndex;
|
||||||
|
TEST_CALLBACK_LAST_USER = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_keyframeSetFireCallbackInvokesPerTrackCallback(void **state) {
|
||||||
|
TEST_CALLBACK_FIRE_COUNT = 0;
|
||||||
|
TEST_CALLBACK_LAST_TRACK = 0xFFFF;
|
||||||
|
TEST_CALLBACK_LAST_USER = NULL;
|
||||||
|
|
||||||
|
keyframe_t xKeyframes[] = {
|
||||||
|
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||||
|
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
|
||||||
|
};
|
||||||
|
keyframe_t yKeyframes[] = {
|
||||||
|
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||||
|
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
|
||||||
|
};
|
||||||
|
keyframe_t *tracks[] = { xKeyframes, yKeyframes };
|
||||||
|
uint16_t trackCounts[] = { 2, 2 };
|
||||||
|
|
||||||
|
int32_t trackOneUser = 42;
|
||||||
|
keyframesetcallback_t callbacks[] = { NULL, test_onTrackCallback };
|
||||||
|
void *users[] = { NULL, &trackOneUser };
|
||||||
|
int32_t setUser = 7;
|
||||||
|
|
||||||
|
keyframeset_t set;
|
||||||
|
keyframeSetInit(&set, tracks, trackCounts, callbacks, users, 2, &setUser);
|
||||||
|
|
||||||
|
// Track 0 has no callback registered -- no-op.
|
||||||
|
keyframeSetFireCallback(&set, 0);
|
||||||
|
assert_int_equal(TEST_CALLBACK_FIRE_COUNT, 0);
|
||||||
|
|
||||||
|
keyframeSetFireCallback(&set, 1);
|
||||||
|
assert_int_equal(TEST_CALLBACK_FIRE_COUNT, 1);
|
||||||
|
assert_int_equal(TEST_CALLBACK_LAST_TRACK, 1);
|
||||||
|
assert_ptr_equal(TEST_CALLBACK_LAST_USER, &trackOneUser);
|
||||||
|
assert_ptr_equal(set.user, &setUser);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
const struct CMUnitTest tests[] = {
|
const struct CMUnitTest tests[] = {
|
||||||
cmocka_unit_test(test_keyframeSetGetValuePerTrack),
|
cmocka_unit_test(test_keyframeSetGetValuePerTrack),
|
||||||
cmocka_unit_test(test_keyframeSetGetValuesFillsAllTracks),
|
cmocka_unit_test(test_keyframeSetGetValuesFillsAllTracks),
|
||||||
cmocka_unit_test(test_keyframeSetGetDurationIsLongestTrack),
|
cmocka_unit_test(test_keyframeSetGetDurationIsLongestTrack),
|
||||||
|
cmocka_unit_test(test_keyframeSetFireCallbackInvokesPerTrackCallback),
|
||||||
};
|
};
|
||||||
|
|
||||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ dusktest(test_assetlocale.c)
|
|||||||
dusktest(test_asset.c)
|
dusktest(test_asset.c)
|
||||||
dusktest(test_assetjsonloader.c)
|
dusktest(test_assetjsonloader.c)
|
||||||
dusktest(test_assettilesetloader.c)
|
dusktest(test_assettilesetloader.c)
|
||||||
|
dusktest(test_assetanimationloader.c)
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
#include "asset/loader/animation/assetanimationloader.h"
|
||||||
|
#include "thread/thread.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include <zip.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Fixtures
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static const char_t *ANIMATION_VALID =
|
||||||
|
"{"
|
||||||
|
" \"loop\": true,"
|
||||||
|
" \"speed\": 2.0,"
|
||||||
|
" \"channels\": ["
|
||||||
|
" ["
|
||||||
|
" { \"time\": 0.0, \"value\": 0.0, \"easing\": \"LINEAR\" },"
|
||||||
|
" { \"time\": 1.0, \"value\": 10.0 }"
|
||||||
|
" ],"
|
||||||
|
" ["
|
||||||
|
" { \"time\": 0.0, \"value\": 100.0, \"easing\": \"IN_QUAD\" },"
|
||||||
|
" { \"time\": 2.0, \"value\": 200.0 }"
|
||||||
|
" ]"
|
||||||
|
" ]"
|
||||||
|
"}";
|
||||||
|
static const char_t *ANIMATION_INVALID_JSON =
|
||||||
|
"{ this is definitely not valid json !!!";
|
||||||
|
static const char_t *ANIMATION_MISSING_CHANNELS = "{ \"loop\": false }";
|
||||||
|
static const char_t *ANIMATION_EMPTY_CHANNELS = "{ \"channels\": [] }";
|
||||||
|
static const char_t *ANIMATION_BAD_EASING =
|
||||||
|
"{"
|
||||||
|
" \"channels\": ["
|
||||||
|
" ["
|
||||||
|
" { \"time\": 0.0, \"value\": 0.0, \"easing\": \"NOT_A_REAL_EASING\" },"
|
||||||
|
" { \"time\": 1.0, \"value\": 10.0 }"
|
||||||
|
" ]"
|
||||||
|
" ]"
|
||||||
|
"}";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Async thread helper
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
assetloading_t *loading;
|
||||||
|
bool_t ok;
|
||||||
|
} animation_async_run_t;
|
||||||
|
|
||||||
|
static void animation_async_thread_cb(thread_t *thread) {
|
||||||
|
animation_async_run_t *run = (animation_async_run_t *)thread->data;
|
||||||
|
errorret_t ret = assetAnimationLoaderAsync(run->loading);
|
||||||
|
run->ok = errorIsOk(ret);
|
||||||
|
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool_t run_animation_async(assetloading_t *loading) {
|
||||||
|
animation_async_run_t run = { .loading = loading, .ok = false };
|
||||||
|
thread_t thread;
|
||||||
|
threadInit(&thread, animation_async_thread_cb);
|
||||||
|
thread.data = &run;
|
||||||
|
threadStart(&thread);
|
||||||
|
threadStop(&thread);
|
||||||
|
return run.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// In-memory ZIP
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static zip_t *g_zip = NULL;
|
||||||
|
|
||||||
|
static int zip_setup(void **state) {
|
||||||
|
zip_error_t err;
|
||||||
|
zip_error_init(&err);
|
||||||
|
|
||||||
|
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
|
||||||
|
if(!write_src) return -1;
|
||||||
|
|
||||||
|
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
|
||||||
|
if(!za) { zip_source_free(write_src); return -1; }
|
||||||
|
|
||||||
|
zip_source_t *s;
|
||||||
|
s = zip_source_buffer(za, ANIMATION_VALID, strlen(ANIMATION_VALID), 0);
|
||||||
|
if(zip_file_add(za, "valid.anim.json", s, ZIP_FL_OVERWRITE) < 0) {
|
||||||
|
zip_close(za); return -1;
|
||||||
|
}
|
||||||
|
s = zip_source_buffer(
|
||||||
|
za, ANIMATION_INVALID_JSON, strlen(ANIMATION_INVALID_JSON), 0
|
||||||
|
);
|
||||||
|
if(zip_file_add(za, "invalid.anim.json", s, ZIP_FL_OVERWRITE) < 0) {
|
||||||
|
zip_close(za); return -1;
|
||||||
|
}
|
||||||
|
s = zip_source_buffer(
|
||||||
|
za, ANIMATION_MISSING_CHANNELS, strlen(ANIMATION_MISSING_CHANNELS), 0
|
||||||
|
);
|
||||||
|
if(zip_file_add(za, "missingchannels.anim.json", s, ZIP_FL_OVERWRITE) < 0) {
|
||||||
|
zip_close(za); return -1;
|
||||||
|
}
|
||||||
|
s = zip_source_buffer(
|
||||||
|
za, ANIMATION_EMPTY_CHANNELS, strlen(ANIMATION_EMPTY_CHANNELS), 0
|
||||||
|
);
|
||||||
|
if(zip_file_add(za, "emptychannels.anim.json", s, ZIP_FL_OVERWRITE) < 0) {
|
||||||
|
zip_close(za); return -1;
|
||||||
|
}
|
||||||
|
s = zip_source_buffer(
|
||||||
|
za, ANIMATION_BAD_EASING, strlen(ANIMATION_BAD_EASING), 0
|
||||||
|
);
|
||||||
|
if(zip_file_add(za, "badeasing.anim.json", s, ZIP_FL_OVERWRITE) < 0) {
|
||||||
|
zip_close(za); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip_source_keep(write_src);
|
||||||
|
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
|
||||||
|
|
||||||
|
zip_stat_t zs;
|
||||||
|
memset(&zs, 0, sizeof(zs));
|
||||||
|
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
|
||||||
|
zip_source_free(write_src); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *zipbuf = malloc((size_t)zs.size);
|
||||||
|
if(!zipbuf) { zip_source_free(write_src); return -1; }
|
||||||
|
|
||||||
|
if(zip_source_open(write_src) != 0) {
|
||||||
|
free(zipbuf); zip_source_free(write_src); return -1;
|
||||||
|
}
|
||||||
|
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
|
||||||
|
zip_source_close(write_src);
|
||||||
|
zip_source_free(write_src);
|
||||||
|
|
||||||
|
zip_error_init(&err);
|
||||||
|
zip_source_t *read_src = zip_source_buffer_create(
|
||||||
|
zipbuf, (zip_uint64_t)zs.size, 1, &err
|
||||||
|
);
|
||||||
|
if(!read_src) { free(zipbuf); return -1; }
|
||||||
|
|
||||||
|
g_zip = zip_open_from_source(read_src, 0, &err);
|
||||||
|
if(!g_zip) { zip_source_free(read_src); return -1; }
|
||||||
|
|
||||||
|
ASSET.zip = g_zip;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int zip_teardown(void **state) {
|
||||||
|
if(g_zip) { zip_close(g_zip); g_zip = NULL; }
|
||||||
|
ASSET.zip = NULL;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Loader pipeline helper
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
assetentry_t entry;
|
||||||
|
assetloading_t loading;
|
||||||
|
} loader_ctx_t;
|
||||||
|
|
||||||
|
static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) {
|
||||||
|
assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_ANIMATION, NULL);
|
||||||
|
threadMutexInit(&ctx->loading.mutex);
|
||||||
|
memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading));
|
||||||
|
ctx->loading.type = ASSET_LOADER_TYPE_ANIMATION;
|
||||||
|
ctx->loading.entry = &ctx->entry;
|
||||||
|
ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drives sync(INITIAL) -> async(READ) -> sync(PARSE).
|
||||||
|
static errorret_t loader_ctx_run(loader_ctx_t *ctx) {
|
||||||
|
errorret_t ret = assetAnimationLoaderSync(&ctx->loading);
|
||||||
|
if(errorIsNotOk(ret)) return ret;
|
||||||
|
|
||||||
|
if(!run_animation_async(&ctx->loading)) {
|
||||||
|
ctx->entry.state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow("Async animation load failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return assetAnimationLoaderSync(&ctx->loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loader_ctx_dispose(loader_ctx_t *ctx) {
|
||||||
|
if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) {
|
||||||
|
errorret_t ret = assetEntryDispose(&ctx->entry);
|
||||||
|
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||||
|
}
|
||||||
|
threadMutexDispose(&ctx->loading.mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_animation_valid_loads(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "valid.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = loader_ctx_run(&ctx);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED);
|
||||||
|
|
||||||
|
animation_t *anim = &ctx.entry.data.animation;
|
||||||
|
assert_true(anim->loop);
|
||||||
|
assert_float_equal(anim->speed, 2.0f, 0.0001f);
|
||||||
|
assert_int_equal(anim->keyframes.trackCount, 2);
|
||||||
|
|
||||||
|
assert_float_equal(
|
||||||
|
keyframeSetGetValue(&anim->keyframes, 0, 0.5f), 5.0f, 0.0001f
|
||||||
|
);
|
||||||
|
// Channel 1's first keyframe uses IN_QUAD -- a segment's easing comes
|
||||||
|
// from its starting keyframe, not its end. easingInQuad(0.5) = 0.25 ->
|
||||||
|
// lerp(100, 200, 0.25) = 125.
|
||||||
|
assert_float_equal(
|
||||||
|
keyframeSetGetValue(&anim->keyframes, 1, 1.0f), 125.0f, 0.0001f
|
||||||
|
);
|
||||||
|
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_animation_parse_error(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "invalid.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = assetAnimationLoaderSync(&ctx.loading);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(run_animation_async(&ctx.loading));
|
||||||
|
|
||||||
|
ret = assetAnimationLoaderSync(&ctx.loading);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
||||||
|
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_animation_missing_channels(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "missingchannels.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = loader_ctx_run(&ctx);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
||||||
|
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_animation_empty_channels(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "emptychannels.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = loader_ctx_run(&ctx);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
||||||
|
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_animation_bad_easing(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "badeasing.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = loader_ctx_run(&ctx);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
||||||
|
|
||||||
|
// Confirms the mid-parse cleanup path frees the channel array it had
|
||||||
|
// already allocated before hitting the bad easing name.
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_animation_missing_file(void **state) {
|
||||||
|
loader_ctx_t ctx;
|
||||||
|
loader_ctx_init(&ctx, "nonexistent.anim.json");
|
||||||
|
|
||||||
|
errorret_t ret = assetAnimationLoaderSync(&ctx.loading);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
assert_false(run_animation_async(&ctx.loading));
|
||||||
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
||||||
|
|
||||||
|
loader_ctx_dispose(&ctx);
|
||||||
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// main
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
assertInit();
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_valid_loads, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_parse_error, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_missing_channels, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_empty_channels, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_bad_easing, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_animation_missing_file, zip_setup, zip_teardown
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user