Switch item definitions to a JSON-driven runtime registry

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]>
This commit is contained in:
2026-09-13 11:36:50 -05:00
co-authored by Claude Sonnet 5
parent 804f8aa1fa
commit 109ddc9e1e
15 changed files with 216 additions and 174 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"items": {
"potion": { "type": "MEDICINE" },
"potato": { "type": "FOOD" },
"apple": { "type": "FOOD" }
}
}
+1 -1
View File
@@ -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
-9
View File
@@ -11,12 +11,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
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)
+11 -23
View File
@@ -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);
}
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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;
+76 -3
View File
@@ -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();
}
+68 -1
View File
@@ -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.
-5
View File
@@ -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" }
]
+5 -3
View File
@@ -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!
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
+29 -1
View File
@@ -7,6 +7,34 @@
#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) {
@@ -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);
}
+6 -2
View File
@@ -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);
}
-114
View File
@@ -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))