diff --git a/assets/items.jsonc b/assets/items.jsonc new file mode 100644 index 00000000..05f02667 --- /dev/null +++ b/assets/items.jsonc @@ -0,0 +1,7 @@ +{ + "items": { + "potion": { "type": "MEDICINE" }, + "potato": { "type": "FOOD" }, + "apple": { "type": "FOOD" } + } +} diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 2c17ccb5..f1420c4a 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -47,8 +47,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(audioInit()); - errorChain(uiInit()); errorChain(rpgInit()); + errorChain(uiInit()); #ifdef DUSK_NETWORK errorChain(networkInit()); #endif diff --git a/src/dusk/rpg/item/CMakeLists.txt b/src/dusk/rpg/item/CMakeLists.txt index cbd4416d..29e79fb0 100644 --- a/src/dusk/rpg/item/CMakeLists.txt +++ b/src/dusk/rpg/item/CMakeLists.txt @@ -10,13 +10,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} inventory.c backpack.c itemgive.c -) - -# Item Definitions -dusk_run_python( - dusk_item_json_defs - tools.item - --json ${CMAKE_CURRENT_SOURCE_DIR}/item.json - --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h -) -add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs) \ No newline at end of file +) \ No newline at end of file diff --git a/src/dusk/rpg/item/backpack.c b/src/dusk/rpg/item/backpack.c index 03bd91ae..b668d0dd 100644 --- a/src/dusk/rpg/item/backpack.c +++ b/src/dusk/rpg/item/backpack.c @@ -1,6 +1,6 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -11,61 +11,49 @@ backpack_t BACKPACK; void backpackInit() { - for(uint8_t i = 0; i < ITEM_TYPE_COUNT; i++) { + for(itemtype_t i = ITEM_TYPE_NULL + 1; i < ITEM.typeCount; i++) { inventoryInit( &BACKPACK.inventories[i], BACKPACK.storage[i], - ITEM_TYPE_COUNT_MAX + ITEM_DEF_COUNT_MAX ); } } inventory_t *backpackGetInventory(const itemtype_t type) { assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); + assertTrue(type < ITEM.typeCount, "Item type out of range"); return &BACKPACK.inventories[type]; } void backpackAdd(const itemid_t item, const uint8_t quantity) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity); + inventoryAdd(backpackGetInventory(itemGetTypeById(item)), item, quantity); } void backpackRemove(const itemid_t item) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - inventoryRemove(backpackGetInventory(ITEMS[item].type), item); + inventoryRemove(backpackGetInventory(itemGetTypeById(item)), item); } void backpackSet(const itemid_t item, const uint8_t quantity) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity); + inventorySet(backpackGetInventory(itemGetTypeById(item)), item, quantity); } uint8_t backpackGetCount(const itemid_t item) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item); + return inventoryGetCount(backpackGetInventory(itemGetTypeById(item)), item); } bool_t backpackItemExists(const itemid_t item) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item); + return inventoryItemExists(backpackGetInventory(itemGetTypeById(item)), item); } bool_t backpackIsFull(const itemtype_t type) { assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); + assertTrue(type < ITEM.typeCount, "Item type out of range"); return inventoryIsFull(backpackGetInventory(type)); } bool_t backpackItemFull(const itemid_t item) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item); + return inventoryItemFull(backpackGetInventory(itemGetTypeById(item)), item); } void backpackSort( @@ -74,6 +62,6 @@ void backpackSort( const bool_t reverse ) { assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range"); + assertTrue(type < ITEM.typeCount, "Item type out of range"); inventorySort(backpackGetInventory(type), sortBy, reverse); -} \ No newline at end of file +} diff --git a/src/dusk/rpg/item/backpack.h b/src/dusk/rpg/item/backpack.h index 5812befa..a8ab4419 100644 --- a/src/dusk/rpg/item/backpack.h +++ b/src/dusk/rpg/item/backpack.h @@ -9,8 +9,8 @@ #include "inventory.h" typedef struct { - inventorystack_t storage[ITEM_TYPE_COUNT][ITEM_TYPE_COUNT_MAX]; - inventory_t inventories[ITEM_TYPE_COUNT]; + inventorystack_t storage[ITEM_TYPE_DEF_COUNT_MAX][ITEM_DEF_COUNT_MAX]; + inventory_t inventories[ITEM_TYPE_DEF_COUNT_MAX]; } backpack_t; extern backpack_t BACKPACK; diff --git a/src/dusk/rpg/item/inventory.c b/src/dusk/rpg/item/inventory.c index b3579ccb..909b7296 100644 --- a/src/dusk/rpg/item/inventory.c +++ b/src/dusk/rpg/item/inventory.c @@ -188,8 +188,8 @@ int_t inventorySortByIdReverse(const void *a, const void *b) { int_t inventorySortByType(const void *a, const void *b) { const inventorystack_t *stackA = (const inventorystack_t*)a; const inventorystack_t *stackB = (const inventorystack_t*)b; - const itemtype_t typeA = ITEMS[stackA->item].type; - const itemtype_t typeB = ITEMS[stackB->item].type; + const itemtype_t typeA = itemGetTypeById(stackA->item); + const itemtype_t typeB = itemGetTypeById(stackB->item); if(typeA < typeB) return -1; if(typeA > typeB) return 1; return 0; @@ -198,8 +198,8 @@ int_t inventorySortByType(const void *a, const void *b) { int_t inventorySortByTypeReverse(const void *a, const void *b) { const inventorystack_t *stackA = (const inventorystack_t*)a; const inventorystack_t *stackB = (const inventorystack_t*)b; - const itemtype_t typeA = ITEMS[stackA->item].type; - const itemtype_t typeB = ITEMS[stackB->item].type; + const itemtype_t typeA = itemGetTypeById(stackA->item); + const itemtype_t typeB = itemGetTypeById(stackB->item); if(typeA < typeB) return 1; if(typeA > typeB) return -1; return 0; diff --git a/src/dusk/rpg/item/item.c b/src/dusk/rpg/item/item.c index 010b6845..937e4302 100644 --- a/src/dusk/rpg/item/item.c +++ b/src/dusk/rpg/item/item.c @@ -8,17 +8,90 @@ #include "item.h" #include "assert/assert.h" #include "locale/localemanager.h" +#include "asset/asset.h" #include "asset/loader/locale/assetlocaleloader.h" +#include "util/memory.h" +#include "util/string.h" +#include "yyjson.h" + +item_t ITEM; + +itemtype_t itemTypeGetOrCreate(const char_t *id) { + for(itemtype_t type = ITEM_TYPE_NULL + 1; type < ITEM.typeCount; type++) { + if(stringEquals(ITEM.types[type].id, id)) return type; + } + + assertTrue( + ITEM.typeCount < ITEM_TYPE_DEF_COUNT_MAX, "Too many item types defined." + ); + itemtype_t type = ITEM.typeCount++; + stringCopy(ITEM.types[type].id, id, ITEM_TYPE_NAME_MAX); + return type; +} + +errorret_t itemInit(void) { + memoryZero(&ITEM, sizeof(ITEM)); + ITEM.typeCount = ITEM_TYPE_NULL + 1; + ITEM.defCount = ITEM_ID_NULL + 1; + + assetentry_t *entry = assetLock("items.jsonc", ASSET_LOADER_TYPE_JSON, NULL); + errorret_t ret = assetRequireLoaded(entry); + if(errorIsNotOk(ret)) { + assetUnlockEntry(entry); + errorChain(ret); + } + + yyjson_val *root = yyjson_doc_get_root(entry->data.json); + yyjson_val *items = yyjson_obj_get(root, "items"); + + yyjson_obj_iter iter = yyjson_obj_iter_with(items); + yyjson_val *key; + while((key = yyjson_obj_iter_next(&iter))) { + assertTrue(ITEM.defCount < ITEM_DEF_COUNT_MAX, "Too many item ids defined."); + + yyjson_val *val = yyjson_obj_iter_get_val(key); + const char_t *typeId = yyjson_get_str(yyjson_obj_get(val, "type")); + + itemid_t id = ITEM.defCount++; + stringCopy(ITEM.defs[id].id, yyjson_get_str(key), ITEM_ID_STRING_MAX); + ITEM.defs[id].type = itemTypeGetOrCreate(typeId); + } + + assetUnlockEntry(entry); + errorOk(); +} + +const itemdef_t *itemGetDefById(const itemid_t id) { + assertTrue(id > ITEM_ID_NULL, "Item ID must not be null"); + assertTrue(id < ITEM.defCount, "Item ID out of range"); + + return &ITEM.defs[id]; +} + +itemtype_t itemGetTypeById(const itemid_t id) { + return itemGetDefById(id)->type; +} + +itemid_t itemGetIdByName(const char_t *name) { + assertNotNull(name, "name must not be NULL"); + + for(itemid_t id = ITEM_ID_NULL + 1; id < ITEM.defCount; id++) { + if(stringEquals(ITEM.defs[id].id, name)) return id; + } + + return ITEM_ID_NULL; +} errorret_t itemGetName( const itemid_t item, char_t *buffer, const size_t bufferSize ) { - assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); + const itemdef_t *def = itemGetDefById(item); - errorChain(localeManagerGetString(ITEMS[item].name, buffer, bufferSize, 0)); + char_t key[ITEM_ID_STRING_MAX + 16]; + stringFormat(key, sizeof(key), "item.%s.name", def->id); + errorChain(localeManagerGetString(key, buffer, bufferSize, 0)); errorOk(); } diff --git a/src/dusk/rpg/item/item.h b/src/dusk/rpg/item/item.h index 1c1beda5..ccb42841 100644 --- a/src/dusk/rpg/item/item.h +++ b/src/dusk/rpg/item/item.h @@ -7,7 +7,74 @@ #pragma once #include "error/error.h" -#include "rpg/item/itemdef.h" + +#define ITEM_TYPE_NAME_MAX 12 +#define ITEM_TYPE_DEF_COUNT_MAX 32 +#define ITEM_ID_STRING_MAX 12 +#define ITEM_DEF_COUNT_MAX 64 + +typedef uint8_t itemtype_t; +typedef uint16_t itemid_t; +#define ITEM_TYPE_NULL ((itemtype_t)0) +#define ITEM_ID_NULL ((itemid_t)0) + +typedef struct { + char_t id[ITEM_TYPE_NAME_MAX]; +} itemtypedef_t; + +typedef struct { + itemtype_t type; + char_t id[ITEM_ID_STRING_MAX]; +} itemdef_t; + +typedef struct { + itemtypedef_t types[ITEM_TYPE_DEF_COUNT_MAX]; + itemtype_t typeCount; + itemdef_t defs[ITEM_DEF_COUNT_MAX]; + itemid_t defCount; +} item_t; + +extern item_t ITEM; + +/** + * Initializes the item system. + * + * @return Any error that occurs. + */ +errorret_t itemInit(void); + +/** + * Finds the itemtype_t matching id among the types registered so far, + * registering a new one if this is the first time it's been seen. + * + * @param id The type's identifier string (see itemtypedef_t.id). + * @return The itemtype_t for id. + */ +itemtype_t itemTypeGetOrCreate(const char_t *id); + +/** + * Gets the itemdef_t for an item id. + * + * @param id The item ID to look up. Must not be ITEM_ID_NULL. + * @return The itemdef_t for id. + */ +const itemdef_t *itemGetDefById(const itemid_t id); + +/** + * Gets the itemtype_t for an item id. + * + * @param id The item ID to look up. Must not be ITEM_ID_NULL. + * @return The itemtype_t for id. + */ +itemtype_t itemGetTypeById(const itemid_t id); + +/** + * Finds the itemid_t whose id string matches name. + * + * @param name The item's identifier string (see itemdef_t.id). + * @return The matching itemid_t, or ITEM_ID_NULL if none matched. + */ +itemid_t itemGetIdByName(const char_t *name); /** * Gets the localized display name for an item. diff --git a/src/dusk/rpg/item/item.json b/src/dusk/rpg/item/item.json deleted file mode 100644 index 35aceb4d..00000000 --- a/src/dusk/rpg/item/item.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { "id": "POTION", "type": "MEDICINE", "weight": 1.0, "name": "potion" }, - { "id": "POTATO", "type": "FOOD", "weight": 0.5, "name": "potato" }, - { "id": "APPLE", "type": "FOOD", "weight": 0.3, "name": "apple" } -] diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 9b0791d5..d2056e9b 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -10,6 +10,7 @@ #include "rpg/overworld/map.h" #include "rpg/overworld/maparea.h" #include "rpg/cutscene/cutscenesystem.h" +#include "rpg/item/item.h" #include "rpg/item/backpack.h" #include "rpg/battle/party.h" #include "save/save.h" @@ -27,6 +28,7 @@ errorret_t rpgInit(void) { memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); + errorChain(itemInit()); backpackInit(); partyInit(); cutsceneSystemInit(); @@ -45,9 +47,9 @@ errorret_t rpgInit(void) { RPG_CAMERA.followEntity.followEntityId = ent->id; // Starting inventory. - backpackAdd(ITEM_ID_POTION, 5); - backpackAdd(ITEM_ID_POTATO, 3); - backpackAdd(ITEM_ID_APPLE, 8); + backpackAdd(itemGetIdByName("potion"), 5); + backpackAdd(itemGetIdByName("potato"), 3); + backpackAdd(itemGetIdByName("apple"), 8); // All Good! diff --git a/src/dusk/ui/screen/backpack/uibackpack.c b/src/dusk/ui/screen/backpack/uibackpack.c index cc37e27a..269b8f0d 100644 --- a/src/dusk/ui/screen/backpack/uibackpack.c +++ b/src/dusk/ui/screen/backpack/uibackpack.c @@ -54,7 +54,8 @@ errorret_t uiBackpackInit(void) { &UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs, uiBackpackTabSelected, NULL, uiBackpackTabChanged ); - for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) { + const uint8_t tabCount = ITEM.typeCount - 1; + for(uint8_t i = 0; i < tabCount; i++) { stringFormat( UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1, UI_BACKPACK.categoryFormat, i + 1 diff --git a/src/dusk/ui/screen/backpack/uibackpack.h b/src/dusk/ui/screen/backpack/uibackpack.h index 46088313..f48c076d 100644 --- a/src/dusk/ui/screen/backpack/uibackpack.h +++ b/src/dusk/ui/screen/backpack/uibackpack.h @@ -11,7 +11,7 @@ #include "ui/widget/uiitemlist.h" #include "rpg/item/item.h" -#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1) +#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_DEF_COUNT_MAX - 1) #define UI_BACKPACK_TAB_LABEL_MAX 32 #define UI_BACKPACK_CATEGORY_FORMAT_MAX 32 #define UI_BACKPACK_ITEM_LIST_COLUMNS 4 diff --git a/test/item/test_inventory.c b/test/item/test_inventory.c index bfe0d848..75d13302 100644 --- a/test/item/test_inventory.c +++ b/test/item/test_inventory.c @@ -7,9 +7,37 @@ #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; @@ -401,5 +429,5 @@ int main(int argc, char** argv) { cmocka_unit_test(test_inventorySort), }; - return cmocka_run_group_tests(tests, NULL, NULL); + return cmocka_run_group_tests(tests, itemFixtureSetup, NULL); } \ No newline at end of file diff --git a/test/rpg/entity/test_entityitem.c b/test/rpg/entity/test_entityitem.c index 13b5ecbd..f4936ec2 100644 --- a/test/rpg/entity/test_entityitem.c +++ b/test/rpg/entity/test_entityitem.c @@ -10,6 +10,10 @@ #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(); @@ -32,8 +36,8 @@ static void test_entityItemSet(void **state) { entity_t *item = setUpItemEntity(); - entityItemSet(item, ITEM_ID_POTION, 3); - assert_int_equal(item->data.item.item, ITEM_ID_POTION); + 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); } diff --git a/tools/item.py b/tools/item.py deleted file mode 100644 index f5d8b9c7..00000000 --- a/tools/item.py +++ /dev/null @@ -1,114 +0,0 @@ -import argparse -import json -import os - -parser = argparse.ArgumentParser(description="Item JSON to .h defines") -parser.add_argument("--json", required=True, help="Path to item JSON file") -parser.add_argument("--output", required=True, help="Path to output .h file") -args = parser.parse_args() - -def type_enum(name): - return "ITEM_TYPE_" + name.upper() - -def id_enum(name): - return "ITEM_ID_" + name.upper() - -# Load JSON -item_ids = [] -item_types = [] -rows = {} - -with open(args.json, encoding="utf-8") as f: - entries = json.load(f) - -if not all( - "id" in row and "type" in row and "name" in row for row in entries -): - raise ValueError("Each item must have 'id', 'type', and 'name' fields") - -for row in entries: - item_id, item_type = row["id"], row["type"] - if item_id not in item_ids: - item_ids.append(item_id) - if item_type not in item_types: - item_types.append(item_type) - rows[item_id] = row - -# Assign enum values: types and IDs each start from 1 with NULL = 0. -type_values = {} -type_count = 1 -for t in item_types: - type_values[t] = type_count - type_count += 1 - -id_values = {} -id_count = 1 -for i in item_ids: - id_values[i] = id_count - id_count += 1 - -# Count items per type -type_item_counts = { t: 0 for t in item_types } -for i in item_ids: - type_item_counts[rows[i]["type"]] += 1 - -# Build output -out = [ - "#pragma once", - '#include "dusk.h"', - "", - "typedef enum {", - " ITEM_TYPE_NULL = 0,", -] -for t in item_types: - out.append(f" {type_enum(t)} = {type_values[t]},") -out += [ - f" ITEM_TYPE_COUNT = {type_count}", - "} itemtype_t;", - "", - "typedef enum {", - " ITEM_ID_NULL = 0,", -] -for i in item_ids: - out.append(f" {id_enum(i)} = {id_values[i]},") -out += [ - f" ITEM_ID_COUNT = {id_count}", - "} itemid_t;", - "", - "typedef struct {", - " itemid_t id;", - " itemtype_t type;", - " const char_t *name;", - "} item_t;", - "", - "static const item_t ITEMS[] = {", -] -for i in item_ids: - row = rows[i] - out += [ - f" [{id_enum(i)}] = {{", - f" .id = {id_enum(i)},", - f" .type = {type_enum(row['type'])},", - f" .name = \"item.{row['name']}.name\",", - " },", - ] -out += [ - "};", - "", - "static const uint8_t ITEM_TYPE_COUNTS[] = {", -] -for t in item_types: - out.append(f" [{type_enum(t)}] = {type_item_counts[t]},") -out += [ - "};", - "", -] -max_type_count = max(type_item_counts.values()) if type_item_counts else 0 -out += [ - f"#define ITEM_TYPE_COUNT_MAX {max_type_count}", - "", -] - -os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as f: - f.write("\n".join(out))