diff --git a/assets/cube.json b/assets/cube.json new file mode 100644 index 00000000..d6534384 --- /dev/null +++ b/assets/cube.json @@ -0,0 +1,15 @@ +{ + "components": { + "position": { + "position": [0.0, 0.0, 0.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0] + }, + "renderable": { + "type": "shader_material", + "priority": 0, + "shaderType": 1, + "stateFlags": 2 + } + } +} diff --git a/assets/entity.json b/assets/entity.json new file mode 100644 index 00000000..fc08bc9c --- /dev/null +++ b/assets/entity.json @@ -0,0 +1,32 @@ +{ + "components": { + "position": { + "position": [0.0, 0.0, 0.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0] + }, + "camera": { + "projType": "perspective", + "nearClip": 0.1, + "farClip": 5000.0, + "fov": 45.0 + }, + "renderable": { + "type": "shader_material", + "priority": 0, + "shaderType": 1, + "stateFlags": 2 + }, + "physics": { + "bodyType": "dynamic", + "shapeType": "cube", + "halfExtents": [0.5, 0.5, 0.5], + "velocity": [0.0, 0.0, 0.0], + "gravityScale": 1.0 + }, + "trigger": { + "min": [-0.5, -0.5, -0.5], + "max": [0.5, 0.5, 0.5] + } + } +} diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index a6242fb7..eb930f86 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -1,6 +1,7 @@ # Find link platform-specific libraries find_package(SDL2 REQUIRED) find_package(OpenGL REQUIRED) +find_package(LIBZIP REQUIRED) # find_package(CURL REQUIRED) # Setup endianess at compile time to optimize. @@ -23,6 +24,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC OpenGL::GL GL m + libzip::zip # CURL::libcurl ) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 52221cc1..6ab0c549 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -56,4 +56,6 @@ add_subdirectory(network) add_subdirectory(overworld) add_subdirectory(save) add_subdirectory(util) -add_subdirectory(thread) \ No newline at end of file +add_subdirectory(thread) +add_subdirectory(asset) +add_subdirectory(scene) \ No newline at end of file diff --git a/src/dusk/asset/CMakeLists.txt b/src/dusk/asset/CMakeLists.txt new file mode 100644 index 00000000..79180ae2 --- /dev/null +++ b/src/dusk/asset/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + asset.c +) + +add_subdirectory(loader) diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c new file mode 100644 index 00000000..a9e4d897 --- /dev/null +++ b/src/dusk/asset/asset.c @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "asset.h" +#include "util/memory.h" +#include "assert/assert.h" + +assetmanager_t ASSET_MANAGER; + +errorret_t assetInit(void) { + assertIsMainThread("assetInit must be on main thread"); + memoryZero(&ASSET_MANAGER, sizeof(assetmanager_t)); + + int_t zipErr = 0; + ASSET_MANAGER.archive = zip_open( + ASSET_FILE_NAME, ZIP_RDONLY, &zipErr + ); + if(!ASSET_MANAGER.archive) { + errorThrow( + "Failed to open %s: error %d", ASSET_FILE_NAME, zipErr + ); + } + + threadMutexInit(&ASSET_MANAGER.mutex); + threadInit(&ASSET_MANAGER.loaderThread, assetLoaderThread); + threadStart(&ASSET_MANAGER.loaderThread); + + errorOk(); +} + +void assetUpdate(void) { + assertIsMainThread("assetUpdate must be on main thread"); + threadMutexLock(&ASSET_MANAGER.mutex); + for(uint8_t i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { + assetentry_t *e = &ASSET_MANAGER.entries[i]; + if(e->state != ASSET_ENTRY_STATE_SYNC_PENDING) continue; + if(ASSET_LOADER_CALLBACKS[e->type].loadSync) { + threadMutexUnlock(&ASSET_MANAGER.mutex); + ASSET_LOADER_CALLBACKS[e->type].loadSync(e); + threadMutexLock(&ASSET_MANAGER.mutex); + } + if(e->state == ASSET_ENTRY_STATE_SYNC_PENDING) { + e->state = ASSET_ENTRY_STATE_LOADED; + } + threadMutexSignal(&ASSET_MANAGER.mutex); + } + threadMutexUnlock(&ASSET_MANAGER.mutex); +} + +errorret_t assetDispose(void) { + assertIsMainThread("assetDispose must be on main thread"); + + threadStopRequest(&ASSET_MANAGER.loaderThread); + threadMutexLock(&ASSET_MANAGER.mutex); + threadMutexSignal(&ASSET_MANAGER.mutex); + threadMutexUnlock(&ASSET_MANAGER.mutex); + threadStop(&ASSET_MANAGER.loaderThread); + + for(uint8_t i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { + assetentry_t *e = &ASSET_MANAGER.entries[i]; + if(e->state == ASSET_ENTRY_STATE_IDLE) continue; + if(!ASSET_LOADER_CALLBACKS[e->type].dispose) continue; + ASSET_LOADER_CALLBACKS[e->type].dispose(e); + } + + if(ASSET_MANAGER.archive) { + zip_close(ASSET_MANAGER.archive); + ASSET_MANAGER.archive = NULL; + } + + threadMutexDispose(&ASSET_MANAGER.mutex); + + errorOk(); +} + +assetentry_t *assetGetEntry( + const char_t *path, + const assetloadertype_t type, + const assetloaderinput_t *input +) { + assertNotNull(path, "Path cannot be null"); + + threadMutexLock(&ASSET_MANAGER.mutex); + + for(uint8_t i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { + assetentry_t *e = &ASSET_MANAGER.entries[i]; + if(e->state == ASSET_ENTRY_STATE_IDLE) continue; + if(e->type != type) continue; + if(strncmp(e->path, path, ASSET_PATH_MAX) != 0) continue; + threadMutexUnlock(&ASSET_MANAGER.mutex); + return e; + } + + assetentry_t *entry = NULL; + for(uint8_t i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { + if(ASSET_MANAGER.entries[i].state != ASSET_ENTRY_STATE_IDLE) { + continue; + } + entry = &ASSET_MANAGER.entries[i]; + break; + } + + if(!entry) { + threadMutexUnlock(&ASSET_MANAGER.mutex); + return NULL; + } + + memoryZero(entry, sizeof(assetentry_t)); + strncpy(entry->path, path, ASSET_PATH_MAX - 1); + entry->path[ASSET_PATH_MAX - 1] = '\0'; + entry->type = type; + if(input) entry->input = *input; + entry->state = ASSET_ENTRY_STATE_QUEUED; + + threadMutexSignal(&ASSET_MANAGER.mutex); + threadMutexUnlock(&ASSET_MANAGER.mutex); + return entry; +} + +errorret_t assetRequireLoaded(assetentry_t *entry) { + assertIsMainThread("assetRequireLoaded must be on main thread"); + assertNotNull(entry, "Entry cannot be null"); + + threadMutexLock(&ASSET_MANAGER.mutex); + while( + entry->state != ASSET_ENTRY_STATE_LOADED && + entry->state != ASSET_ENTRY_STATE_ERROR + ) { + if(entry->state == ASSET_ENTRY_STATE_SYNC_PENDING) { + if(ASSET_LOADER_CALLBACKS[entry->type].loadSync) { + threadMutexUnlock(&ASSET_MANAGER.mutex); + ASSET_LOADER_CALLBACKS[entry->type].loadSync(entry); + threadMutexLock(&ASSET_MANAGER.mutex); + } + if(entry->state == ASSET_ENTRY_STATE_SYNC_PENDING) { + entry->state = ASSET_ENTRY_STATE_LOADED; + } + threadMutexSignal(&ASSET_MANAGER.mutex); + break; + } + threadMutexWaitLock(&ASSET_MANAGER.mutex); + } + bool_t ok = (entry->state == ASSET_ENTRY_STATE_LOADED); + threadMutexUnlock(&ASSET_MANAGER.mutex); + + if(!ok) errorThrow("Asset failed to load: %s", entry->path); + errorOk(); +} + +void assetLock(assetentry_t *entry) { + assertNotNull(entry, "Entry cannot be null"); + threadMutexLock(&ASSET_MANAGER.mutex); + entry->refCount++; + threadMutexUnlock(&ASSET_MANAGER.mutex); +} + +void assetUnlock(assetentry_t *entry) { + assertNotNull(entry, "Entry cannot be null"); + threadMutexLock(&ASSET_MANAGER.mutex); + if(entry->refCount > 0) entry->refCount--; + threadMutexUnlock(&ASSET_MANAGER.mutex); +} + +void assetLoaderThread(thread_t *thread) { + threadMutexLock(&ASSET_MANAGER.mutex); + while(!threadShouldStop(thread)) { + assetentry_t *entry = NULL; + for(uint8_t i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { + assetentry_t *e = &ASSET_MANAGER.entries[i]; + if(e->state != ASSET_ENTRY_STATE_QUEUED) continue; + entry = e; + break; + } + + if(!entry) { + threadMutexWaitLock(&ASSET_MANAGER.mutex); + continue; + } + + entry->state = ASSET_ENTRY_STATE_READING; + threadMutexUnlock(&ASSET_MANAGER.mutex); + + if(ASSET_LOADER_CALLBACKS[entry->type].loadAsync) { + ASSET_LOADER_CALLBACKS[entry->type].loadAsync(entry); + } + + threadMutexLock(&ASSET_MANAGER.mutex); + if(entry->state == ASSET_ENTRY_STATE_READING) { + if(ASSET_LOADER_CALLBACKS[entry->type].loadSync) { + entry->state = ASSET_ENTRY_STATE_SYNC_PENDING; + } else { + entry->state = ASSET_ENTRY_STATE_LOADED; + } + } + threadMutexSignal(&ASSET_MANAGER.mutex); + } + threadMutexUnlock(&ASSET_MANAGER.mutex); +} diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h new file mode 100644 index 00000000..918b321b --- /dev/null +++ b/src/dusk/asset/asset.h @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "error/error.h" +#include "thread/thread.h" +#include "thread/threadmutex.h" +#include +#include + +#define ASSET_FILE_NAME "dusk.dsk" +#define ASSET_PATH_MAX 128 +#define ASSET_ENTRY_COUNT_MAX 128 +#define ASSET_JSON_MAX_SIZE (256 * 1024) + +typedef enum { + ASSET_LOADER_TYPE_NULL = 0, + ASSET_LOADER_TYPE_JSON, + ASSET_LOADER_TYPE_COUNT +} assetloadertype_t; + +typedef union { + uint8_t _pad; +} assetloaderinput_t; + +typedef union { + struct { + uint8_t *bytes; + size_t size; + } raw; +} assetloaderloading_t; + +typedef union { + struct { + yyjson_doc *doc; + yyjson_val *root; + } json; +} assetloaderdata_t; + +typedef enum { + ASSET_ENTRY_STATE_IDLE = 0, + ASSET_ENTRY_STATE_QUEUED, + ASSET_ENTRY_STATE_READING, + ASSET_ENTRY_STATE_SYNC_PENDING, + ASSET_ENTRY_STATE_LOADED, + ASSET_ENTRY_STATE_ERROR, +} assetentrystate_t; + +typedef struct { + assetentrystate_t state; + char_t path[ASSET_PATH_MAX]; + assetloadertype_t type; + assetloaderinput_t input; + assetloaderloading_t loading; + assetloaderdata_t data; + uint8_t refCount; +} assetentry_t; + +typedef struct { + void (*loadAsync)(assetentry_t *entry); + void (*loadSync)(assetentry_t *entry); + void (*dispose)(assetentry_t *entry); +} assetloadercallbacks_t; + +typedef struct { + assetentry_t entries[ASSET_ENTRY_COUNT_MAX]; + zip_t *archive; + thread_t loaderThread; + threadmutex_t mutex; +} assetmanager_t; + +extern assetmanager_t ASSET_MANAGER; +extern const assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ + ASSET_LOADER_TYPE_COUNT +]; + +/** + * Set entry->state to ERROR and return from a loader callback. + * Requires the loader callback parameter to be named `entry`. + */ +#define assetLoaderErrorThrow(msg, ...) \ + do { \ + entry->state = ASSET_ENTRY_STATE_ERROR; \ + return; \ + } while(0) + +/** + * If retval is an error, set entry->state to ERROR and return. + * Requires the loader callback parameter to be named `entry`. + */ +#define assetLoaderErrorChain(retval) \ + do { \ + errorret_t _alerr = (retval); \ + if(errorIsNotOk(_alerr)) { \ + entry->state = ASSET_ENTRY_STATE_ERROR; \ + errorCatch(_alerr); \ + return; \ + } \ + } while(0) + +/** + * Initializes the asset manager and opens the asset archive. + * @returns errorret_t + */ +errorret_t assetInit(void); + +/** + * Processes sync-pending entries. Call once per frame from the + * main thread. + */ +void assetUpdate(void); + +/** + * Stops the loader thread, disposes all entries, and closes the + * archive. + * @returns errorret_t + */ +errorret_t assetDispose(void); + +/** + * Gets or creates a cached entry for the given path and type. + * Signals the loader thread to begin loading. + * @param path Asset path within the archive. + * @param type Loader type. + * @param input Optional per-type config, may be NULL. + * @returns Pointer to the entry, or NULL if cache is full. + */ +assetentry_t *assetGetEntry( + const char_t *path, + const assetloadertype_t type, + const assetloaderinput_t *input +); + +/** + * Blocks until entry is LOADED or ERROR. Runs the sync phase + * inline when SYNC_PENDING. Must be called from the main thread. + * @param entry The entry to wait for. + * @returns errorret_t + */ +errorret_t assetRequireLoaded(assetentry_t *entry); + +/** + * Increments the reference count on an entry. + * @param entry The entry to lock. + */ +void assetLock(assetentry_t *entry); + +/** + * Decrements the reference count on an entry. + * @param entry The entry to unlock. + */ +void assetUnlock(assetentry_t *entry); + +/** + * Background loader thread callback. + * @param thread The running thread. + */ +void assetLoaderThread(thread_t *thread); diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt new file mode 100644 index 00000000..91cb9809 --- /dev/null +++ b/src/dusk/asset/loader/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetloader.c +) + +add_subdirectory(json) diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c new file mode 100644 index 00000000..b67f5b8d --- /dev/null +++ b/src/dusk/asset/loader/assetloader.c @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "asset/asset.h" +#include "asset/loader/json/assetjsonloader.h" + +const assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ + ASSET_LOADER_TYPE_COUNT +] = { + [ASSET_LOADER_TYPE_JSON] = { + .loadAsync = assetJsonLoaderAsync, + .loadSync = NULL, + .dispose = assetJsonLoaderDispose + }, +}; diff --git a/src/dusk/asset/loader/json/CMakeLists.txt b/src/dusk/asset/loader/json/CMakeLists.txt new file mode 100644 index 00000000..0226418c --- /dev/null +++ b/src/dusk/asset/loader/json/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetjsonloader.c +) diff --git a/src/dusk/asset/loader/json/assetjsonloader.c b/src/dusk/asset/loader/json/assetjsonloader.c new file mode 100644 index 00000000..c4275183 --- /dev/null +++ b/src/dusk/asset/loader/json/assetjsonloader.c @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetjsonloader.h" +#include "util/memory.h" + +void assetJsonLoaderAsync(assetentry_t *entry) { + zip_int64_t idx = zip_name_locate( + ASSET_MANAGER.archive, entry->path, 0 + ); + if(idx < 0) { + assetLoaderErrorThrow("JSON not found: %s", entry->path); + } + + zip_stat_t stat; + memoryZero(&stat, sizeof(zip_stat_t)); + zip_stat_index( + ASSET_MANAGER.archive, (zip_uint64_t)idx, 0, &stat + ); + + if(!(stat.valid & ZIP_STAT_SIZE)) { + assetLoaderErrorThrow("Cannot stat JSON: %s", entry->path); + } + + if(stat.size > ASSET_JSON_MAX_SIZE) { + assetLoaderErrorThrow("JSON too large: %s", entry->path); + } + + size_t size = (size_t)stat.size; + uint8_t *buf = (uint8_t *)memoryAllocate(size + 1); + + zip_file_t *zf = zip_fopen_index( + ASSET_MANAGER.archive, (zip_uint64_t)idx, 0 + ); + if(!zf) { + memoryFree(buf); + assetLoaderErrorThrow( + "Failed to open ZIP entry: %s", entry->path + ); + } + + zip_fread(zf, buf, size); + zip_fclose(zf); + buf[size] = '\0'; + + entry->data.json.doc = yyjson_read( + (const char_t *)buf, size, 0 + ); + memoryFree(buf); + + if(!entry->data.json.doc) { + assetLoaderErrorThrow("Failed to parse JSON: %s", entry->path); + } + + entry->data.json.root = yyjson_doc_get_root( + entry->data.json.doc + ); +} + +void assetJsonLoaderDispose(assetentry_t *entry) { + if(!entry->data.json.doc) return; + yyjson_doc_free(entry->data.json.doc); + entry->data.json.doc = NULL; + entry->data.json.root = NULL; +} diff --git a/src/dusk/asset/loader/json/assetjsonloader.h b/src/dusk/asset/loader/json/assetjsonloader.h new file mode 100644 index 00000000..65eed107 --- /dev/null +++ b/src/dusk/asset/loader/json/assetjsonloader.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/asset.h" + +/** + * Async loader callback for JSON assets. Opens the archive entry, + * reads bytes, and parses with yyjson. Sets entry->data.json.doc + * and entry->data.json.root on success. + * @param entry The asset entry to load into. + */ +void assetJsonLoaderAsync(assetentry_t *entry); + +/** + * Dispose callback for JSON asset entries. Frees the yyjson doc. + * @param entry The asset entry to dispose. + */ +void assetJsonLoaderDispose(assetentry_t *entry); diff --git a/src/dusk/display/renderpipeline.c b/src/dusk/display/renderpipeline.c index 006f4be0..be61b136 100644 --- a/src/dusk/display/renderpipeline.c +++ b/src/dusk/display/renderpipeline.c @@ -34,7 +34,11 @@ int_t renderPipelineCompare(const void *a, const void *b) { assertNotNull(b, "Entry b cannot be null"); const renderpipelineentry_t *ea = (const renderpipelineentry_t *)a; const renderpipelineentry_t *eb = (const renderpipelineentry_t *)b; - return (int_t)ea->effectivePriority - (int_t)eb->effectivePriority; + int_t pri = (int_t)ea->effectivePriority - (int_t)eb->effectivePriority; + if(pri != 0) return pri; + int_t aHasPos = (ea->posComp != COMPONENT_ID_INVALID) ? 1 : 0; + int_t bHasPos = (eb->posComp != COMPONENT_ID_INVALID) ? 1 : 0; + return aHasPos - bHasPos; } shader_t *renderPipelineGetShader(const entityrenderable_t *r) { @@ -69,6 +73,9 @@ errorret_t renderPipeline(const entityid_t cameraId) { ); pipeline[i].entityId = entities[i]; pipeline[i].componentId = components[i]; + pipeline[i].posComp = entityGetComponent( + entities[i], COMPONENT_TYPE_POSITION + ); pipeline[i].effectivePriority = renderPipelineGetPriority(r); } sort( @@ -104,24 +111,30 @@ errorret_t renderPipeline(const entityid_t cameraId) { errorChain(shaderSetMatrix(s, SHADER_UNLIT_PROJECTION, proj)); } + shader_t *prevShader = NULL; + bool_t prevNoPos = false; for(entityid_t i = 0; i < entCount; i++) { entityid_t eid = pipeline[i].entityId; componentid_t cid = pipeline[i].componentId; + componentid_t posComp = pipeline[i].posComp; entityrenderable_t *r = componentGetData( eid, cid, COMPONENT_TYPE_RENDERABLE ); shader_t *s = renderPipelineGetShader(r); + errorChain(shaderBind(s)); - componentid_t posComp = entityGetComponent(eid, COMPONENT_TYPE_POSITION); if(posComp == COMPONENT_ID_INVALID) { - errorChain(shaderBind(s)); - errorChain(shaderSetMatrix(s, SHADER_UNLIT_MODEL, ident)); + if(!prevNoPos || s != prevShader) { + errorChain(shaderSetMatrix(s, SHADER_UNLIT_MODEL, ident)); + } + prevNoPos = true; } else { entityPositionGetTransform(eid, posComp, model); - errorChain(shaderBind(s)); errorChain(shaderSetMatrix(s, SHADER_UNLIT_MODEL, model)); + prevNoPos = false; } + prevShader = s; errorChain(entityRenderableDraw(eid, cid)); } diff --git a/src/dusk/display/renderpipeline.h b/src/dusk/display/renderpipeline.h index 7e85fc7f..a384547c 100644 --- a/src/dusk/display/renderpipeline.h +++ b/src/dusk/display/renderpipeline.h @@ -14,6 +14,11 @@ typedef struct { entityid_t entityId; componentid_t componentId; + /** + * Precomputed position component ID, or COMPONENT_ID_INVALID if the entity + * has no position. Used for secondary sort and model-matrix batching. + */ + componentid_t posComp; int8_t effectivePriority; } renderpipelineentry_t; @@ -29,8 +34,11 @@ typedef struct { int8_t renderPipelineGetPriority(const entityrenderable_t *r); /** - * sortcompare_t comparator for renderpipelineentry_t. Compares by - * effectivePriority ascending so lower-priority entries sort first. + * sortcompare_t comparator for renderpipelineentry_t. Primary sort by + * effectivePriority ascending. Secondary sort: entries with no position + * component (posComp == COMPONENT_ID_INVALID) sort before entries with a + * position component, grouping them to reduce redundant identity matrix + * uploads. * * @param a Pointer to the first renderpipelineentry_t. * @param b Pointer to the second renderpipelineentry_t. diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 00622171..9e2d6aa6 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -14,6 +14,7 @@ #include "ui/ui.h" #include "ui/uitextbox.h" #include "assert/assert.h" +#include "asset/asset.h" #include "entity/entitymanager.h" #include "entity/component/physics/entityphysics.h" #include "physics/physicsmanager.h" @@ -22,6 +23,7 @@ #include "console/console.h" #include "item/backpack.h" #include "save/save.h" +#include "scene/scene/initial/initialscene.h" engine_t ENGINE; @@ -39,6 +41,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { consoleInit(); errorChain(inputInit()); // errorChain(saveInit()); + errorChain(assetInit()); errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(uiInit()); @@ -46,6 +49,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { entityManagerInit(); backpackInit(); physicsManagerInit(); + errorChain(initialSceneInit()); errorChain(networkInit()); consolePrint("Engine initialized"); @@ -55,6 +59,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineUpdate(void) { // Order here is important. + assetUpdate(); errorChain(networkUpdate()); timeUpdate(); inputUpdate(); @@ -82,6 +87,7 @@ errorret_t engineDispose(void) { consoleDispose(); errorChain(displayDispose()); // errorChain(saveDispose()); + errorChain(assetDispose()); errorOk(); } diff --git a/src/dusk/entity/CMakeLists.txt b/src/dusk/entity/CMakeLists.txt index 150936ec..b5c01e66 100644 --- a/src/dusk/entity/CMakeLists.txt +++ b/src/dusk/entity/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} entity.c entitymanager.c component.c + entityjson.c ) # Subdirs diff --git a/src/dusk/entity/entityjson.c b/src/dusk/entity/entityjson.c new file mode 100644 index 00000000..ba9c21ca --- /dev/null +++ b/src/dusk/entity/entityjson.c @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityjson.h" +#include "entity/entitymanager.h" +#include "entity/entity.h" +#include "entity/component.h" +#include "assert/assert.h" + +void entitySerialize( + const entityid_t entityId, + yyjson_mut_doc *doc, + yyjson_mut_val *obj +) { + assertNotNull(doc, "JSON doc cannot be null"); + assertNotNull(obj, "JSON obj cannot be null"); + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + + yyjson_mut_val *comps = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, obj, "components", comps); + + for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) { + componentindex_t idx = componentGetIndex(entityId, cid); + component_t *cmp = &ENTITY_MANAGER.components[idx]; + if(cmp->type == COMPONENT_TYPE_NULL) continue; + if(!COMPONENT_DEFINITIONS[cmp->type].serialize) continue; + + const char_t *name = COMPONENT_DEFINITIONS[cmp->type].name; + yyjson_mut_val *compObj = yyjson_mut_obj(doc); + COMPONENT_DEFINITIONS[cmp->type].serialize( + entityId, cid, doc, compObj + ); + yyjson_mut_obj_add_val(doc, comps, name, compObj); + } +} + +errorret_t entityDeserialize( + const entityid_t entityId, + yyjson_val *obj +) { + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertNotNull(obj, "JSON obj cannot be null"); + + yyjson_val *comps = yyjson_obj_get(obj, "components"); + if(!comps || !yyjson_is_obj(comps)) errorOk(); + + for(componenttype_t t = 1; t < COMPONENT_TYPE_COUNT; t++) { + const char_t *name = COMPONENT_DEFINITIONS[t].name; + if(!name) continue; + yyjson_val *compObj = yyjson_obj_get(comps, name); + if(!compObj) continue; + componentid_t cid = entityAddComponent(entityId, t); + errorChain(componentDeserialize(entityId, cid, compObj)); + } + errorOk(); +} diff --git a/src/dusk/entity/entityjson.h b/src/dusk/entity/entityjson.h new file mode 100644 index 00000000..525f59fd --- /dev/null +++ b/src/dusk/entity/entityjson.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "error/error.h" +#include + +/** + * Serializes all components of an entity into a JSON object. Each + * component is written as a nested object under its lowercase name + * (e.g. "position", "camera", "renderable"). Components with no + * serialize callback are silently skipped. + * + * @param entityId The entity to serialize. + * @param doc The mutable JSON document to allocate values from. + * @param obj The JSON object to write into. + */ +void entitySerialize( + const entityid_t entityId, + yyjson_mut_doc *doc, + yyjson_mut_val *obj +); + +/** + * Deserializes an entity from a JSON object. Reads the "components" + * object and, for each recognized component name, adds the component + * and deserializes its data. Components present in the JSON but not + * in COMPONENT_DEFINITIONS are silently ignored. + * + * @param entityId The entity to deserialize into (must be initialized). + * @param obj The JSON object to read from. + * @return Error state. + */ +errorret_t entityDeserialize( + const entityid_t entityId, + yyjson_val *obj +); diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt new file mode 100644 index 00000000..d8bbb85b --- /dev/null +++ b/src/dusk/scene/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(scene) diff --git a/src/dusk/scene/scene/CMakeLists.txt b/src/dusk/scene/scene/CMakeLists.txt new file mode 100644 index 00000000..9fcd4d99 --- /dev/null +++ b/src/dusk/scene/scene/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(initial) diff --git a/src/dusk/scene/scene/initial/CMakeLists.txt b/src/dusk/scene/scene/initial/CMakeLists.txt new file mode 100644 index 00000000..d09cd339 --- /dev/null +++ b/src/dusk/scene/scene/initial/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + initialscene.c +) diff --git a/src/dusk/scene/scene/initial/initialscene.c b/src/dusk/scene/scene/initial/initialscene.c new file mode 100644 index 00000000..7c78c7ea --- /dev/null +++ b/src/dusk/scene/scene/initial/initialscene.c @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "initialscene.h" +#include "asset/asset.h" +#include "entity/entitymanager.h" +#include "entity/entity.h" +#include "entity/entityjson.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" + +errorret_t initialSceneInit(void) { + entityid_t camId = entityManagerAdd(); + entityInit(camId); + componentid_t camPos = entityAddComponent( + camId, COMPONENT_TYPE_POSITION + ); + entityAddComponent(camId, COMPONENT_TYPE_CAMERA); + + vec3 eye = {0.0f, 2.0f, 5.0f}; + vec3 target = {0.5f, 0.5f, 0.5f}; + vec3 up = {0.0f, 1.0f, 0.0f}; + entityPositionLookAt(camId, camPos, eye, target, up); + + assetentry_t *entry = assetGetEntry( + "cube.json", ASSET_LOADER_TYPE_JSON, NULL + ); + if(!entry) errorThrow("Failed to get asset entry for cube.json"); + + assetLock(entry); + errorret_t ret = assetRequireLoaded(entry); + if(errorIsOk(ret)) { + entityid_t cubeId = entityManagerAdd(); + entityInit(cubeId); + ret = entityDeserialize(cubeId, entry->data.json.root); + } + assetUnlock(entry); + errorChain(ret); + errorOk(); +} diff --git a/src/dusk/scene/scene/initial/initialscene.h b/src/dusk/scene/scene/initial/initialscene.h new file mode 100644 index 00000000..edb56b15 --- /dev/null +++ b/src/dusk/scene/scene/initial/initialscene.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Creates the initial test scene: a camera entity built in C and + * a cube entity deserialized from cube.json via the asset loader. + * @returns errorret_t + */ +errorret_t initialSceneInit(void); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c172972d..2cf36a1e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,4 +10,5 @@ add_subdirectory(display) # add_subdirectory(rpg) # add_subdirectory(item) add_subdirectory(time) -add_subdirectory(util) \ No newline at end of file +add_subdirectory(util) +add_subdirectory(entity) \ No newline at end of file diff --git a/test/entity/CMakeLists.txt b/test/entity/CMakeLists.txt new file mode 100644 index 00000000..01aad90b --- /dev/null +++ b/test/entity/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(dusktest) +dusktest(test_entityjson.c) +target_compile_definitions(test_entityjson PRIVATE + DUSK_ENTITY_JSON_PATH="${CMAKE_SOURCE_DIR}/assets/entity.json" +) diff --git a/test/entity/test_entityjson.c b/test/entity/test_entityjson.c new file mode 100644 index 00000000..5df71190 --- /dev/null +++ b/test/entity/test_entityjson.c @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "entity/entitymanager.h" +#include "entity/entity.h" +#include "entity/entityjson.h" +#include "entity/component.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" +#include "entity/component/display/entityrenderable.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component/trigger/entitytrigger.h" +#include "util/memory.h" +#include "log/log.h" +#include + +static void test_entityjson_load(void **state) { + yyjson_read_err readErr; + yyjson_doc *doc = yyjson_read_file( + DUSK_ENTITY_JSON_PATH, 0, NULL, &readErr + ); + assert_non_null(doc); + + yyjson_val *root = yyjson_doc_get_root(doc); + assert_non_null(root); + + entityid_t entId = entityManagerAdd(); + entityInit(entId); + + errorret_t ret = entityDeserialize(entId, root); + assert_true(errorIsOk(ret)); + + logDebug("Loaded entity from: %s\n", DUSK_ENTITY_JSON_PATH); + + // Position + componentid_t posComp = entityGetComponent(entId, COMPONENT_TYPE_POSITION); + assert_int_not_equal(posComp, COMPONENT_ID_INVALID); + entityposition_t *pos = componentGetData( + entId, posComp, COMPONENT_TYPE_POSITION + ); + logDebug(" position:\n"); + logDebug( + " position: [%f, %f, %f]\n", + pos->position[0], pos->position[1], pos->position[2] + ); + logDebug( + " rotation: [%f, %f, %f]\n", + pos->rotation[0], pos->rotation[1], pos->rotation[2] + ); + logDebug( + " scale: [%f, %f, %f]\n", + pos->scale[0], pos->scale[1], pos->scale[2] + ); + assert_float_equal(pos->position[0], 0.0f, 0.0001f); + assert_float_equal(pos->position[1], 0.0f, 0.0001f); + assert_float_equal(pos->position[2], 0.0f, 0.0001f); + assert_float_equal(pos->scale[0], 1.0f, 0.0001f); + assert_float_equal(pos->scale[1], 1.0f, 0.0001f); + assert_float_equal(pos->scale[2], 1.0f, 0.0001f); + + // Camera + componentid_t camComp = entityGetComponent(entId, COMPONENT_TYPE_CAMERA); + assert_int_not_equal(camComp, COMPONENT_ID_INVALID); + entitycamera_t *cam = componentGetData( + entId, camComp, COMPONENT_TYPE_CAMERA + ); + const char_t *projStr = "orthographic"; + if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE) { + projStr = "perspective"; + } else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) { + projStr = "perspective_flipped"; + } + logDebug(" camera:\n"); + logDebug(" projType: %s\n", projStr); + logDebug(" fov: %f\n", cam->perspective.fov); + logDebug(" nearClip: %f\n", cam->nearClip); + logDebug(" farClip: %f\n", cam->farClip); + assert_int_equal( + cam->projType, ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE + ); + assert_float_equal(cam->perspective.fov, 45.0f, 0.0001f); + assert_float_equal(cam->nearClip, 0.1f, 0.0001f); + assert_float_equal(cam->farClip, 5000.0f, 0.1f); + + // Renderable + componentid_t rendComp = entityGetComponent( + entId, COMPONENT_TYPE_RENDERABLE + ); + assert_int_not_equal(rendComp, COMPONENT_ID_INVALID); + entityrenderable_t *rend = componentGetData( + entId, rendComp, COMPONENT_TYPE_RENDERABLE + ); + const char_t *rendTypeStr = "custom"; + if(rend->type == ENTITY_RENDERABLE_TYPE_SPRITEBATCH) { + rendTypeStr = "spritebatch"; + } else if(rend->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL) { + rendTypeStr = "shader_material"; + } + logDebug(" renderable:\n"); + logDebug(" type: %s\n", rendTypeStr); + logDebug(" priority: %d\n", (int_t)rend->priority); + logDebug( + " shaderType: %d\n", (int_t)rend->data.material.shaderType + ); + logDebug( + " stateFlags: %d\n", (int_t)rend->data.material.state.flags + ); + assert_int_equal(rend->type, ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL); + assert_int_equal(rend->priority, 0); + + // Physics + componentid_t physComp = entityGetComponent(entId, COMPONENT_TYPE_PHYSICS); + assert_int_not_equal(physComp, COMPONENT_ID_INVALID); + entityphysics_t *phys = componentGetData( + entId, physComp, COMPONENT_TYPE_PHYSICS + ); + const char_t *bodyStr = "static"; + if(phys->type == PHYSICS_BODY_DYNAMIC) { + bodyStr = "dynamic"; + } else if(phys->type == PHYSICS_BODY_KINEMATIC) { + bodyStr = "kinematic"; + } + const char_t *shapeStr = "cube"; + if(phys->shape.type == PHYSICS_SHAPE_SPHERE) { + shapeStr = "sphere"; + } else if(phys->shape.type == PHYSICS_SHAPE_CAPSULE) { + shapeStr = "capsule"; + } else if(phys->shape.type == PHYSICS_SHAPE_PLANE) { + shapeStr = "plane"; + } + logDebug(" physics:\n"); + logDebug(" bodyType: %s\n", bodyStr); + logDebug(" shapeType: %s\n", shapeStr); + logDebug(" gravityScale: %f\n", phys->gravityScale); + assert_int_equal(phys->type, PHYSICS_BODY_DYNAMIC); + assert_int_equal(phys->shape.type, PHYSICS_SHAPE_CUBE); + assert_float_equal(phys->gravityScale, 1.0f, 0.0001f); + + // Trigger + componentid_t trigComp = entityGetComponent( + entId, COMPONENT_TYPE_TRIGGER + ); + assert_int_not_equal(trigComp, COMPONENT_ID_INVALID); + entitytrigger_t *trig = componentGetData( + entId, trigComp, COMPONENT_TYPE_TRIGGER + ); + logDebug(" trigger:\n"); + logDebug( + " min: [%f, %f, %f]\n", + trig->min[0], trig->min[1], trig->min[2] + ); + logDebug( + " max: [%f, %f, %f]\n", + trig->max[0], trig->max[1], trig->max[2] + ); + assert_float_equal(trig->min[0], -0.5f, 0.0001f); + assert_float_equal(trig->max[0], 0.5f, 0.0001f); + + entityDispose(entId); + yyjson_doc_free(doc); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static int groupSetup(void **state) { + assertInit(); + entityManagerInit(); + return 0; +} + +static int groupTeardown(void **state) { + entityManagerDispose(); + return 0; +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_entityjson_load), + }; + return cmocka_run_group_tests(tests, groupSetup, groupTeardown); +}