Replaces the build-time item.json/tools.item.py code generator with a single item_t ITEM registry populated at runtime by parsing assets/items.jsonc, so items/types can grow without a rebuild. Adds itemGetDefById/itemGetTypeById/itemGetIdByName/itemTypeGetOrCreate, updates inventory/backpack to size off the new *_DEF_COUNT_MAX caps and look up types dynamically, and reorders engine init so rpgInit (which loads items) runs before uiInit (which needs the item type count to size the backpack's tabs). Co-Authored-By: Claude Sonnet 5 <[email protected]>
433 lines
14 KiB
C
433 lines
14 KiB
C
/**
|
|
* 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);
|
|
} |