Remove rpg, ui, editor and scene subfolders; split scripts by platform

Delete src/dusk/rpg, src/dusk/ui, the animation/keyframe modules, the
scene subfolders, the chunk asset loader and ./editor, and strip the
dependent save slot, scene type, engine and test code so the main
binary and tests build again. Add scene/node.c/.h skeleton and move
build scripts into scripts/<platform>/.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
YourWishesandClaude Sonnet 5.5 committed 2026-11-03 07:29:25 -06:00
1 parent 3948607b1e
commit 31ee7989de
464 files changed
+70 -33009

No files matched your search

-2
View File
@@ -10,8 +10,6 @@ add_subdirectory(console)
add_subdirectory(error)
add_subdirectory(thread)
add_subdirectory(display)
add_subdirectory(rpg)
add_subdirectory(item)
add_subdirectory(save)
add_subdirectory(time)
add_subdirectory(util)
-2
View File
@@ -7,5 +7,3 @@ include(dusktest)
# Tests
dusktest(test_easing.c)
dusktest(test_keyframe.c)
dusktest(test_animation.c)
-443
View File
@@ -1,443 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "animation/animation.h"
#include "util/memory.h"
typedef struct {
uint16_t updateCount;
uint16_t loopCount;
uint16_t completeCount;
float_t lastValues[8];
} animationtestcallbacks_t;
static void testOnUpdate(
const uint16_t layer, const float_t value, void *user
) {
animationtestcallbacks_t *cb = (animationtestcallbacks_t *)user;
cb->updateCount++;
cb->lastValues[layer] = value;
}
static void testOnLoop(void *user) {
((animationtestcallbacks_t *)user)->loopCount++;
}
static void testOnComplete(void *user) {
((animationtestcallbacks_t *)user)->completeCount++;
}
static void animationTestInit(
animation_t *anim,
animationtestcallbacks_t *cb,
keyframe_t *keyframes,
uint16_t *keyframeCounts,
const uint16_t layerCount
) {
memoryZero(cb, sizeof(animationtestcallbacks_t));
animationInit(anim, keyframes, keyframeCounts, layerCount);
anim->user = cb;
anim->onUpdate = testOnUpdate;
anim->onLoop = testOnLoop;
anim->onComplete = testOnComplete;
}
static void test_animationInitSingleLayerDuration(void **state) {
keyframe_t keyframes[3] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 3 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 1);
assert_float_equal(anim.duration, 2.0f, 0.0001f);
assert_int_equal(anim.layerCount, 1);
assert_float_equal(anim.time, 0.0f, 0.0001f);
assert_int_equal(anim.flags, 0);
}
static void test_animationInitMultiLayerDurationIsMax(void **state) {
// Both layers must share the same keyframe count - animationInit/
// animationGetLayerValue flatten the array as layer * keyframeCounts[layer],
// which only produces the correct offset when every layer's count matches.
keyframe_t keyframes[4] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 3.0f, .value = 300.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[2] = { 2, 2 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 2);
// Layer 1's last keyframe (time=3) is later than layer 0's (time=1).
assert_float_equal(anim.duration, 3.0f, 0.0001f);
}
static void test_animationInitNullAsserts(void **state) {
keyframe_t keyframes[1] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 1 };
animation_t anim;
expect_assert_failure(animationInit(NULL, keyframes, keyframeCounts, 1));
expect_assert_failure(animationInit(&anim, NULL, keyframeCounts, 1));
expect_assert_failure(animationInit(&anim, keyframes, NULL, 1));
}
static void test_animationInitZeroLayerCountAsserts(void **state) {
keyframe_t keyframes[1] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 1 };
animation_t anim;
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 0));
}
static void test_animationInitUnsortedKeyframesAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
}
static void test_animationGetLayerValueSamplesEachLayerIndependently(
void **state
) {
keyframe_t keyframes[4] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[2] = { 2, 2 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 2);
anim.time = 1.0f;
assert_float_equal(animationGetLayerValue(&anim, 0), 10.0f, 0.0001f);
assert_float_equal(animationGetLayerValue(&anim, 1), 50.0f, 0.0001f);
}
static void test_animationGetLayerValueOutOfBoundsAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 1);
expect_assert_failure(animationGetLayerValue(&anim, 1));
}
static void test_animationUpdateAdvancesTimeAndCallsOnUpdate(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
animationUpdate(&anim, 0.5f);
assert_float_equal(anim.time, 0.5f, 0.0001f);
assert_int_equal(cb.updateCount, 1);
assert_float_equal(cb.lastValues[0], 5.0f, 0.0001f);
assert_int_equal(cb.completeCount, 0);
}
static void test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly(
void **state
) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
// Overshoot the duration in a single update.
animationUpdate(&anim, 5.0f);
assert_float_equal(anim.time, 2.0f, 0.0001f);
assert_float_equal(cb.lastValues[0], 20.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
// Further updates must not clamp past the end or refire onComplete.
animationUpdate(&anim, 1.0f);
assert_float_equal(anim.time, 2.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdateExactLandingOnEndFiresOnComplete(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
// deltaTime exactly matches the remaining distance to the boundary.
animationUpdate(&anim, 2.0f);
assert_float_equal(anim.time, 2.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdateLoopWrapsAndFiresOnLoop(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_LOOP;
// 2.5 durations worth of time: wraps twice, lands at 1.0 into the third.
animationUpdate(&anim, 5.0f);
assert_float_equal(anim.time, 1.0f, 0.0001f);
assert_int_equal(cb.loopCount, 2);
assert_int_equal(cb.completeCount, 0);
}
static void test_animationUpdateLoopStopEndCompletesInsteadOfWrapping(
void **state
) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_STOP_END;
animationUpdate(&anim, 5.0f);
assert_float_equal(anim.time, 2.0f, 0.0001f);
assert_int_equal(cb.loopCount, 0);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdateReversePlaysBackward(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_REVERSE;
anim.time = anim.duration;
animationUpdate(&anim, 0.5f);
assert_float_equal(anim.time, 1.5f, 0.0001f);
assert_float_equal(cb.lastValues[0], 15.0f, 0.0001f);
}
static void test_animationUpdateReverseStopsAtBeginning(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_REVERSE;
anim.time = anim.duration;
animationUpdate(&anim, 10.0f);
assert_float_equal(anim.time, 0.0f, 0.0001f);
assert_float_equal(cb.lastValues[0], 0.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdatePingpongBouncesForeverWithoutStopFlags(
void **state
) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_PINGPONG;
// 0 -> 2 (2s consumed), reflect, backward 1s more -> time=1, going backward.
animationUpdate(&anim, 3.0f);
assert_float_equal(anim.time, 1.0f, 0.0001f);
assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0);
assert_int_equal(cb.completeCount, 0);
// Continue backward to 0 (1s), reflect, forward 1s more -> time=1, forward.
animationUpdate(&anim, 2.0f);
assert_float_equal(anim.time, 1.0f, 0.0001f);
assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) == 0);
assert_int_equal(cb.completeCount, 0);
}
static void test_animationUpdatePingpongStopEndCompletesAtEnd(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_END;
animationUpdate(&anim, 10.0f);
assert_float_equal(anim.time, 2.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdatePingpongStopBeginningCompletesAtStart(
void **state
) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_BEGINNING;
anim.time = anim.duration;
anim.flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
animationUpdate(&anim, 10.0f);
assert_float_equal(anim.time, 0.0f, 0.0001f);
assert_int_equal(cb.completeCount, 1);
}
static void test_animationUpdateMultiLayerCallsOnUpdatePerLayer(void **state) {
keyframe_t keyframes[4] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[2] = { 2, 2 };
animation_t anim;
animationtestcallbacks_t cb;
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 2);
animationUpdate(&anim, 1.0f);
assert_int_equal(cb.updateCount, 2);
assert_float_equal(cb.lastValues[0], 10.0f, 0.0001f);
assert_float_equal(cb.lastValues[1], 50.0f, 0.0001f);
}
static void test_animationUpdateNullAsserts(void **state) {
expect_assert_failure(animationUpdate(NULL, 1.0f));
}
static void test_animationUpdateNegativeDeltaTimeAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 1);
expect_assert_failure(animationUpdate(&anim, -1.0f));
}
static void test_animationInitZeroDurationAsserts(void **state) {
keyframe_t keyframes[1] = {
{ .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 1 };
animation_t anim;
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
}
static void test_animationUpdateLoopAndPingpongTogetherAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
uint16_t keyframeCounts[1] = { 2 };
animation_t anim;
animationInit(&anim, keyframes, keyframeCounts, 1);
anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_PINGPONG;
expect_assert_failure(animationUpdate(&anim, 0.1f));
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_animationInitSingleLayerDuration),
cmocka_unit_test(test_animationInitMultiLayerDurationIsMax),
cmocka_unit_test(test_animationInitNullAsserts),
cmocka_unit_test(test_animationInitZeroLayerCountAsserts),
cmocka_unit_test(test_animationInitUnsortedKeyframesAsserts),
cmocka_unit_test(test_animationGetLayerValueSamplesEachLayerIndependently),
cmocka_unit_test(test_animationGetLayerValueOutOfBoundsAsserts),
cmocka_unit_test(test_animationUpdateAdvancesTimeAndCallsOnUpdate),
cmocka_unit_test(test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly),
cmocka_unit_test(test_animationUpdateExactLandingOnEndFiresOnComplete),
cmocka_unit_test(test_animationUpdateLoopWrapsAndFiresOnLoop),
cmocka_unit_test(test_animationUpdateLoopStopEndCompletesInsteadOfWrapping),
cmocka_unit_test(test_animationUpdateReversePlaysBackward),
cmocka_unit_test(test_animationUpdateReverseStopsAtBeginning),
cmocka_unit_test(
test_animationUpdatePingpongBouncesForeverWithoutStopFlags
),
cmocka_unit_test(test_animationUpdatePingpongStopEndCompletesAtEnd),
cmocka_unit_test(test_animationUpdatePingpongStopBeginningCompletesAtStart),
cmocka_unit_test(test_animationUpdateMultiLayerCallsOnUpdatePerLayer),
cmocka_unit_test(test_animationUpdateNullAsserts),
cmocka_unit_test(test_animationUpdateNegativeDeltaTimeAsserts),
cmocka_unit_test(test_animationInitZeroDurationAsserts),
cmocka_unit_test(test_animationUpdateLoopAndPingpongTogetherAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-188
View File
@@ -1,188 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "animation/keyframe.h"
static void test_keyframeGetValueSingleSegmentLinear(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 0.25f), 2.5f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 0.75f), 7.5f, 0.0001f);
}
static void test_keyframeGetValueMultiSegment(void **state) {
keyframe_t keyframes[3] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
};
// Within the first segment.
assert_float_equal(keyframeGetValue(keyframes, 3, 0.5f), 5.0f, 0.0001f);
// Exactly on the interior keyframe, resolved as the end of segment one.
assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f);
// Within the second segment.
assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 15.0f, 0.0001f);
}
static void test_keyframeGetValueDescendingValues(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 100.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 50.0f, 0.0001f);
}
static void test_keyframeGetValueAppliesEasing(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
// EASING_IN_QUAD is t * t, so halfway through time should be a quarter of
// the way through the value range, not half.
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f);
}
static void test_keyframeGetValueNonZeroStartTime(void **state) {
keyframe_t keyframes[2] = {
{ .time = 5.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 10.0f, .value = 100.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 0.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 7.5f), 50.0f, 0.0001f);
}
// NOTE: keyframeGetValue does not clamp to the first keyframe's value when
// queried before the first keyframe's time - the start and end pointers
// collapse onto the same keyframe, so the interpolation divides by a zero
// time delta. This test documents that current behavior rather than
// asserting it is desirable; flag to the maintainer if this should instead
// clamp like the last-keyframe case does.
static void test_keyframeGetValueBeforeFirstKeyframeIsNaN(void **state) {
keyframe_t keyframes[2] = {
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 3.0f, .value = 30.0f, .easing = EASING_LINEAR },
};
assert_true(isnan(keyframeGetValue(keyframes, 2, 0.0f)));
}
static void test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue(
void **state
) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 2.0f), 10.0f, 0.0001f);
}
static void test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue(
void **state
) {
keyframe_t keyframes[1] = {
{ .time = 5.0f, .value = 42.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 1, 5.0f), 42.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 1, 10.0f), 42.0f, 0.0001f);
// Before the single keyframe's time is still the documented NaN case.
assert_true(isnan(keyframeGetValue(keyframes, 1, 0.0f)));
}
static void test_keyframeGetValueNullKeyframesAsserts(void **state) {
expect_assert_failure(keyframeGetValue(NULL, 1, 0.0f));
}
static void test_keyframeGetValueZeroCountAsserts(void **state) {
keyframe_t keyframes[1] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
expect_assert_failure(keyframeGetValue(keyframes, 0, 0.0f));
}
static void test_keyframeGetValueNegativeTimeAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
expect_assert_failure(keyframeGetValue(keyframes, 2, -1.0f));
}
static void test_keyframeGetValueUnsortedKeyframesAsserts(void **state) {
keyframe_t keyframes[2] = {
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
};
expect_assert_failure(keyframeGetValue(keyframes, 2, 0.0f));
}
static void test_keyframeGetValueLaterKeyframeOutOfOrderAsserts(void **state) {
// The first pair is sorted, but the third keyframe is out of order - the
// check must walk the whole array, not just the first pair.
keyframe_t keyframes[3] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 0.5f, .value = 5.0f, .easing = EASING_LINEAR },
};
expect_assert_failure(keyframeGetValue(keyframes, 3, 0.0f));
}
static void test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert(
void **state
) {
// Equal (non-decreasing) times are allowed - only strictly decreasing
// times should trigger the sorted check.
keyframe_t keyframes[3] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
};
assert_float_equal(keyframeGetValue(keyframes, 3, 0.0f), 5.0f, 0.0001f);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_keyframeGetValueSingleSegmentLinear),
cmocka_unit_test(test_keyframeGetValueMultiSegment),
cmocka_unit_test(test_keyframeGetValueDescendingValues),
cmocka_unit_test(test_keyframeGetValueAppliesEasing),
cmocka_unit_test(test_keyframeGetValueNonZeroStartTime),
cmocka_unit_test(test_keyframeGetValueBeforeFirstKeyframeIsNaN),
cmocka_unit_test(
test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue
),
cmocka_unit_test(test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue),
cmocka_unit_test(test_keyframeGetValueNullKeyframesAsserts),
cmocka_unit_test(test_keyframeGetValueZeroCountAsserts),
cmocka_unit_test(test_keyframeGetValueNegativeTimeAsserts),
cmocka_unit_test(test_keyframeGetValueUnsortedKeyframesAsserts),
cmocka_unit_test(test_keyframeGetValueLaterKeyframeOutOfOrderAsserts),
cmocka_unit_test(test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-9
View File
@@ -1,9 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_inventory.c)
-433
View File
@@ -1,433 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/item/inventory.h"
#include "util/memory.h"
static itemid_t ITEM_ID_POTION;
static itemid_t ITEM_ID_POTATO;
static itemid_t ITEM_ID_APPLE;
// Registers three test items (POTION as MEDICINE, POTATO/APPLE as FOOD)
// directly into the global ITEM registry -- these tests exercise the
// inventory/backpack layer only, not the JSON-driven itemInit() pipeline.
static int_t itemFixtureSetup(void **state) {
memoryZero(&ITEM, sizeof(ITEM));
ITEM.typeCount = ITEM_TYPE_NULL + 1;
ITEM.defCount = ITEM_ID_NULL + 1;
const itemtype_t medicine = itemTypeGetOrCreate("MEDICINE");
const itemtype_t food = itemTypeGetOrCreate("FOOD");
ITEM_ID_POTION = ITEM.defCount++;
ITEM.defs[ITEM_ID_POTION].type = medicine;
ITEM_ID_POTATO = ITEM.defCount++;
ITEM.defs[ITEM_ID_POTATO].type = food;
ITEM_ID_APPLE = ITEM.defCount++;
ITEM.defs[ITEM_ID_APPLE].type = food;
return 0;
}
static void test_inventoryInit(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Should setup struct
assert_non_null(inventory.storage);
assert_int_equal(inventory.storageSize, 5);
// Should zero the item ids.
for(size_t i = 0; i < inventory.storageSize; i++) {
assert_int_equal(inventory.storage[i].item, ITEM_ID_NULL);
assert_int_equal(inventory.storage[i].quantity, 0);
}
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryInit(NULL, storage, 5));
// Should fail when given NULL storage pointer
expect_assert_failure(inventoryInit(&inventory, NULL, 5));
// Should fail when given zero storage size
expect_assert_failure(inventoryInit(&inventory, storage, 0));
}
static void test_inventoryItemExists(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Initially should not exist
assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
// Add item
inventorySet(&inventory, ITEM_ID_POTION, 3);
assert_true(inventoryItemExists(&inventory, ITEM_ID_POTION));
// Remove item
inventorySet(&inventory, ITEM_ID_POTION, 0);
assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryItemExists(NULL, ITEM_ID_POTION));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
inventory.storageSize = 5;
// Should fail when given ITEM_ID_NULL
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_NULL));
// Should fail if item has zero quantity somehow
inventorySet(&inventory, ITEM_ID_POTION, 3);
inventory.storage[0].quantity = 0;
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
}
static void test_inventorySet(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Set item quantity
inventorySet(&inventory, ITEM_ID_POTION, 4);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].quantity, 4);
// Update item quantity
inventorySet(&inventory, ITEM_ID_POTION, 2);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].quantity, 2);
// Remove item by setting quantity to 0
inventorySet(&inventory, ITEM_ID_POTION, 0);
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
// Setting multiple items should not cause issues
inventorySet(&inventory, ITEM_ID_POTION, 1);
inventorySet(&inventory, ITEM_ID_POTATO, 5);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].quantity, 1);
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
assert_int_equal(inventory.storage[1].quantity, 5);
assert_int_equal(inventory.storage[2].item, ITEM_ID_NULL);
// Setting early item to 0 should shift others down
inventorySet(&inventory, ITEM_ID_POTION, 0);// Item 0 removed
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTATO);
assert_int_equal(inventory.storage[0].quantity, 5);
assert_int_equal(inventory.storage[1].item, ITEM_ID_NULL);
// Setting non-existing item should add it
inventorySet(&inventory, ITEM_ID_POTION, 3);
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[1].quantity, 3);
// Should fail when given NULL inventory pointer
expect_assert_failure(inventorySet(NULL, ITEM_ID_POTION, 3));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
inventory.storageSize = 5;
// Should fail when given ITEM_ID_NULL
expect_assert_failure(inventorySet(&inventory, ITEM_ID_NULL, 3));
}
static void test_inventoryAdd(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Add item quantity
inventoryAdd(&inventory, ITEM_ID_POTION, 4);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].quantity, 4);
// Add more to existing item
inventoryAdd(&inventory, ITEM_ID_POTION, 3);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].quantity, 7);
// Adding item that would overflow should assert
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 250));
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryAdd(NULL, ITEM_ID_POTION, 3));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
inventory.storageSize = 5;
// Should fail when given ITEM_ID_NULL
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_NULL, 3));
}
static void test_inventoryRemove(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Add item and then remove it
inventorySet(&inventory, ITEM_ID_POTION, 5);
inventoryRemove(&inventory, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
// Removing non-existing item should do nothing
inventoryRemove(&inventory, ITEM_ID_POTION);
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryRemove(NULL, ITEM_ID_POTION));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
inventory.storageSize = 5;
// Should fail when given ITEM_ID_NULL
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_NULL));
}
static void test_inventoryGetCount(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Initially should be zero
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
// Add item and check count
inventorySet(&inventory, ITEM_ID_POTION, 4);
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 4);
// Update item quantity and check count
inventorySet(&inventory, ITEM_ID_POTION, 2);
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 2);
// Remove item and check count
inventorySet(&inventory, ITEM_ID_POTION, 0);
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryGetCount(NULL, ITEM_ID_POTION));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
inventory.storageSize = 5;
// Should fail when given ITEM_ID_NULL
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_NULL));
}
static void test_inventoryIsFull(void **state) {
inventorystack_t storage[2];
inventory_t inventory;
inventoryInit(&inventory, storage, 2);
// Initially should not be full
assert_false(inventoryIsFull(&inventory));
// Fill inventory
inventorySet(&inventory, ITEM_ID_POTION, 1);
inventorySet(&inventory, ITEM_ID_POTATO, 1);
assert_true(inventoryIsFull(&inventory));
// Remove one item
inventoryRemove(&inventory, ITEM_ID_POTION);
assert_false(inventoryIsFull(&inventory));
// Add one item back
inventorySet(&inventory, ITEM_ID_POTION, 1);
assert_true(inventoryIsFull(&inventory));
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryIsFull(NULL));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryIsFull(&inventory));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryIsFull(&inventory));
inventory.storageSize = 2;
}
static void test_inventoryItemFull(void **state) {
inventorystack_t storage[2];
inventory_t inventory;
inventoryInit(&inventory, storage, 2);
// Initially should not be full
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
// Add some potions, but not too many.
inventorySet(&inventory, ITEM_ID_POTION, 100);
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
// Fill with potions
inventorySet(&inventory, ITEM_ID_POTION, ITEM_STACK_QUANTITY_MAX);
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
// Add potatoes, should not affect potions
inventorySet(&inventory, ITEM_ID_POTATO, 50);
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTATO));
// Remove some potions
inventorySet(&inventory, ITEM_ID_POTION, 200);
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
// Fill with potatoes
inventorySet(&inventory, ITEM_ID_POTATO, ITEM_STACK_QUANTITY_MAX);
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTATO));
// Should fail when given NULL inventory pointer
expect_assert_failure(inventoryItemFull(NULL, ITEM_ID_POTION));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
inventory.storageSize = 2;
}
static void test_inventorySort(void **state) {
inventorystack_t storage[5];
inventory_t inventory;
inventoryInit(&inventory, storage, 5);
// Add items out of order
inventorySet(&inventory, ITEM_ID_POTATO, 3);
inventorySet(&inventory, ITEM_ID_APPLE, 5);
inventorySet(&inventory, ITEM_ID_POTION, 2);
// Sort by ID
inventorySort(&inventory, INVENTORY_SORT_BY_ID, false);
// Check order
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
assert_int_equal(inventory.storage[2].item, ITEM_ID_APPLE);
// Sort by ID reverse
inventorySort(&inventory, INVENTORY_SORT_BY_ID, true);
assert_int_equal(inventory.storage[0].item, ITEM_ID_APPLE);
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
// Sort by type ascending - POTION (MEDICINE) sorts before POTATO/APPLE
// (both FOOD). The two FOOD items compare equal, so their relative order
// isn't guaranteed (qsort is not stable) - only the type grouping is.
inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, false);
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
assert_true(
(inventory.storage[1].item == ITEM_ID_POTATO &&
inventory.storage[2].item == ITEM_ID_APPLE) ||
(inventory.storage[1].item == ITEM_ID_APPLE &&
inventory.storage[2].item == ITEM_ID_POTATO)
);
// Sort by type reverse - FOOD items first (order unspecified), POTION last.
inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, true);
assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
assert_true(
(inventory.storage[0].item == ITEM_ID_POTATO &&
inventory.storage[1].item == ITEM_ID_APPLE) ||
(inventory.storage[0].item == ITEM_ID_APPLE &&
inventory.storage[1].item == ITEM_ID_POTATO)
);
// Should fail when given NULL inventory pointer
expect_assert_failure(inventorySort(NULL, INVENTORY_SORT_BY_ID, false));
// Should fail when given NULL storage pointer
inventory.storage = NULL;
expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
inventory.storage = storage;
// Should fail when given zero storage size
inventory.storageSize = 0;
expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
inventory.storageSize = 5;
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_inventoryInit),
cmocka_unit_test(test_inventoryItemExists),
cmocka_unit_test(test_inventorySet),
cmocka_unit_test(test_inventoryAdd),
cmocka_unit_test(test_inventoryRemove),
cmocka_unit_test(test_inventoryGetCount),
cmocka_unit_test(test_inventoryIsFull),
cmocka_unit_test(test_inventoryItemFull),
cmocka_unit_test(test_inventorySort),
};
return cmocka_run_group_tests(tests, itemFixtureSetup, NULL);
}
-15
View File
@@ -1,15 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(battle)
add_subdirectory(entity)
add_subdirectory(cutscene)
-9
View File
@@ -1,9 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_battle.c)
-271
View File
@@ -1,271 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/battle/battle.h"
// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup.
#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1)
static battlefighter_t *addFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const uint16_t attack,
const uint16_t defense,
const uint16_t speed,
const uint16_t healthMax
) {
const battlefighterstats_t stats = {
.attack = attack, .defense = defense, .speed = speed
};
return battleAddFighter(team, controller, stats, healthMax, 0);
}
static void test_battleStartEntersOpeningThenPreRound(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 10, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 10, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
assert_int_equal(BATTLE.state, BATTLE_STATE_OPENING);
// OPENING only transitions to PRE_ROUND; it doesn't run PRE_ROUND's logic
// in the same call.
battleUpdate();
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
assert_int_equal(BATTLE.executionCount, 0);
// PRE_ROUND builds the execution order and moves on to selection.
battleUpdate();
assert_int_equal(BATTLE.executionCount, 2);
assert_true(
BATTLE.state == BATTLE_STATE_PLAYER_SELECTION ||
BATTLE.state == BATTLE_STATE_AI_SELECTION
);
}
static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters(
void **state
) {
battleInit();
battlefighter_t *ally1 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 100, 20
);
battlefighter_t *ally2 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 50, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION, positions the cursor
// ally1 is fastest, so it's first to decide.
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
assert_ptr_equal(battleGetCurrentFighter(), ally1);
// A forced move (as a scripted-battle cutscene item would push) marks
// ally2 as already decided, so PLAYER_SELECTION must skip it.
battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id);
battlePlayerAttack(enemy->id);
assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK);
assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id);
// ally2 already had a decision queued, and enemy is AI-controlled, so
// there's nothing left for PLAYER_SELECTION -- it should have moved on.
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
}
static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// ally decides, selection moves to AI
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
battleUpdate();// AI_SELECTION auto-decides for enemy
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK);
assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id);
}
static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles(
void **state
) {
battleInit();
// ally1 is fast enough to kill enemy1 before enemy1 or ally2 act. enemy2
// keeps the battle alive so execution continues to ally2's now-moot
// attack against the already-dead enemy1.
battlefighter_t *ally1 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 100, 20
);
battlefighter_t *ally2 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
);
battlefighter_t *enemy1 = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 50, 10
);
battlefighter_t *enemy2 = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 1, 100
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION (ally1 first, fastest)
assert_ptr_equal(battleGetCurrentFighter(), ally1);
battlePlayerAttack(enemy1->id);
assert_ptr_equal(battleGetCurrentFighter(), ally2);
battlePlayerAttack(enemy1->id);// both allies target enemy1
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
battleUpdate();// AI_SELECTION
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
// Execution order: ally1 (100), enemy1 (50), ally2 (10), enemy2 (1).
battleUpdate();// ally1 kills enemy1
assert_false(battleFighterIsAlive(enemy1));
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);// enemy2 alive
battleUpdate();// enemy1's own (now dead) turn is skipped
battleUpdate();// ally2's attack against the dead enemy1 fizzles silently
assert_int_equal(enemy1->health, 0);// no double-kill, no underflow
battleUpdate();// enemy2 acts
battleUpdate();// all 4 slots processed -> POST_ROUND
assert_int_equal(BATTLE.state, BATTLE_STATE_POST_ROUND);
battleUpdate();// POST_ROUND -> PRE_ROUND, round advances
assert_int_equal(BATTLE.round, 2);
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
}
static void test_battleWinEndsInStateEnded(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 10
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
battleUpdate();// ally kills the only enemy
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_WIN);
}
static void test_battleLossEndsInStateEnded(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 5, 10
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 50, 0, 10, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
battleUpdate();// ally attacks first (irrelevant to outcome)
battleUpdate();// enemy kills the only ally
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_LOSS);
}
static void test_battleFleeEndsInStateEndedImmediately(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerFlee();
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_FLED);
}
static void test_battleFleeUnavailableIsIgnored(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, false);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerFlee();
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE);
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_battleStartEntersOpeningThenPreRound),
cmocka_unit_test(test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters),
cmocka_unit_test(test_battleAiSelectionAutoQueuesUndecidedAiFighters),
cmocka_unit_test(test_battleMovesExecutingResolvesInSpeedOrderAndFizzles),
cmocka_unit_test(test_battleWinEndsInStateEnded),
cmocka_unit_test(test_battleLossEndsInStateEnded),
cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately),
cmocka_unit_test(test_battleFleeUnavailableIsIgnored),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-12
View File
@@ -1,12 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_cutscene.c)
dusktest(test_cutscenesystem.c)
dusktest(test_cutscenecontrol.c)
dusktest(test_cutscenemaparea.c)
-84
View File
@@ -1,84 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/cutscene/cutscene.h"
#include "util/memory.h"
#include "util/string.h"
// ============================================================
// cutsceneRemoveFront
// ============================================================
static cutsceneitem_t MARKER_ITEM(const char_t *name) {
cutsceneitem_t item;
memoryZero(&item, sizeof(item));
item.type = CUTSCENE_ITEM_TYPE_MARKER;
stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1);
return item;
}
static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero(
void **state
) {
cutsceneitem_t items[4] = {
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d")
};
cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 };
cutsceneRemoveFront(&scene, 2);
assert_int_equal(scene.itemCount, 2);
assert_string_equal(scene.items[0].marker.name, "c");
assert_string_equal(scene.items[1].marker.name, "d");
}
static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) {
cutsceneitem_t items[3] = {
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c")
};
cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 };
cutsceneRemoveFront(&scene, 3);
assert_int_equal(scene.itemCount, 0);
}
static void test_cutsceneRemoveFrontZeroIsNoop(void **state) {
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
cutsceneRemoveFront(&scene, 0);
assert_int_equal(scene.itemCount, 2);
assert_string_equal(scene.items[0].marker.name, "a");
assert_string_equal(scene.items[1].marker.name, "b");
}
static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount(
void **state
) {
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
expect_assert_failure(cutsceneRemoveFront(&scene, 3));
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero),
cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene),
cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop),
cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-173
View File
@@ -1,173 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "time/time.h"
#include "util/memory.h"
static void test_cutsceneWaitCompletesAfterItsDuration(void **state) {
cutsceneitem_t item = { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
cutsceneitemdata_t data;
memoryZero(&data, sizeof(data));
cutsceneWaitStart(&item, &data);
TIME.delta = 0.5f;
assert_false(cutsceneWaitUpdate(&item, &data));
TIME.delta = 0.6f;
assert_true(cutsceneWaitUpdate(&item, &data));
}
static void *lastUserData;
static uint8_t callbackCallCount;
static void recordCallback(void *userData) {
lastUserData = userData;
callbackCallCount++;
}
static void test_cutsceneCallbackFiresOnStartWithNullUserData(void **state) {
callbackCallCount = 0;
lastUserData = (void *)0x1;
cutsceneitem_t item =
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback };
cutsceneitemdata_t data;
cutsceneCallbackStart(&item, &data);
assert_int_equal(callbackCallCount, 1);
assert_null(lastUserData);
// Callback items always complete immediately -- the effect already
// happened in Start, not Update.
assert_true(cutsceneCallbackUpdate(&item, &data));
assert_int_equal(callbackCallCount, 1);// Update doesn't fire it again
}
static void test_cutsceneCallbackNullIsNoop(void **state) {
callbackCallCount = 0;
cutsceneitem_t item =
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = NULL };
cutsceneitemdata_t data;
cutsceneCallbackStart(&item, &data);// should not crash
assert_int_equal(callbackCallCount, 0);
}
static void test_cutsceneSetPauseAppliesImmediately(void **state) {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_SET_PAUSE,
.setPause = CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
};
cutsceneitemdata_t data;
cutsceneSetPauseStart(&item, &data);
assert_int_equal(
CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
);
assert_true(cutsceneSetPauseUpdate(&item, &data));
}
static void test_cutsceneConcurrentCompletesOnceAllChildrenDo(void **state) {
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
.concurrent = {
.items = (const cutsceneitem_t[]){
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.2f },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }
},
.count = 2
}
};
cutsceneitemdata_t data;
memoryZero(&data, sizeof(data));
cutsceneConcurrentStart(&item, &data);
TIME.delta = 0.3f;
// Child 0 (0.2s) elapses; child 1 (0.5s) still has 0.2s left.
assert_false(cutsceneConcurrentUpdate(&item, &data));
TIME.delta = 0.3f;
// Child 1 now elapses too.
assert_true(cutsceneConcurrentUpdate(&item, &data));
}
static void test_cutsceneConcurrentDoesNotReUpdateFinishedChildren(
void **state
) {
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
.concurrent = {
.items = (const cutsceneitem_t[]){
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.1f },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 10.0f }
},
.count = 2
}
};
cutsceneitemdata_t data;
memoryZero(&data, sizeof(data));
cutsceneConcurrentStart(&item, &data);
TIME.delta = 0.2f;
cutsceneConcurrentUpdate(&item, &data);// child 0 finishes
// If child 0 were re-updated, its stored wait value would keep dropping
// further below zero -- not observable directly, but the overall result
// must not falsely report done just because child 0 keeps completing.
TIME.delta = 0.2f;
assert_false(cutsceneConcurrentUpdate(&item, &data));
}
static void test_cutsceneConcurrentCannotNest(void **state) {
cutsceneitem_t outer = {
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
.concurrent = {
.items = (const cutsceneitem_t[]){
{
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
.concurrent = {
.items = (const cutsceneitem_t[]){
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
},
.count = 1
}
}
},
.count = 1
}
};
cutsceneitemdata_t data;
expect_assert_failure(cutsceneConcurrentStart(&outer, &data));
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_cutsceneWaitCompletesAfterItsDuration),
cmocka_unit_test(test_cutsceneCallbackFiresOnStartWithNullUserData),
cmocka_unit_test(test_cutsceneCallbackNullIsNoop),
cmocka_unit_test(test_cutsceneSetPauseAppliesImmediately),
cmocka_unit_test(test_cutsceneConcurrentCompletesOnceAllChildrenDo),
cmocka_unit_test(test_cutsceneConcurrentDoesNotReUpdateFinishedChildren),
cmocka_unit_test(test_cutsceneConcurrentCannotNest),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-175
View File
@@ -1,175 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/overworld/maparea.h"
#include "util/memory.h"
static void resetMapAreas(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
}
static void test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated(
void **state
) {
resetMapAreas();
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
.mapAreaAdd = {
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
.callback = mapAreaNoopCallback,
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
}
};
cutsceneitemdata_t data;
cutsceneMapAreaAddStart(&item, &data);
assert_true(cutsceneMapAreaAddUpdate(&item, &data));
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
assert_ptr_equal(MAP_AREAS[id].callback, mapAreaNoopCallback);
assert_int_equal(MAP_AREAS[id].max.x, 5);
}
static void test_cutsceneMapAreaRemoveClearsSlot(void **state) {
resetMapAreas();
uint8_t id = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
.mapAreaRemove = { .areaId = id }
};
cutsceneitemdata_t data;
cutsceneMapAreaRemoveStart(&item, &data);
assert_true(cutsceneMapAreaRemoveUpdate(&item, &data));
assert_null(MAP_AREAS[id].callback);
}
static void test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel(
void **state
) {
resetMapAreas();
cutsceneitem_t addItem = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
.mapAreaAdd = {
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
.callback = mapAreaNoopCallback,
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
}
};
cutsceneitemdata_t data;
cutsceneMapAreaAddStart(&addItem, &data);
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
cutsceneitem_t removeItem = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
.mapAreaRemove = { .areaId = CUTSCENE_REF_AREA_LAST_CREATED }
};
cutsceneMapAreaRemoveStart(&removeItem, &data);
assert_null(MAP_AREAS[id].callback);
}
static void test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges(
void **state
) {
resetMapAreas();
uint8_t id = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
.mapAreaWait = { .areaIds = { id }, .count = 1 }
};
cutsceneitemdata_t data;
cutsceneMapAreaWaitStart(&item, &data);
assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
MAP_AREAS[id].triggerCount++;// simulates the area's callback firing
assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
}
static void test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges(
void **state
) {
resetMapAreas();
uint8_t idA = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
uint8_t idB = mapAreaAdd(
(worldpos_t){ 10, 10, 0 }, (worldpos_t){ 15, 15, 0 },
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
cutsceneitem_t item = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
.mapAreaWait = { .areaIds = { idA, idB }, .count = 2 }
};
cutsceneitemdata_t data;
cutsceneMapAreaWaitStart(&item, &data);
assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
MAP_AREAS[idB].triggerCount++;// only the second watched area fires
assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
}
static void test_cutsceneMapAreaWaitResolvesLastCreatedSentinel(
void **state
) {
resetMapAreas();
cutsceneitem_t addItem = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
.mapAreaAdd = {
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
.callback = mapAreaNoopCallback,
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
}
};
cutsceneitemdata_t data;
cutsceneMapAreaAddStart(&addItem, &data);
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
cutsceneitem_t waitItem = {
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
.mapAreaWait = { .areaIds = { CUTSCENE_REF_AREA_LAST_CREATED }, .count = 1 }
};
cutsceneMapAreaWaitStart(&waitItem, &data);
assert_false(cutsceneMapAreaWaitUpdate(&waitItem, &data));
MAP_AREAS[id].triggerCount++;
assert_true(cutsceneMapAreaWaitUpdate(&waitItem, &data));
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated),
cmocka_unit_test(test_cutsceneMapAreaRemoveClearsSlot),
cmocka_unit_test(test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel),
cmocka_unit_test(test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges),
cmocka_unit_test(
test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges
),
cmocka_unit_test(test_cutsceneMapAreaWaitResolvesLastCreatedSentinel),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-583
View File
@@ -1,583 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "time/time.h"
#include "util/string.h"
static uint8_t callbackFired;
static void recordCallback(void *userData) {
callbackFired++;
}
// INNER is jumped into by OUTER's second item, never returning -- this is
// the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart
// replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect
// fires in Start, not Update, so it fires the same frame the jump happens.
static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback }
};
static cutscene_t CUTSCENE_TEST_INNER = {
.items = CUTSCENE_TEST_INNER_ITEMS,
.itemCount = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f },
{
.type = CUTSCENE_ITEM_TYPE_CUTSCENE,
.cutsceneRef = { .cutscene = &CUTSCENE_TEST_INNER }
}
};
static cutscene_t CUTSCENE_TEST_OUTER = {
.items = CUTSCENE_TEST_OUTER_ITEMS,
.itemCount = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_DEFAULT
};
static void test_cutsceneSystemStartSetsUpInitialItem(void **state) {
cutsceneSystemInit();
cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_DEFAULT);
}
static void test_cutsceneSystemNestedCutsceneIsOneWayJump(void **state) {
cutsceneSystemInit();
callbackFired = 0;
cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
TIME.delta = 0.1f;
cutsceneSystemUpdate();// wait not elapsed yet
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
assert_int_equal(callbackFired, 0);
// The wait elapses, advancing to the nested-cutscene item, which jumps
// straight into INNER and starts its first item (the callback) -- all
// within this single update call.
TIME.delta = 1.0f;
cutsceneSystemUpdate();
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INNER);
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
assert_int_equal(callbackFired, 1);
// INNER's only item (the callback) always reports complete -- one more
// update ends the whole cutscene.
cutsceneSystemUpdate();
assert_null(CUTSCENE_SYSTEM.scene);
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
}
static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) {
cutsceneSystemInit();
cutsceneSystemUpdate();// should not crash
assert_null(CUTSCENE_SYSTEM.scene);
}
static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
};
static cutscene_t CUTSCENE_TEST_SINGLE_WAIT = {
.items = CUTSCENE_TEST_SINGLE_WAIT_ITEMS,
.itemCount = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
static void test_cutsceneSystemStartWithSetsInteractEntities(void **state) {
cutsceneSystemInit();
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
cutsceneSystemStartCutsceneWith(
&CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[1]
);
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[0]);
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[1]);
assert_ptr_equal(
cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT), &ENTITIES[0]
);
assert_ptr_equal(
cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED), &ENTITIES[1]
);
TIME.delta = 2.0f;
cutsceneSystemUpdate();// ends the cutscene
assert_null(CUTSCENE_SYSTEM.refs.entityInteract);// reset on end
assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
}
static void test_cutsceneSystemGetEntitySentinelsRequireBeingSet(
void **state
) {
cutsceneSystemInit();
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT));
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED));
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_CREATED));
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF));
}
static void test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef(
void **state
) {
cutsceneSystemInit();
entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
entity_t *resolved = cutsceneSystemGetEntity(2);
assert_ptr_equal(resolved, &ENTITIES[2]);
// Resolving by direct index also updates LAST_REF.
assert_ptr_equal(
cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF), &ENTITIES[2]
);
}
static void test_cutsceneSystemGetAreaId(void **state) {
cutsceneSystemInit();
// cutsceneSystemInit() zero-inits the field -- only actually starting a
// cutscene sets it to the "nothing created yet" sentinel.
CUTSCENE_SYSTEM.refs.areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED));
assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through
CUTSCENE_SYSTEM.refs.areaLastCreated = 3;
assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED), 3);
}
static void test_cutsceneSystemGetTextMiniId(void **state) {
cutsceneSystemInit();
CUTSCENE_SYSTEM.refs.textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
expect_assert_failure(
cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED)
);
assert_int_equal(cutsceneSystemGetTextMiniId(4), 4);
CUTSCENE_SYSTEM.refs.textMiniLastCreated = 1;
assert_int_equal(
cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED), 1
);
}
static void test_cutsceneSystemDisposeResetsState(void **state) {
cutsceneSystemInit();
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
cutsceneSystemStartCutsceneWith(
&CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[0]
);
cutsceneSystemDispose();
assert_null(CUTSCENE_SYSTEM.scene);
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
assert_null(CUTSCENE_SYSTEM.refs.entityInteract);
assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
}
// --- loadedFile survival across cutsceneSystemPrepare -------------------
//
// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp
// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from
// - cutsceneSystemGoTo needs that to survive so it can reload the same source
// later. cutsceneCutsceneResolve's caller stamps it BEFORE calling
// cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so
// cutsceneSystemPrepare must not blindly clear loadedFile on every
// start - only when switching away from loadedScene to some other
// cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This
// regression-tests the exact bug hit via initial.jsonc's CUTSCENE item
// into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then
// the immediately following cutsceneSystemStartCutscene call wiped it
// straight back out, so a later cutsceneSystemGoTo on the still-running
// cutscene had nothing to reload from.
static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene(
void **state
) {
cutsceneSystemInit();
// Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems
// and stamp loadedFile with its source, before the cutscene has
// actually started running.
CUTSCENE_SYSTEM.loadedItems[0] =
(cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
CUTSCENE_SYSTEM.loadedScene = (cutscene_t){
.items = CUTSCENE_SYSTEM.loadedItems,
.itemCount = 1,
.itemsMax = CUTSCENE_LOADED_ITEMS_MAX,
.pause = CUTSCENE_PAUSE_NONE
};
stringCopy(
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc",
ASSET_FILE_NAME_MAX - 1
);
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
assert_string_equal(
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc"
);
// Starting a genuinely different cutscene, though, must still
// invalidate it - there's nothing left to reload it from.
cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT);
assert_string_equal(CUTSCENE_SYSTEM.loadedFile, "");
}
// --- CUTSCENE_INSERT ---------------------------------------------------
//
// INSERT now splices its target's items directly into the running
// scene's own array (see cutsceneSystemInsertCutscene) rather than
// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
// cutscene throughout every test below. cutsceneSystemNext pops each
// already-run item off the front of the array as it advances (see
// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only
// what's left to run at any given point, not a running total of
// everything ever spliced in - the itemCount assertions below are sized
// accordingly. Since the running scene's own array is what gets appended
// into, any outer cutscene used with INSERT must still declare its items
// array (and .itemsMax) with enough spare capacity for whatever ends up
// spliced into it at once, including transitively through any nested
// INSERT.
static uint8_t insertOrderLog[8];
static uint8_t insertOrderLogCount;
static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; }
static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; }
static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; }
static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; }
static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; }
static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f },
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
};
static cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = {
.items = CUTSCENE_TEST_INSERT_SNIPPET_ITEMS,
.itemCount =
sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax =
sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
// 5 items to start, +3 spare slots for CUTSCENE_TEST_INSERT_SNIPPET's items.
static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[8] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f },
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 },
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_SNIPPET }
},
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
};
static cutscene_t CUTSCENE_TEST_INSERT_OUTER = {
.items = CUTSCENE_TEST_INSERT_OUTER_ITEMS,
.itemCount = 5,
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_OUTER_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
insertOrderLogCount = 0;
cutsceneSystemInit();
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER);
TIME.delta = 1.5f;// elapses the outer WAIT(1.0)
cutsceneSystemUpdate();// order0 done -> starts the outer WAIT
cutsceneSystemUpdate();// the WAIT elapses -> starts order1
cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed.
// order0/the WAIT/order1/the INSERT item itself have all since been
// popped off the front, leaving the snippet's 3 items (orderA now
// running, its WAIT, orderB) plus order2.
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4);
TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0)
cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB
cutsceneSystemUpdate();// orderB done -> continues on to order2
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
cutsceneSystemUpdate();// order2 done -> cutscene ends naturally
assert_null(CUTSCENE_SYSTEM.scene);
const uint8_t expected[] = { 0, 1, 3, 4, 2 };
assert_int_equal(insertOrderLogCount, sizeof(expected));
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
}
static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
};
static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = {
.items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS,
.itemCount = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
sizeof(cutsceneitem_t),
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
// 2 items to start, +1 spare slot for the snippet's single item.
static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[3] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = {
.cutscene = &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET
}
}
};
static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER = {
.items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS,
.itemCount = 2,
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS) /
sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
void **state
) {
insertOrderLogCount = 0;
cutsceneSystemInit();
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
// order0 and the INSERT item itself have both been popped off the
// front by now, leaving just orderA (currently running).
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
// orderA is the snippet's only item, and the snippet was spliced in as
// the outer's last item -- finishing it should reach the natural end
// of the whole cutscene, not get stuck.
cutsceneSystemUpdate();
assert_null(CUTSCENE_SYSTEM.scene);
const uint8_t expected[] = { 0, 3 };
assert_int_equal(insertOrderLogCount, sizeof(expected));
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
}
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
};
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = {
.items = CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS,
.itemCount =
sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax =
sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
// INSERT is MID's first item, so entering MID (from OUTER) cascades
// straight through into INNER within the same Start chain, one level
// deeper than a single insert. MID is only ever read from as a splice
// source (see CUTSCENE_TEST_INSERT_NESTED_OUTER) - it's never itself the
// running scene, so it needs no spare capacity of its own.
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = {
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER }
},
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
};
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = {
.items = CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS,
.itemCount =
sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
.itemsMax =
sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
// 3 items to start, +2 spare for MID's items, +1 more for MID's own
// nested INSERT(INNER) once MID's items become part of this array too.
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[6] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_MID }
},
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
};
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_OUTER = {
.items = CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS,
.itemCount = 3,
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS) /
sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE
};
static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
insertOrderLogCount = 0;
cutsceneSystemInit();
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER);
// order0 done -> starts INSERT(MID), cascading through INSERT(INNER) ->
// orderA, all within this one Update call's Start chain - both splices
// land in OUTER's own array.
cutsceneSystemUpdate();
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
// order0 and both INSERT items themselves have all been popped off the
// front by now, leaving orderA (currently running), orderB, order2.
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
cutsceneSystemUpdate();// orderA done -> continues on to orderB
cutsceneSystemUpdate();// orderB done -> continues on to order2
cutsceneSystemUpdate();// order2 done -> ends naturally
assert_null(CUTSCENE_SYSTEM.scene);
const uint8_t expected[] = { 0, 3, 4, 2 };
assert_int_equal(insertOrderLogCount, sizeof(expected));
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
}
static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
};
static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET = {
.items = CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS,
.itemCount = 1,
.itemsMax = 1,
.pause = CUTSCENE_PAUSE_NONE
};
// No spare capacity at all - itemsMax equals itemCount, leaving no room
// to splice anything into it.
static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS[1] = {
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET }
}
};
static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER = {
.items = CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS,
.itemCount = 1,
.itemsMax = 1,
.pause = CUTSCENE_PAUSE_NONE
};
static void test_cutsceneInsertAssertsWhenExceedingItemsMax(void **state) {
cutsceneSystemInit();
expect_assert_failure(
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER)
);
}
static void noopInsertCallback(void *userData) {}
static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback }
};
static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = {
.items = CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS,
.itemCount = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
sizeof(cutsceneitem_t),
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_ALL
};
// 1 item to start, +1 spare slot for the snippet's single item.
static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[2] = {
{
.type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET }
}
};
static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_OUTER = {
.items = CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS,
.itemCount = 1,
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS) /
sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NPC
};
static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities(
void **state
) {
cutsceneSystemInit();
entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER);
entityInit(&ENTITIES[4], ENTITY_TYPE_NPC);
cutsceneSystemStartCutsceneWith(
&CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4]
);
// The INSERT item's Start already spliced the snippet into place and
// cascaded into it by now - and since the INSERT item itself has since
// been popped off the front, only the snippet's one item is left.
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER);
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[3]);
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[4]);
// The snippet declares ALL, but INSERT doesn't apply it -- the outer
// cutscene's NPC pause, already in effect, stands.
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC);
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem),
cmocka_unit_test(test_cutsceneSystemNestedCutsceneIsOneWayJump),
cmocka_unit_test(test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene),
cmocka_unit_test(test_cutsceneSystemStartWithSetsInteractEntities),
cmocka_unit_test(test_cutsceneSystemGetEntitySentinelsRequireBeingSet),
cmocka_unit_test(test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef),
cmocka_unit_test(test_cutsceneSystemGetAreaId),
cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
cmocka_unit_test(
test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
),
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
cmocka_unit_test(test_cutsceneInsertAssertsWhenExceedingItemsMax),
cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-19
View File
@@ -1,19 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_entity.c)
target_sources(test_entity PRIVATE entitytestfixture.c)
dusktest(test_npc.c)
target_sources(test_npc PRIVATE entitytestfixture.c)
dusktest(test_entityinteract.c)
target_sources(test_entityinteract PRIVATE entitytestfixture.c)
dusktest(test_entityitem.c)
target_sources(test_entityitem PRIVATE entitytestfixture.c)
-31
View File
@@ -1,31 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitytestfixture.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "util/memory.h"
#include "util/string.h"
void entityTestFixtureReset(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(&MAP, sizeof(map_t));
stringCopy(MAP.name, "test", MAP_NAME_MAX);
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
chunk_t *chunk = &MAP.chunks[0];
chunk->position = (chunkpos_t){ 0, 0, 0 };
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
}
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
chunk->entities[i] = 0xFF;
}
MAP.chunkOrder[0] = chunk;
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entity.h"
/**
* Resets ENTITIES[] to all-empty and builds a single minimal, synchronously
* "loaded" chunk at chunk position (0, 0, 0) on the MAP global, entirely
* TILE_SHAPE_GROUND at local Z 0. Sufficient for entityWalk/Turn/Run, chunk
* bookkeeping, and NPC movement tests -- does not exercise the real async
* chunk-loading pipeline (mapChunkLoad).
*
* World positions with x/y in [0, CHUNK_WIDTH-1]/[0, CHUNK_HEIGHT-1] and
* z == 0 fall within the fixture chunk. Individual tests may override
* specific columns afterward (e.g. to place a ramp).
*/
void entityTestFixtureReset(void);
-300
View File
@@ -1,300 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "entitytestfixture.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/worldpos.h"
static void setTile(
const worldpos_t pos, const tileshape_t shape, const uint8_t z
) {
chunktileindex_t index = worldPosToChunkTileIndex(&pos);
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = z };
}
static void test_entityInit(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[3], ENTITY_TYPE_ITEM);
assert_int_equal(ENTITIES[3].id, 3);
assert_int_equal(ENTITIES[3].type, ENTITY_TYPE_ITEM);
assert_int_equal(ENTITIES[3].chunkIndex, 0xFF);
assert_int_equal(ENTITIES[3].globalId, ENTITY_GLOBAL_ID_NULL);
// entityItemInit wires the interact callback -- confirms init dispatch ran.
assert_int_equal(ENTITIES[3].interact.type, ENTITY_INTERACT_CALLBACK);
expect_assert_failure(entityInit(NULL, ENTITY_TYPE_ITEM));
expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_NULL));
expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_COUNT));
entity_t offBounds;
expect_assert_failure(entityInit(&offBounds, ENTITY_TYPE_ITEM));
}
static void test_entityGetAvailable(void **state) {
entityTestFixtureReset();
assert_int_equal(entityGetAvailable(), 0);
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
assert_int_equal(entityGetAvailable(), 1);
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
assert_int_equal(entityGetAvailable(), 2);
}
static void test_entityGetAt(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 3, 4, 0 };
assert_ptr_equal(entityGetAt((worldpos_t){ 3, 4, 0 }), &ENTITIES[0]);
assert_null(entityGetAt((worldpos_t){ 3, 4, 1 }));
assert_null(entityGetAt((worldpos_t){ 0, 0, 0 }));
}
static void test_entityGetByGlobalId(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
ENTITIES[2].globalId = 42;
assert_ptr_equal(entityGetByGlobalId(42), &ENTITIES[2]);
assert_null(entityGetByGlobalId(43));
assert_null(entityGetByGlobalId(ENTITY_GLOBAL_ID_NULL));
}
static void test_entityCanUnload(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START - 1;
assert_true(entityCanUnload(&ENTITIES[0]));
ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START;
assert_false(entityCanUnload(&ENTITIES[0]));
}
static void test_entityCanTurnWalkRun(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
assert_true(entityCanTurn(&ENTITIES[0]));
assert_true(entityCanWalk(&ENTITIES[0]));
assert_true(entityCanRun(&ENTITIES[0]));
ENTITIES[0].animation = ENTITY_ANIM_WALK;
assert_false(entityCanTurn(&ENTITIES[0]));
assert_false(entityCanWalk(&ENTITIES[0]));
assert_false(entityCanRun(&ENTITIES[0]));
ENTITIES[0].animation = ENTITY_ANIM_IDLE;
ENTITIES[0].walkEndCooldown = 0.1f;
assert_false(entityCanTurn(&ENTITIES[0]));// cooldown blocks turning only
assert_true(entityCanWalk(&ENTITIES[0]));
}
static void test_entityTurn(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
entityTurn(&ENTITIES[0], ENTITY_DIR_EAST);
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_EAST);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_TURN);
// Can't turn again mid-turn.
ENTITIES[0].direction = ENTITY_DIR_NORTH;
entityTurn(&ENTITIES[0], ENTITY_DIR_WEST);
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
}
static void test_entityWalkOnOpenGround(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
assert_int_equal(ENTITIES[0].position.x, 5);
assert_int_equal(ENTITIES[0].position.y, 6);
assert_int_equal(ENTITIES[0].position.z, 0);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_WALK);
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
// entityUpdateChunk should have assigned it to the fixture chunk.
assert_int_equal(ENTITIES[0].chunkIndex, 0);
assert_int_equal(MAP.chunks[0].entities[0], 0);
}
static void test_entityWalkBlockedAtChunkEdge(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 0, 0, 0 };
// West/south of the loaded chunk is unloaded (TILE_NULL) -- blocked.
entityWalk(&ENTITIES[0], ENTITY_DIR_WEST);
assert_int_equal(ENTITIES[0].position.x, 0);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
}
static void test_entityWalkBlockedByOtherEntity(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
ENTITIES[1].position = (worldpos_t){ 5, 6, 0 };// directly north
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
assert_int_equal(ENTITIES[0].position.x, 5);
assert_int_equal(ENTITIES[0].position.y, 5);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
}
static void test_entityWalkCannotWhileNotIdle(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
ENTITIES[0].animation = ENTITY_ANIM_WALK;
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
assert_int_equal(ENTITIES[0].position.y, 5);// unchanged
}
// Ramp at (5,5) facing north, raised GROUND at (5,6)/z1 -- walking north
// off the ramp raises the entity by one Z layer, and walking back south
// from the raised tile falls back down onto the ramp.
static void setUpRamp(void) {
setTile((worldpos_t){ 5, 5, 0 }, TILE_SHAPE_RAMP_NORTH, 0);
setTile((worldpos_t){ 5, 6, 0 }, TILE_SHAPE_GROUND, 1);
}
static void test_entityWalkUpRamp(void **state) {
entityTestFixtureReset();
setUpRamp();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
assert_int_equal(ENTITIES[0].position.x, 5);
assert_int_equal(ENTITIES[0].position.y, 6);
assert_int_equal(ENTITIES[0].position.z, 1);
}
static void test_entityWalkFallDownRamp(void **state) {
entityTestFixtureReset();
setUpRamp();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 6, 1 };
entityWalk(&ENTITIES[0], ENTITY_DIR_SOUTH);
assert_int_equal(ENTITIES[0].position.x, 5);
assert_int_equal(ENTITIES[0].position.y, 5);
assert_int_equal(ENTITIES[0].position.z, 0);
}
static void test_entityWalkCannotClimbRampFromTheSide(void **state) {
entityTestFixtureReset();
setUpRamp();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
// Only NORTH climbs this ramp -- EAST off the ramp tile is just blocked
// ground movement, not a climb.
entityWalk(&ENTITIES[0], ENTITY_DIR_EAST);
assert_int_equal(ENTITIES[0].position.x, 6);
assert_int_equal(ENTITIES[0].position.z, 0);
}
static void test_entityRun(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
entityRun(&ENTITIES[0], ENTITY_DIR_NORTH);
assert_int_equal(ENTITIES[0].position.y, 6);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_RUN);
}
static void test_entitySetChunkAndUpdateChunk(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
entitySetChunk(&ENTITIES[0], 0);
assert_int_equal(ENTITIES[0].chunkIndex, 0);
assert_int_equal(MAP.chunks[0].entities[0], 0);
entitySetChunk(&ENTITIES[0], 0xFF);
assert_int_equal(ENTITIES[0].chunkIndex, 0xFF);
assert_int_equal(MAP.chunks[0].entities[0], 0xFF);
ENTITIES[0].position = (worldpos_t){ 3, 3, 0 };
entityUpdateChunk(&ENTITIES[0]);
assert_int_equal(ENTITIES[0].chunkIndex, 0);
}
static void test_entityPositionSet(void **state) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].animation = ENTITY_ANIM_WALK;
ENTITIES[0].walkEndCooldown = 5;
entityPositionSet(&ENTITIES[0], (worldpos_t){ 7, 8, 0 });
assert_int_equal(ENTITIES[0].position.x, 7);
assert_int_equal(ENTITIES[0].position.y, 8);
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
assert_int_equal(ENTITIES[0].walkEndCooldown, 0);
assert_int_equal(ENTITIES[0].chunkIndex, 0);// updated as a side effect
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityInit),
cmocka_unit_test(test_entityGetAvailable),
cmocka_unit_test(test_entityGetAt),
cmocka_unit_test(test_entityGetByGlobalId),
cmocka_unit_test(test_entityCanUnload),
cmocka_unit_test(test_entityCanTurnWalkRun),
cmocka_unit_test(test_entityTurn),
cmocka_unit_test(test_entityWalkOnOpenGround),
cmocka_unit_test(test_entityWalkBlockedAtChunkEdge),
cmocka_unit_test(test_entityWalkBlockedByOtherEntity),
cmocka_unit_test(test_entityWalkCannotWhileNotIdle),
cmocka_unit_test(test_entityWalkUpRamp),
cmocka_unit_test(test_entityWalkFallDownRamp),
cmocka_unit_test(test_entityWalkCannotClimbRampFromTheSide),
cmocka_unit_test(test_entityRun),
cmocka_unit_test(test_entitySetChunkAndUpdateChunk),
cmocka_unit_test(test_entityPositionSet),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-135
View File
@@ -1,135 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "entitytestfixture.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/focus/uifocus.h"
#include "rpg/cutscene/cutscenesystem.h"
static cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
};
static cutscene_t CUTSCENE_TEST_INTERACT = {
.items = CUTSCENE_TEST_INTERACT_ITEMS,
.itemCount = 1,
.itemsMax = 1,
.pause = CUTSCENE_PAUSE_NONE
};
static entity_t *player;
static entity_t *target;
static void resetInteractFixture(void) {
entityTestFixtureReset();
uiFocusInit();
uiTextboxMainInit();
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
cutsceneSystemInit();
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
player = &ENTITIES[0];
target = &ENTITIES[1];
player->direction = ENTITY_DIR_NORTH;
}
static void test_entityInteractWithNull(void **state) {
resetInteractFixture();
target->interact.type = ENTITY_INTERACT_NULL;
entityInteractWith(player, target);// should not crash or change anything
assert_int_equal(target->interact.type, ENTITY_INTERACT_NULL);
}
static void test_entityInteractWithPrintTurnsNpcToFacePlayer(void **state) {
resetInteractFixture();
target->interact.type = ENTITY_INTERACT_PRINT;
target->interact.data.message = "Hello!";
entityInteractWith(player, target);
assert_true(uiTextboxMainIsActive());
assert_int_equal(
target->data.npc.interactState, NPC_INTERACT_STATE_CONVERSING
);
// NPC turns to face the opposite of the player's facing direction.
assert_int_equal(target->direction, entityDirGetOpposite(player->direction));
}
static entity_t *callbackPlayerArg;
static entity_t *callbackTargetArg;
static uint8_t callbackCount;
static void recordInteractCallback(entity_t *p, entity_t *t) {
callbackPlayerArg = p;
callbackTargetArg = t;
callbackCount++;
}
static void test_entityInteractWithCallback(void **state) {
resetInteractFixture();
callbackCount = 0;
target->interact.type = ENTITY_INTERACT_CALLBACK;
target->interact.data.callback = recordInteractCallback;
entityInteractWith(player, target);
assert_int_equal(callbackCount, 1);
assert_ptr_equal(callbackPlayerArg, player);
assert_ptr_equal(callbackTargetArg, target);
}
static void test_entityInteractWithCallbackRequiresNonNull(void **state) {
resetInteractFixture();
target->interact.type = ENTITY_INTERACT_CALLBACK;
target->interact.data.callback = NULL;
expect_assert_failure(entityInteractWith(player, target));
}
static void test_entityInteractWithCutsceneStartsIt(void **state) {
resetInteractFixture();
target->interact.type = ENTITY_INTERACT_CUTSCENE;
target->interact.data.cutscene = &CUTSCENE_TEST_INTERACT;
entityInteractWith(player, target);
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INTERACT);
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, player);
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, target);
}
static void test_entityInteractWithRequiresNonNullEntities(void **state) {
resetInteractFixture();
expect_assert_failure(entityInteractWith(NULL, target));
expect_assert_failure(entityInteractWith(player, NULL));
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityInteractWithNull),
cmocka_unit_test(test_entityInteractWithPrintTurnsNpcToFacePlayer),
cmocka_unit_test(test_entityInteractWithCallback),
cmocka_unit_test(test_entityInteractWithCallbackRequiresNonNull),
cmocka_unit_test(test_entityInteractWithCutsceneStartsIt),
cmocka_unit_test(test_entityInteractWithRequiresNonNullEntities),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-85
View File
@@ -1,85 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "entitytestfixture.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/focus/uifocus.h"
// An arbitrary, non-null itemid_t -- entityItemSet just stores/reports it
// verbatim, it doesn't need a real item registered in ITEM.
#define TEST_ITEM_ID ((itemid_t)1)
static entity_t *setUpItemEntity(void) {
entityTestFixtureReset();
uiFocusInit();
uiTextboxMainInit();
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
entityInit(&ENTITIES[0], ENTITY_TYPE_ITEM);
return &ENTITIES[0];
}
static void test_entityItemInit(void **state) {
entity_t *item = setUpItemEntity();
assert_int_equal(item->interact.type, ENTITY_INTERACT_CALLBACK);
assert_ptr_equal(item->interact.data.callback, entityItemInteract);
}
static void test_entityItemSet(void **state) {
entity_t *item = setUpItemEntity();
entityItemSet(item, TEST_ITEM_ID, 3);
assert_int_equal(item->data.item.item, TEST_ITEM_ID);
assert_int_equal(item->data.item.quantity, 3);
assert_false(item->data.item.collected);
}
// entityItemInteract itself is NOT covered here: it unconditionally calls
// itemGive(), which calls itemGetName(), which dereferences
// LOCALE.entry->data.locale -- a real locale asset populated only by the
// async asset-loading pipeline (see localemanager.h/assetlocaleloader.h).
// That's exactly the asset-loading dependency this test pass scoped out;
// faking it cheaply isn't possible without hand-building the loader's
// internal hash format, which is more coupling than it's worth. Backpack
// bookkeeping itself is already covered by test/item/test_inventory.c-style
// tests at the inventory/backpack layer.
static void test_entityItemMovementWaitsForCollectionAndTextbox(void **state) {
entity_t *item = setUpItemEntity();
// Not collected yet -- stays.
entityItemMovement(item);
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
// Collected (set directly -- see note above on why entityItemInteract
// itself isn't driven here), but a message is still showing -- stays.
item->data.item.collected = true;
uiTextboxMainSetText("Picked up!");
entityItemMovement(item);
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
// Once the textbox is dismissed, the entity despawns.
uiFocusPop();
assert_false(uiTextboxMainIsActive());
entityItemMovement(item);
assert_int_equal(item->type, ENTITY_TYPE_NULL);
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityItemInit),
cmocka_unit_test(test_entityItemSet),
cmocka_unit_test(test_entityItemMovementWaitsForCollectionAndTextbox),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-155
View File
@@ -1,155 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "entitytestfixture.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "time/time.h"
static entity_t *setUpNpc(const worldpos_t position) {
entityTestFixtureReset();
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
ENTITIES[0].position = position;
return &ENTITIES[0];
}
static void test_npcSetMoveType(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
assert_int_equal(npc->data.npc.moveType, NPC_MOVE_TYPE_RANDOM_TURN);
// Init ran: timer was seeded within the default frequency range.
assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
}
static void test_npcRandomTurnMovementFiresOnceTimerElapses(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
npc->data.npc.moveData.randomTurn.timer = 0.05f;
TIME.delta = 0.01f;
npcRandomTurnMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// timer not elapsed yet
TIME.delta = 1.0f;
npcRandomTurnMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_TURN);
// Timer is reseeded, not left at/below zero.
assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
}
static void test_npcRandomWalkMovementFiresOnceTimerElapses(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
npc->data.npc.moveData.randomWalk.timer = 0.05f;
TIME.delta = 0.01f;
npcRandomWalkMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
TIME.delta = 1.0f;
npcRandomWalkMovement(npc);
// Open ground on all sides -- whichever direction is chosen, it moves.
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
}
static void test_npcRandomTurnAndWalkMovementRunsBothIndependently(
void **state
) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK);
npcrandomturnandwalk_t *tw = &npc->data.npc.moveData.randomTurnAndWalk;
tw->turn.timer = 999.0f;// don't fire this tick
tw->walk.timer = 0.05f;// fires this tick
TIME.delta = 1.0f;
npcRandomTurnAndWalkMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
assert_true(tw->turn.timer > 900.0f);// unaffected by the walk firing
}
static void test_npcMovementGatedByCutscenePause(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
npc->data.npc.moveData.randomWalk.timer = 0.0f;
TIME.delta = 1.0f;
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NPC;
npcMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked by pause
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
npcMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
}
static void test_npcMovementGatedByInteractState(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
npc->data.npc.moveData.randomWalk.timer = 0.0f;
TIME.delta = 1.0f;
npc->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
npcMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked while conversing
npc->data.npc.interactState = NPC_INTERACT_STATE_NONE;
npcMovement(npc);
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
}
static void test_npcPathMovementFollowsAndLoopsWaypoints(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
npcpath_t *path = &npc->data.npc.moveData.path;
npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 6, 0 });
npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 5, 0 });
npcPathMovement(npc);// steps toward waypoint 0
assert_int_equal(npc->position.x, 5);
assert_int_equal(npc->position.y, 6);
assert_int_equal(path->index, 0);// not yet considered "arrived"
npc->animation = ENTITY_ANIM_IDLE;// simulate the walk animation finishing
npcPathMovement(npc);// arrives at waypoint 0, advances, steps toward wp 1
assert_int_equal(path->index, 1);
assert_int_equal(npc->position.x, 5);
assert_int_equal(npc->position.y, 5);
}
static void test_npcPathMovementNoopWithEmptyPath(void **state) {
entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
npcPathMovement(npc);
assert_int_equal(npc->position.x, 5);
assert_int_equal(npc->position.y, 5);
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_npcSetMoveType),
cmocka_unit_test(test_npcRandomTurnMovementFiresOnceTimerElapses),
cmocka_unit_test(test_npcRandomWalkMovementFiresOnceTimerElapses),
cmocka_unit_test(test_npcRandomTurnAndWalkMovementRunsBothIndependently),
cmocka_unit_test(test_npcMovementGatedByCutscenePause),
cmocka_unit_test(test_npcMovementGatedByInteractState),
cmocka_unit_test(test_npcPathMovementFollowsAndLoopsWaypoints),
cmocka_unit_test(test_npcPathMovementNoopWithEmptyPath),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-11
View File
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_maparea.c)
# Subdirs
-217
View File
@@ -1,217 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/overworld/maparea.h"
#include "rpg/overworld/map.h"
#include "rpg/entity/entity.h"
#include "util/memory.h"
static uint8_t callbackCount;
static uint8_t lastTrigger;
static void recordCallback(entity_t *entity, const uint8_t trigger) {
callbackCount++;
lastTrigger = trigger;
}
static void resetMapAreas(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
callbackCount = 0;
lastTrigger = 0;
}
static void test_mapAreaInit(void **state) {
maparea_t area;
const worldpos_t min = { 5, 5, 0 };
const worldpos_t max = { 0, 0, 0 };
// min/max should be normalized regardless of argument order.
mapAreaInit(
&area, min, max, recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
assert_int_equal(area.min.x, 0);
assert_int_equal(area.min.y, 0);
assert_int_equal(area.max.x, 5);
assert_int_equal(area.max.y, 5);
assert_int_equal(area.triggerCount, 0);
expect_assert_failure(
mapAreaInit(&area, min, max, NULL, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL)
);
}
static void test_mapAreaIsInside(void **state) {
maparea_t area;
mapAreaInit(
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
assert_true(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 0 }));
assert_true(mapAreaIsInside(&area, (worldpos_t){ 0, 0, 0 }));// inclusive min
assert_true(
mapAreaIsInside(&area, (worldpos_t){ 10, 10, 0 })
);// inclusive max
assert_false(mapAreaIsInside(&area, (worldpos_t){ 11, 5, 0 }));
assert_false(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 1 }));
}
static void test_mapAreaShouldNotify(void **state) {
maparea_t area;
mapAreaInit(
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
recordCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
);
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
entityInit(&ENTITIES[2], ENTITY_TYPE_ITEM);
assert_true(mapAreaShouldNotify(&area, &ENTITIES[0]));
assert_false(mapAreaShouldNotify(&area, &ENTITIES[1]));// not notified
assert_false(mapAreaShouldNotify(&area, &ENTITIES[2]));// item never notifies
area.notify = MAP_AREA_NOTIFY_ALL;
assert_true(mapAreaShouldNotify(&area, &ENTITIES[1]));
}
static void test_mapAreaAddAndRemove(void **state) {
resetMapAreas();
uint8_t id = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
assert_int_equal(id, 0);
assert_ptr_equal(MAP_AREAS[0].callback, recordCallback);
// Adding again should reuse the next free slot, not the same one.
uint8_t id2 = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
assert_int_equal(id2, 1);
mapAreaRemove(id);
assert_null(MAP_AREAS[0].callback);// slot freed
// Removing frees the slot for reuse.
uint8_t id3 = mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
assert_int_equal(id3, 0);
}
static void test_mapAreaCheckEntityTriggersEnterStepExit(void **state) {
resetMapAreas();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };// outside
// Outside -> outside: no callback.
mapAreaCheckEntity(&ENTITIES[0]);
assert_int_equal(callbackCount, 0);
assert_int_equal(MAP_AREAS[0].triggerCount, 0);
// Outside -> inside: ENTER.
ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&ENTITIES[0]);
assert_int_equal(callbackCount, 1);
assert_int_equal(lastTrigger, MAP_TRIGGER_ENTER);
assert_int_equal(MAP_AREAS[0].triggerCount, 1);
// Inside -> inside: STEP, every subsequent call.
mapAreaCheckEntity(&ENTITIES[0]);
assert_int_equal(callbackCount, 2);
assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
mapAreaCheckEntity(&ENTITIES[0]);
assert_int_equal(callbackCount, 3);
assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
// Inside -> outside: EXIT.
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&ENTITIES[0]);
assert_int_equal(callbackCount, 4);
assert_int_equal(lastTrigger, MAP_TRIGGER_EXIT);
}
static void test_mapAreaCheckEntityRespectsTriggerMask(void **state) {
resetMapAreas();
// Only interested in ENTER, not STEP or EXIT.
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ENTER
);
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&ENTITIES[0]);// ENTER: fires
assert_int_equal(callbackCount, 1);
mapAreaCheckEntity(&ENTITIES[0]);// STEP: masked out
assert_int_equal(callbackCount, 1);
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&ENTITIES[0]);// EXIT: masked out
assert_int_equal(callbackCount, 1);
}
static void test_mapAreaCanUnload(void **state) {
resetMapAreas();
memoryZero(&MAP, sizeof(map_t));
maparea_t area;
mapAreaInit(
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
// No chunk overlaps the area's bounds (all chunks default to position 0,
// 0, 0 after memoryZero -- move them far away first).
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
}
assert_true(mapAreaCanUnload(&area));
// One chunk overlapping the area's bounds blocks unload.
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
assert_false(mapAreaCanUnload(&area));
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_mapAreaInit),
cmocka_unit_test(test_mapAreaIsInside),
cmocka_unit_test(test_mapAreaShouldNotify),
cmocka_unit_test(test_mapAreaAddAndRemove),
cmocka_unit_test(test_mapAreaCheckEntityTriggersEnterStepExit),
cmocka_unit_test(test_mapAreaCheckEntityRespectsTriggerMask),
cmocka_unit_test(test_mapAreaCanUnload),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-16
View File
@@ -1,16 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
// Add RPG tests here in the future
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-51
View File
@@ -10,9 +10,6 @@
#include "savetestfixture.h"
#include "assert/assert.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "util/memory.h"
#include "util/string.h"
#include <ftw.h>
@@ -25,51 +22,6 @@ static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON =
"{\"members\":{\"hero\":{"
"\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0,"
"\"healthMax\":30,\"mpMax\":10"
"}}}";
static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) {
loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) {
errorOk();
}
void saveTestFixtureStubPartyAsset(void) {
memoryZero(&ASSET, sizeof(ASSET));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) {
SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read(
SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0
);
assertNotNull(
SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture"
);
}
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync =
saveTestFixturePartyJsonLoadSyncStub;
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose =
saveTestFixturePartyJsonDisposeStub;
}
void saveTestFixtureUnstubPartyAsset(void) {
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return;
yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC);
SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
}
static int_t saveTestFixtureRemoveEntry(
const char *path,
const struct stat *statBuffer,
@@ -97,8 +49,6 @@ int saveTestFixtureSetup(void **state) {
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
saveTestFixtureStubPartyAsset();
errorret_t ret = saveInit();
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
@@ -106,7 +56,6 @@ int saveTestFixtureSetup(void **state) {
}
int saveTestFixtureTeardown(void **state) {
saveTestFixtureUnstubPartyAsset();
nftw(
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
-17
View File
@@ -36,20 +36,3 @@ int saveTestFixtureTeardown(void **state);
*/
void saveTestFixtureBlockSaveDirectory(void);
/**
* Stubs the JSON asset loader's sync callback to hand back a small
* in-memory party.jsonc-shaped doc instead of doing a real file/zip read -
* saveInit()/saveSlotInit() seed the party roster via partyInit(), which
* loads party.jsonc through the real asset system, and there's no real
* asset bundle available under the test harness. No thread, no real I/O.
* Resets ASSET to a fresh, otherwise-empty state each call, so it's safe
* to call once per test. Called automatically by saveTestFixtureSetup.
*/
void saveTestFixtureStubPartyAsset(void);
/**
* Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe
* to call even if that function was never called. Called automatically
* by saveTestFixtureTeardown.
*/
void saveTestFixtureUnstubPartyAsset(void);
+2 -38
View File
@@ -15,19 +15,6 @@
#include <stdlib.h>
#include <string.h>
// saveSlotInit() seeds the party roster via partyInit(), which loads
// party.jsonc through the real asset system - stub it out (see
// savetestfixture.h) rather than needing a real asset bundle here.
static int assetGroupSetup(void **state) {
saveTestFixtureStubPartyAsset();
return 0;
}
static int assetGroupTeardown(void **state) {
saveTestFixtureUnstubPartyAsset();
return 0;
}
// ============================================================
// Helpers - wrap the writeInit/readInit macros (which can only be used
// inside a function returning errorret_t) so tests can call them plainly.
@@ -156,12 +143,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 42;
// An in-use slot (non-empty name) requires a non-empty map name too -
// see saveSlotNewGame/requireStrMin("mapName", 1) in saveSlotVer1ReadJSON.
stringCopy(
SAVE.slot.cachedData.mapName, "overworld",
sizeof(SAVE.slot.cachedData.mapName)
);
char_t *json;
size_t len;
@@ -178,7 +159,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
assert_true(stringEquals(read.cachedData.name, "Hero"));
assert_int_equal(read.cachedData.playerLevel, 42);
assert_true(stringEquals(read.cachedData.mapName, "overworld"));
assert_true(read.cachedData.time.time == writtenTime.time);
// version/dataType are struct-only bookkeeping, never serialized - the
@@ -199,33 +179,18 @@ static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) {
// No "name" key (or an empty one) means a blank/never-used slot (see
// saveSlotInUse) - the reader returns right after reading that, without
// needing or validating time/mapName/partyMembers/partyOrder at all, so
// needing or validating time at all, so
// this succeeds despite omitting fields that would otherwise be required.
errorret_t ret = slotFromJSON(&slot, "{\"version\":1}");
assert_true(errorIsOk(ret));
assert_int_equal(slot.cachedData.name[0], '\0');
assert_int_equal(slot.cachedData.mapName[0], '\0');
// Not readInt32's own default of 1 - the early return happens before
// playerLevel is ever read, leaving the zero-init saveSlotCurrentReadJSON
// seeds ver1 with (see the memoryZero in saveSlotCurrentReadJSON).
assert_int_equal(slot.cachedData.playerLevel, 0);
}
static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) {
saveslot_t slot;
// "name" is present (in use) but "mapName" is missing - a real save
// (via saveSlotNewGame) always has both, so this is corrupt.
errorret_t ret = slotFromJSON(
&slot,
"{\"version\":1,\"name\":\"Hero\","
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
}
static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
saveslot_t slot;
@@ -302,7 +267,6 @@ int main(void) {
cmocka_unit_test(test_saveSlotReadJSON_nullAsserts),
cmocka_unit_test(test_saveSlotReadJSON_roundTrip),
cmocka_unit_test(test_saveSlotReadJSON_blankSlot_shortCircuits),
cmocka_unit_test(test_saveSlotReadJSON_inUseMissingMapName_errors),
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
@@ -310,5 +274,5 @@ int main(void) {
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
};
return cmocka_run_group_tests(tests, assetGroupSetup, assetGroupTeardown);
return cmocka_run_group_tests(tests, NULL, NULL);
}