Some animation work
This commit is contained in:
@@ -21,7 +21,7 @@ static void test_keyframeSetGetValuePerTrack(void **state) {
|
||||
uint16_t trackCounts[] = { 2, 2 };
|
||||
|
||||
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, 1, 0.5f), 150.0f, 0.0001f);
|
||||
@@ -40,7 +40,7 @@ static void test_keyframeSetGetValuesFillsAllTracks(void **state) {
|
||||
uint16_t trackCounts[] = { 2, 2 };
|
||||
|
||||
keyframeset_t set;
|
||||
keyframeSetInit(&set, tracks, trackCounts, 2);
|
||||
keyframeSetInit(&set, tracks, trackCounts, NULL, NULL, 2, NULL);
|
||||
|
||||
float_t values[2];
|
||||
keyframeSetGetValues(&set, 0.5f, values);
|
||||
@@ -61,16 +61,66 @@ static void test_keyframeSetGetDurationIsLongestTrack(void **state) {
|
||||
uint16_t trackCounts[] = { 2, 2 };
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_keyframeSetGetValuePerTrack),
|
||||
cmocka_unit_test(test_keyframeSetGetValuesFillsAllTracks),
|
||||
cmocka_unit_test(test_keyframeSetGetDurationIsLongestTrack),
|
||||
cmocka_unit_test(test_keyframeSetFireCallbackInvokesPerTrackCallback),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
||||
@@ -9,3 +9,4 @@ dusktest(test_assetlocale.c)
|
||||
dusktest(test_asset.c)
|
||||
dusktest(test_assetjsonloader.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