This commit is contained in:
2026-06-18 07:51:09 -05:00
parent f5db2458cd
commit 647e3a2580
26 changed files with 975 additions and 9 deletions
+15
View File
@@ -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
}
}
}
+32
View File
@@ -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]
}
}
}
+2
View File
@@ -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
)
+3 -1
View File
@@ -56,4 +56,6 @@ add_subdirectory(network)
add_subdirectory(overworld)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
add_subdirectory(thread)
add_subdirectory(asset)
add_subdirectory(scene)
+11
View File
@@ -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)
+202
View File
@@ -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);
}
+163
View File
@@ -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 <zip.h>
#include <yyjson.h>
#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);
+11
View File
@@ -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)
+19
View File
@@ -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
},
};
@@ -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
)
@@ -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;
}
@@ -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);
+18 -5
View File
@@ -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));
}
+10 -2
View File
@@ -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.
+6
View File
@@ -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();
}
+1
View File
@@ -9,6 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
entity.c
entitymanager.c
component.c
entityjson.c
)
# Subdirs
+60
View File
@@ -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();
}
+42
View File
@@ -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 <yyjson.h>
/**
* 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
);
+6
View File
@@ -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)
+6
View File
@@ -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)
@@ -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
)
@@ -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();
}
@@ -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);
+2 -1
View File
@@ -10,4 +10,5 @@ add_subdirectory(display)
# add_subdirectory(rpg)
# add_subdirectory(item)
add_subdirectory(time)
add_subdirectory(util)
add_subdirectory(util)
add_subdirectory(entity)
+10
View File
@@ -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"
)
+186
View File
@@ -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 <yyjson.h>
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);
}