/** * 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); }