From 373f1c1010db3db0f90fc9e437fc909993144855 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 19 Jul 2026 22:12:53 -0500 Subject: [PATCH] test --- assets/scenes/test.json | 85 ++++++++ src/dusk/display/mesh/plane.c | 12 +- src/dusk/entity/component.c | 51 ++++- src/dusk/entity/component.h | 66 +++++- .../entity/component/display/entitycamera.c | 97 +++++++++ .../entity/component/display/entitycamera.h | 43 ++++ .../entity/component/display/entityposition.c | 115 ++++++++++ .../entity/component/display/entityposition.h | 75 +++++++ .../component/display/entityrenderable.c | 145 +++++++++++++ .../component/display/entityrenderable.h | 46 ++++ .../entity/component/physics/entityphysics.c | 197 ++++++++++++++++++ .../entity/component/physics/entityphysics.h | 44 ++++ src/dusk/entity/componentlist.h | 14 +- src/dusk/entity/entity.c | 103 +++++++++ src/dusk/entity/entity.h | 89 ++++++++ src/dusk/scene/scene.c | 132 ++++++++++++ src/dusk/scene/scene.h | 57 +++++ .../entity/component/overworld/entityplayer.c | 60 ++++++ .../entity/component/overworld/entityplayer.h | 49 ++++- src/duskrpg/entity/gamecomponentlist.h | 5 +- src/duskrpg/game/game.c | 26 ++- test/entity/test_entitymanager.c | 27 +++ test/entity/test_entityposition.c | 45 ++++ test/scene/test_scene.c | 79 +++++++ 24 files changed, 1646 insertions(+), 16 deletions(-) create mode 100644 assets/scenes/test.json diff --git a/assets/scenes/test.json b/assets/scenes/test.json new file mode 100644 index 00000000..bb07bfa4 --- /dev/null +++ b/assets/scenes/test.json @@ -0,0 +1,85 @@ +{ + "entities": [ + { + "name": "camera", + "components": [ + { + "type": "POSITION", + "x": 10, "y": 10, "z": 10, + "lookAt": { "x": 0, "y": 0, "z": 0 } + }, + { + "type": "CAMERA", + "projType": "PERSPECTIVE", + "nearClip": 0.1, "farClip": 1000, + "fov": 0.9 + } + ] + }, + { + "components": [ + { "type": "POSITION", "x": 0, "y": 1, "z": 0, "parent": "camera" } + ] + }, + { + "name": "player", + "components": [ + { "type": "POSITION", "x": 0, "y": 5, "z": 0 }, + { + "type": "RENDERABLE", + "renderType": "SHADER_MATERIAL", + "priority": 0, + "shaderType": "UNLIT", + "color": { "r": 255, "g": 80, "b": 80, "a": 255 }, + "displayState": { "cull": false, "depthTest": true, "blend": false } + }, + { "type": "PLAYER", "moveSpeed": 4, "jumpImpulse": 6 }, + { "type": "PHYSICS", "bodyType": "DYNAMIC" } + ] + }, + { + "components": [ + { + "type": "POSITION", + "x": 0, "y": 0, "z": 0.85, + "parent": "player", + "scale": { "x": 0.7, "y": 0.7, "z": 0.7 } + }, + { + "type": "RENDERABLE", + "renderType": "SHADER_MATERIAL", + "priority": 0, + "shaderType": "UNLIT", + "color": { "r": 80, "g": 160, "b": 255, "a": 255 }, + "displayState": { "cull": false, "depthTest": true, "blend": false } + } + ] + }, + { + "components": [ + { + "type": "POSITION", + "x": 0, "y": -0.1, "z": 0, + "scale": { "x": 20, "y": 0.2, "z": 20 } + }, + { + "type": "RENDERABLE", + "renderType": "SHADER_MATERIAL", + "priority": 0, + "shaderType": "UNLIT", + "color": { "r": 128, "g": 128, "b": 128, "a": 255 }, + "displayState": { "cull": false, "depthTest": true, "blend": false } + }, + { + "type": "PHYSICS", + "bodyType": "STATIC", + "shape": { + "type": "PLANE", + "normalX": 0, "normalY": 1, "normalZ": 0, + "distance": 0 + } + } + ] + } + ] +} diff --git a/src/dusk/display/mesh/plane.c b/src/dusk/display/mesh/plane.c index 86d178ba..4c9acbbe 100644 --- a/src/dusk/display/mesh/plane.c +++ b/src/dusk/display/mesh/plane.c @@ -59,7 +59,8 @@ void planeBuffer( switch(axis) { case PLANE_AXIS_XY: { - /* Flat in XY at z = min[2]; spans X and Y. */ + // Flat in XY at z = min[2]; spans X and Y. + // +Z normal: CCW when viewed from +Z (matches cube.c's front face). const float_t z = min[2]; PLANE_VERT(0, min[0], min[1], z, u0, v0) PLANE_VERT(1, max[0], min[1], z, u1, v0) @@ -71,7 +72,11 @@ void planeBuffer( } case PLANE_AXIS_XZ: { - /* Flat in XZ at y = min[1]; spans X and Z. */ + // Flat in XZ at y = min[1]; spans X and Z. + // +Y normal: CCW when viewed from +Y (matches cube.c's top face). + // X and Z swap handedness relative to XY/YZ (right-hand rule with Y + // up), so the corner order here is deliberately not a straight copy + // of the XY/YZ pattern -- copying it as-is would flip this to -Y. const float_t y = min[1]; PLANE_VERT(0, min[0], y, min[2], u0, v0) PLANE_VERT(1, max[0], y, max[2], u1, v1) @@ -83,7 +88,8 @@ void planeBuffer( } case PLANE_AXIS_YZ: { - /* Flat in YZ at x = min[0]; spans Y and Z. */ + // Flat in YZ at x = min[0]; spans Y and Z. + // +X normal: CCW when viewed from +X (matches cube.c's right face). const float_t x = min[0]; PLANE_VERT(0, x, min[1], min[2], u0, v0) PLANE_VERT(1, x, max[1], min[2], u1, v0) diff --git a/src/dusk/entity/component.c b/src/dusk/entity/component.c index 58512710..ebf475a3 100644 --- a/src/dusk/entity/component.c +++ b/src/dusk/entity/component.c @@ -12,13 +12,15 @@ componentdefinition_t COMPONENT_DEFINITIONS[] = { [COMPONENT_TYPE_NULL] = { 0 }, - #define X(enm, type, field, iMethod, dMethod, rMethod) \ + #define X(enm, type, field, iMethod, dMethod, rMethod, sMethod, dsMethod) \ [COMPONENT_TYPE_##enm] = { \ .enumName = #enm, \ .name = #field, \ .init = iMethod, \ .dispose = dMethod, \ - .render = rMethod \ + .render = rMethod, \ + .serialize = sMethod, \ + .deserialize = dsMethod \ }, #include "componentlist.h" @@ -150,3 +152,48 @@ void componentDispose( cmp->type = COMPONENT_TYPE_NULL; } + +errorret_t componentSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + assertNotNull(mgr, "Entity manager cannot be null"); + assertNotNull(doc, "JSON document cannot be null"); + assertNotNull(json, "JSON object cannot be null"); + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB"); + assertTrue(type != COMPONENT_TYPE_NULL, "Cannot serialize null component"); + + if(!COMPONENT_DEFINITIONS[type].serialize) errorOk(); + errorChain( + COMPONENT_DEFINITIONS[type].serialize(mgr, entityId, componentId, doc, + json) + ); + errorOk(); +} + +errorret_t componentDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type, + yyjson_val *json +) { + assertNotNull(mgr, "Entity manager cannot be null"); + assertNotNull(json, "JSON object cannot be null"); + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB"); + assertTrue(type != COMPONENT_TYPE_NULL, "Cannot deserialize null component"); + + if(!COMPONENT_DEFINITIONS[type].deserialize) errorOk(); + errorChain( + COMPONENT_DEFINITIONS[type].deserialize(mgr, entityId, componentId, json) + ); + errorOk(); +} diff --git a/src/dusk/entity/component.h b/src/dusk/entity/component.h index b380646a..c93b285c 100644 --- a/src/dusk/entity/component.h +++ b/src/dusk/entity/component.h @@ -7,14 +7,18 @@ #pragma once #include "entitybase.h" +#include "error/error.h" +#include "yyjson.h" -#define X(enumName, type, field, init, dispose, render) \ +#define X(enumName, type, field, init, dispose, render, serialize, \ + deserialize) \ // do nothing #include "componentlist.h" #undef X typedef union { - #define X(enumName, type, field, init, dispose, render) type field; + #define X(enumName, type, field, init, dispose, render, serialize, \ + deserialize) type field; #include "componentlist.h" #undef X } componentdata_t; @@ -25,12 +29,26 @@ typedef struct { void (*init)(entitymanager_t *, const entityid_t, const componentid_t); void (*dispose)(entitymanager_t *, const entityid_t, const componentid_t); errorret_t (*render)(entitymanager_t *, const entityid_t, const componentid_t); + errorret_t (*serialize)( + entitymanager_t *, + const entityid_t, + const componentid_t, + yyjson_mut_doc *, + yyjson_mut_val * + ); + errorret_t (*deserialize)( + entitymanager_t *, + const entityid_t, + const componentid_t, + yyjson_val * + ); } componentdefinition_t; typedef enum { COMPONENT_TYPE_NULL, - #define X(enumName, type, field, init, dispose, render) \ + #define X(enumName, type, field, init, dispose, render, serialize, \ + deserialize) \ COMPONENT_TYPE_##enumName, #include "componentlist.h" #undef X @@ -127,3 +145,45 @@ void componentDispose( * @return Error state. */ errorret_t componentRenderAll(entitymanager_t *mgr); + +/** + * Writes a component's data into the given JSON object, if the component + * type defines a serialize callback. No-op (returns success) for + * components whose definition has serialize == NULL. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param type The type of the component to serialize. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t componentSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads a component's data from the given JSON object, if the component + * type defines a deserialize callback. No-op (returns success) for + * components whose definition has deserialize == NULL. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param type The type of the component to deserialize. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t componentDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type, + yyjson_val *json +); diff --git a/src/dusk/entity/component/display/entitycamera.c b/src/dusk/entity/component/display/entitycamera.c index 06697054..95dce2a6 100644 --- a/src/dusk/entity/component/display/entitycamera.c +++ b/src/dusk/entity/component/display/entitycamera.c @@ -9,6 +9,8 @@ #include "entity/entity.h" #include "entity/component/display/entityposition.h" #include "display/screen/screen.h" +#include "util/string.h" +#include "assert/assert.h" void entityCameraInit( entitymanager_t *mgr, @@ -138,3 +140,98 @@ void entityCameraLookAtPixelPerfect( vec3 up = { 0.0f, 0.0f, -1.0f }; entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up); } + +errorret_t entityCameraSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + entitycamera_t *cam = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_CAMERA + ); + + const char_t *projTypeName; + switch(cam->projType) { + case ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE: + projTypeName = "PERSPECTIVE"; + break; + case ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED: + projTypeName = "PERSPECTIVE_FLIPPED"; + break; + case ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC: + projTypeName = "ORTHOGRAPHIC"; + break; + default: + assertUnreachable("Unknown camera projection type"); + } + yyjson_mut_obj_add_str(doc, json, "projType", projTypeName); + yyjson_mut_obj_add_real(doc, json, "nearClip", cam->nearClip); + yyjson_mut_obj_add_real(doc, json, "farClip", cam->farClip); + + if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) { + yyjson_mut_obj_add_real(doc, json, "left", cam->orthographic.left); + yyjson_mut_obj_add_real(doc, json, "right", cam->orthographic.right); + yyjson_mut_obj_add_real(doc, json, "top", cam->orthographic.top); + yyjson_mut_obj_add_real(doc, json, "bottom", cam->orthographic.bottom); + } else { + yyjson_mut_obj_add_real(doc, json, "fov", cam->perspective.fov); + } + + errorOk(); +} + +errorret_t entityCameraDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + entitycamera_t *cam = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_CAMERA + ); + + yyjson_val *projTypeVal = yyjson_obj_get(json, "projType"); + if(projTypeVal) { + const char_t *projTypeName = yyjson_get_str(projTypeVal); + if(stringEquals(projTypeName, "ORTHOGRAPHIC")) { + cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC; + } else if(stringEquals(projTypeName, "PERSPECTIVE_FLIPPED")) { + cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED; + } else if(stringEquals(projTypeName, "PERSPECTIVE")) { + cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE; + } else { + errorThrow("Unknown camera projection type '%s'", projTypeName); + } + } + + yyjson_val *v; + if((v = yyjson_obj_get(json, "nearClip"))) { + cam->nearClip = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "farClip"))) { + cam->farClip = (float_t)yyjson_get_num(v); + } + + if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) { + if((v = yyjson_obj_get(json, "left"))) { + cam->orthographic.left = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "right"))) { + cam->orthographic.right = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "top"))) { + cam->orthographic.top = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "bottom"))) { + cam->orthographic.bottom = (float_t)yyjson_get_num(v); + } + } else { + if((v = yyjson_obj_get(json, "fov"))) { + cam->perspective.fov = (float_t)yyjson_get_num(v); + } + } + + errorOk(); +} diff --git a/src/dusk/entity/component/display/entitycamera.h b/src/dusk/entity/component/display/entitycamera.h index 3205dea5..983d0a90 100644 --- a/src/dusk/entity/component/display/entitycamera.h +++ b/src/dusk/entity/component/display/entitycamera.h @@ -7,6 +7,8 @@ #pragma once #include "entity/entitybase.h" +#include "error/error.h" +#include "yyjson.h" typedef enum { ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE, @@ -119,3 +121,44 @@ void entityCameraLookAtPixelPerfect( const vec3 eyeOffset, const float_t scale ); + +/** + * Serializes the camera's projection type ("projType": "PERSPECTIVE" / + * "PERSPECTIVE_FLIPPED" / "ORTHOGRAPHIC"), "nearClip", "farClip", and + * whichever projection-specific fields apply ("fov" for the perspective + * types, "left"/"right"/"top"/"bottom" for orthographic) into the given + * JSON object. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityCameraSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads "projType", "nearClip", "farClip", and the projection-specific + * fields (see entityCameraSerialize()) from the given JSON object and + * applies whichever are present, leaving the rest at their current + * values. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityCameraDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/dusk/entity/component/display/entityposition.c b/src/dusk/entity/component/display/entityposition.c index f1a4b5cb..7c068a77 100644 --- a/src/dusk/entity/component/display/entityposition.c +++ b/src/dusk/entity/component/display/entityposition.c @@ -6,6 +6,7 @@ */ #include "entity/entitymanager.h" +#include "assert/assert.h" void entityPositionInit( entitymanager_t *mgr, @@ -458,6 +459,17 @@ void entityPositionSetParent( entityPositionMarkDirty(mgr, pos); } +entityid_t entityPositionGetParent( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +) { + entityposition_t *pos = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_POSITION + ); + return pos->parentEntityId; +} + entityposition_t *entityPositionGet( entitymanager_t *mgr, const entityid_t entityId, @@ -655,3 +667,106 @@ void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) { pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY; } + +errorret_t entityPositionSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + vec3 position, rotation, scale; + entityPositionGetLocalPosition(mgr, entityId, componentId, position); + entityPositionGetLocalRotation(mgr, entityId, componentId, rotation); + entityPositionGetLocalScale(mgr, entityId, componentId, scale); + + yyjson_mut_obj_add_real(doc, json, "x", position[0]); + yyjson_mut_obj_add_real(doc, json, "y", position[1]); + yyjson_mut_obj_add_real(doc, json, "z", position[2]); + + yyjson_mut_val *rot = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, json, "rotation", rot); + yyjson_mut_obj_add_real(doc, rot, "x", rotation[0]); + yyjson_mut_obj_add_real(doc, rot, "y", rotation[1]); + yyjson_mut_obj_add_real(doc, rot, "z", rotation[2]); + + yyjson_mut_val *scl = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, json, "scale", scl); + yyjson_mut_obj_add_real(doc, scl, "x", scale[0]); + yyjson_mut_obj_add_real(doc, scl, "y", scale[1]); + yyjson_mut_obj_add_real(doc, scl, "z", scale[2]); + + errorOk(); +} + +errorret_t entityPositionDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + yyjson_val *xVal = yyjson_obj_get(json, "x"); + yyjson_val *yVal = yyjson_obj_get(json, "y"); + yyjson_val *zVal = yyjson_obj_get(json, "z"); + if(xVal || yVal || zVal) { + vec3 position; + entityPositionGetLocalPosition(mgr, entityId, componentId, position); + if(xVal) position[0] = (float_t)yyjson_get_num(xVal); + if(yVal) position[1] = (float_t)yyjson_get_num(yVal); + if(zVal) position[2] = (float_t)yyjson_get_num(zVal); + entityPositionSetLocalPosition(mgr, entityId, componentId, position); + } + + yyjson_val *rot = yyjson_obj_get(json, "rotation"); + if(rot) { + vec3 rotation; + entityPositionGetLocalRotation(mgr, entityId, componentId, rotation); + yyjson_val *v; + if((v = yyjson_obj_get(rot, "x"))) rotation[0] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(rot, "y"))) rotation[1] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(rot, "z"))) rotation[2] = (float_t)yyjson_get_num(v); + entityPositionSetLocalRotation(mgr, entityId, componentId, rotation); + } + + yyjson_val *scl = yyjson_obj_get(json, "scale"); + if(scl) { + vec3 scale; + entityPositionGetLocalScale(mgr, entityId, componentId, scale); + yyjson_val *v; + if((v = yyjson_obj_get(scl, "x"))) scale[0] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(scl, "y"))) scale[1] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(scl, "z"))) scale[2] = (float_t)yyjson_get_num(v); + entityPositionSetLocalScale(mgr, entityId, componentId, scale); + } + + yyjson_val *lookAt = yyjson_obj_get(json, "lookAt"); + if(lookAt) { + yyjson_val *targetXVal = yyjson_obj_get(lookAt, "x"); + yyjson_val *targetYVal = yyjson_obj_get(lookAt, "y"); + yyjson_val *targetZVal = yyjson_obj_get(lookAt, "z"); + assertTrue( + targetXVal && targetYVal && targetZVal, + "Position JSON 'lookAt' missing 'x'/'y'/'z' field" + ); + vec3 target = { + (float_t)yyjson_get_num(targetXVal), + (float_t)yyjson_get_num(targetYVal), + (float_t)yyjson_get_num(targetZVal) + }; + + vec3 up = { 0.0f, 1.0f, 0.0f }; + yyjson_val *upVal = yyjson_obj_get(lookAt, "up"); + if(upVal) { + yyjson_val *v; + if((v = yyjson_obj_get(upVal, "x"))) up[0] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(upVal, "y"))) up[1] = (float_t)yyjson_get_num(v); + if((v = yyjson_obj_get(upVal, "z"))) up[2] = (float_t)yyjson_get_num(v); + } + + vec3 eye; + entityPositionGetLocalPosition(mgr, entityId, componentId, eye); + entityPositionLookAt(mgr, entityId, componentId, eye, target, up); + } + + errorOk(); +} diff --git a/src/dusk/entity/component/display/entityposition.h b/src/dusk/entity/component/display/entityposition.h index 0979c850..fb7cc5bc 100644 --- a/src/dusk/entity/component/display/entityposition.h +++ b/src/dusk/entity/component/display/entityposition.h @@ -7,6 +7,8 @@ #pragma once #include "entity/entitybase.h" +#include "error/error.h" +#include "yyjson.h" /** Maximum number of child position components this node can track. */ #define ENTITY_POSITION_CHILDREN_MAX 8 @@ -358,6 +360,20 @@ void entityPositionSetParent( const componentid_t parentComponentId ); +/** + * Gets the entity ID of this position component's parent. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The parent entity ID, or ENTITY_ID_INVALID if unparented. + */ +entityid_t entityPositionGetParent( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId +); + /** * Returns a direct pointer to the entity position component data. * After modifying localTransform directly, call entityPositionMarkDirty() to @@ -443,3 +459,62 @@ void entityPositionEnsureLocal(entityposition_t *pos); * @param pos The position component to update. */ void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos); + +/** + * Serializes the local position as "x"/"y"/"z", and the local rotation + * and scale as nested "rotation"/"scale" objects (each with "x"/"y"/"z"), + * into the given JSON object. Does NOT write a "parent" field itself -- + * the parent link (see entityPositionSetParent()) is a cross-entity + * reference that only sceneSerialize() has enough context (every + * entity's name) to resolve, so it writes "parent" onto this same JSON + * object as a sibling field after calling this function. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityPositionSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads the local position ("x"/"y"/"z") and the local rotation/scale + * (nested "rotation"/"scale" objects, each with "x"/"y"/"z") from the + * given JSON object and applies whichever fields are present, leaving + * the rest at their current values. + * + * Also supports a nested "lookAt" object as an authoring convenience for + * orienting the entity, in place of an explicit "rotation": "x"/"y"/"z" + * (required, the world-space point to face) plus an optional nested + * "up" ({"x","y","z"}, defaults to {0,1,0}). The eye point is whatever + * the local position resolves to after applying "x"/"y"/"z" above (its + * current value if none of those were given). If both "rotation" and + * "lookAt" are present, "lookAt" wins since it is applied afterward. Not + * emitted by entityPositionSerialize() -- lookAt is a one-time input + * convenience, not persisted state; it round-trips through "rotation". + * + * Does NOT read a "parent" field itself, for the same reason + * entityPositionSerialize() doesn't write one -- resolving a "parent" + * name to an entityId requires sceneDeserialize()'s scene-wide view, so + * it calls entityPositionSetParent() directly once every entity in the + * scene exists. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityPositionDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c index 03af2edf..5a764cc9 100644 --- a/src/dusk/entity/component/display/entityrenderable.c +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -12,6 +12,7 @@ #include "display/display.h" #include "display/mesh/cube.h" #include "util/memory.h" +#include "util/string.h" #include "assert/assert.h" void entityRenderableInit( @@ -164,3 +165,147 @@ errorret_t entityRenderableDrawCustom( ) { return custom->draw(mgr, entityId, componentId, custom->drawUser); } + +errorret_t entityRenderableSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + entityrenderable_t *r = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + + const char_t *renderTypeName; + switch(r->type) { + case ENTITY_RENDERABLE_TYPE_CUSTOM: + renderTypeName = "CUSTOM"; + break; + case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: + renderTypeName = "SPRITEBATCH"; + break; + case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: + renderTypeName = "SHADER_MATERIAL"; + break; + default: + assertUnreachable("Unknown renderable type"); + } + yyjson_mut_obj_add_str(doc, json, "renderType", renderTypeName); + yyjson_mut_obj_add_int(doc, json, "priority", r->priority); + + if(r->type != ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL) errorOk(); + + const entityrenderablematerial_t *m = &r->data.material; + + const char_t *shaderTypeName; + switch(m->shaderType) { + case SHADER_LIST_SHADER_UNLIT: + shaderTypeName = "UNLIT"; + break; + default: + errorThrow("Cannot serialize unknown shaderType %d", m->shaderType); + } + yyjson_mut_obj_add_str(doc, json, "shaderType", shaderTypeName); + + yyjson_mut_val *color = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, json, "color", color); + yyjson_mut_obj_add_int(doc, color, "r", m->material.unlit.color.r); + yyjson_mut_obj_add_int(doc, color, "g", m->material.unlit.color.g); + yyjson_mut_obj_add_int(doc, color, "b", m->material.unlit.color.b); + yyjson_mut_obj_add_int(doc, color, "a", m->material.unlit.color.a); + + yyjson_mut_val *displayState = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, json, "displayState", displayState); + yyjson_mut_obj_add_bool( + doc, displayState, "cull", (m->state.flags & DISPLAY_STATE_FLAG_CULL) != 0 + ); + yyjson_mut_obj_add_bool( + doc, displayState, "depthTest", + (m->state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) != 0 + ); + yyjson_mut_obj_add_bool( + doc, displayState, "blend", + (m->state.flags & DISPLAY_STATE_FLAG_BLEND) != 0 + ); + + errorOk(); +} + +errorret_t entityRenderableDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + entityrenderable_t *r = componentGetData( + mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + + yyjson_val *renderTypeVal = yyjson_obj_get(json, "renderType"); + if(renderTypeVal) { + const char_t *renderTypeName = yyjson_get_str(renderTypeVal); + if(stringEquals(renderTypeName, "CUSTOM")) { + r->type = ENTITY_RENDERABLE_TYPE_CUSTOM; + } else if(stringEquals(renderTypeName, "SPRITEBATCH")) { + r->type = ENTITY_RENDERABLE_TYPE_SPRITEBATCH; + } else if(stringEquals(renderTypeName, "SHADER_MATERIAL")) { + r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL; + } else { + errorThrow("Unknown renderable renderType '%s'", renderTypeName); + } + } + + yyjson_val *priorityVal = yyjson_obj_get(json, "priority"); + if(priorityVal) r->priority = (int8_t)yyjson_get_int(priorityVal); + + if(r->type != ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL) errorOk(); + + entityrenderablematerial_t *m = &r->data.material; + + yyjson_val *shaderTypeVal = yyjson_obj_get(json, "shaderType"); + if(shaderTypeVal) { + const char_t *shaderTypeName = yyjson_get_str(shaderTypeVal); + if(stringEquals(shaderTypeName, "UNLIT")) { + m->shaderType = SHADER_LIST_SHADER_UNLIT; + } else { + errorThrow("Unknown renderable shaderType '%s'", shaderTypeName); + } + } + + yyjson_val *color = yyjson_obj_get(json, "color"); + if(color) { + yyjson_val *v; + if((v = yyjson_obj_get(color, "r"))) { + m->material.unlit.color.r = (colorchannel8_t)yyjson_get_int(v); + } + if((v = yyjson_obj_get(color, "g"))) { + m->material.unlit.color.g = (colorchannel8_t)yyjson_get_int(v); + } + if((v = yyjson_obj_get(color, "b"))) { + m->material.unlit.color.b = (colorchannel8_t)yyjson_get_int(v); + } + if((v = yyjson_obj_get(color, "a"))) { + m->material.unlit.color.a = (colorchannel8_t)yyjson_get_int(v); + } + } + + yyjson_val *displayState = yyjson_obj_get(json, "displayState"); + if(displayState) { + yyjson_val *v; + if((v = yyjson_obj_get(displayState, "cull"))) { + if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_CULL; + else m->state.flags &= ~DISPLAY_STATE_FLAG_CULL; + } + if((v = yyjson_obj_get(displayState, "depthTest"))) { + if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_DEPTH_TEST; + else m->state.flags &= ~DISPLAY_STATE_FLAG_DEPTH_TEST; + } + if((v = yyjson_obj_get(displayState, "blend"))) { + if(yyjson_get_bool(v)) m->state.flags |= DISPLAY_STATE_FLAG_BLEND; + else m->state.flags &= ~DISPLAY_STATE_FLAG_BLEND; + } + } + + errorOk(); +} diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h index 0fa01183..2a559eac 100644 --- a/src/dusk/entity/component/display/entityrenderable.h +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -11,6 +11,8 @@ #include "display/shader/shadermaterial.h" #include "display/spritebatch/spritebatch.h" #include "display/displaystate.h" +#include "error/error.h" +#include "yyjson.h" #define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64 #define ENTITY_RENDERABLE_MESHES_MAX 8 @@ -211,3 +213,47 @@ errorret_t entityRenderableDrawCustom( const componentid_t componentId, const entityrenderablecustom_t *custom ); + +/** + * Serializes the renderable's "renderType" + * ("CUSTOM"/"SPRITEBATCH"/"SHADER_MATERIAL") and "priority" into the + * given JSON object. For ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL, also + * serializes "shaderType" (currently only "UNLIT"), its "color" + * ({"r","g","b","a"}, 0-255), and "displayState" (an object of + * "cull"/"depthTest"/"blend" booleans). Meshes, spritebatch + * textures/sprites, and the custom draw callback are asset-loaded or + * runtime-only and are not serialized. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityRenderableSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads "renderType", "priority", and (for SHADER_MATERIAL) + * "shaderType"/"color"/"displayState" (see entityRenderableSerialize()) + * from the given JSON object and applies whichever are present, leaving + * the rest at their current values. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityRenderableDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/dusk/entity/component/physics/entityphysics.c b/src/dusk/entity/component/physics/entityphysics.c index 9ea6d7fc..1db2dff4 100644 --- a/src/dusk/entity/component/physics/entityphysics.c +++ b/src/dusk/entity/component/physics/entityphysics.c @@ -8,6 +8,8 @@ #include "entityphysics.h" #include "entity/entitymanager.h" #include "util/memory.h" +#include "util/string.h" +#include "assert/assert.h" void entityPhysicsInit( entitymanager_t *mgr, @@ -113,3 +115,198 @@ physicsbodytype_t entityPhysicsGetBodyType( entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId); return phys->type; } + +errorret_t entityPhysicsSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId); + + const char_t *bodyTypeName; + switch(phys->type) { + case PHYSICS_BODY_STATIC: + bodyTypeName = "STATIC"; + break; + case PHYSICS_BODY_DYNAMIC: + bodyTypeName = "DYNAMIC"; + break; + case PHYSICS_BODY_KINEMATIC: + bodyTypeName = "KINEMATIC"; + break; + default: + assertUnreachable("Unknown physics body type"); + } + yyjson_mut_obj_add_str(doc, json, "bodyType", bodyTypeName); + + yyjson_mut_val *shape = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, json, "shape", shape); + + switch(phys->shape.type) { + case PHYSICS_SHAPE_CUBE: + yyjson_mut_obj_add_str(doc, shape, "type", "CUBE"); + yyjson_mut_obj_add_real( + doc, shape, "halfExtentX", phys->shape.data.cube.halfExtents[0] + ); + yyjson_mut_obj_add_real( + doc, shape, "halfExtentY", phys->shape.data.cube.halfExtents[1] + ); + yyjson_mut_obj_add_real( + doc, shape, "halfExtentZ", phys->shape.data.cube.halfExtents[2] + ); + break; + case PHYSICS_SHAPE_SPHERE: + yyjson_mut_obj_add_str(doc, shape, "type", "SPHERE"); + yyjson_mut_obj_add_real( + doc, shape, "radius", phys->shape.data.sphere.radius + ); + break; + case PHYSICS_SHAPE_CAPSULE: + yyjson_mut_obj_add_str(doc, shape, "type", "CAPSULE"); + yyjson_mut_obj_add_real( + doc, shape, "radius", phys->shape.data.capsule.radius + ); + yyjson_mut_obj_add_real( + doc, shape, "halfHeight", phys->shape.data.capsule.halfHeight + ); + break; + case PHYSICS_SHAPE_PLANE: + yyjson_mut_obj_add_str(doc, shape, "type", "PLANE"); + yyjson_mut_obj_add_real( + doc, shape, "normalX", phys->shape.data.plane.normal[0] + ); + yyjson_mut_obj_add_real( + doc, shape, "normalY", phys->shape.data.plane.normal[1] + ); + yyjson_mut_obj_add_real( + doc, shape, "normalZ", phys->shape.data.plane.normal[2] + ); + yyjson_mut_obj_add_real( + doc, shape, "distance", phys->shape.data.plane.distance + ); + break; + case PHYSICS_SHAPE_CUSTOM: + errorThrow( + "Cannot serialize a PHYSICS_SHAPE_CUSTOM shape (callback/userData " + "are not representable in JSON)" + ); + default: + assertUnreachable("Unknown physics shape type"); + } + + yyjson_mut_obj_add_real(doc, json, "velocityX", phys->velocity[0]); + yyjson_mut_obj_add_real(doc, json, "velocityY", phys->velocity[1]); + yyjson_mut_obj_add_real(doc, json, "velocityZ", phys->velocity[2]); + yyjson_mut_obj_add_real(doc, json, "gravityScale", phys->gravityScale); + yyjson_mut_obj_add_bool(doc, json, "onGround", phys->onGround); + + errorOk(); +} + +errorret_t entityPhysicsDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId); + + yyjson_val *bodyTypeVal = yyjson_obj_get(json, "bodyType"); + if(bodyTypeVal) { + const char_t *bodyTypeName = yyjson_get_str(bodyTypeVal); + if(stringEquals(bodyTypeName, "STATIC")) { + phys->type = PHYSICS_BODY_STATIC; + } else if(stringEquals(bodyTypeName, "DYNAMIC")) { + phys->type = PHYSICS_BODY_DYNAMIC; + } else if(stringEquals(bodyTypeName, "KINEMATIC")) { + phys->type = PHYSICS_BODY_KINEMATIC; + } else { + errorThrow("Unknown physics body type '%s'", bodyTypeName); + } + } + + yyjson_val *shape = yyjson_obj_get(json, "shape"); + if(shape) { + yyjson_val *shapeTypeVal = yyjson_obj_get(shape, "type"); + assertNotNull(shapeTypeVal, "Physics shape JSON missing 'type' field"); + const char_t *shapeTypeName = yyjson_get_str(shapeTypeVal); + yyjson_val *v; + + if(stringEquals(shapeTypeName, "CUBE")) { + physicsshape_t s = { .type = PHYSICS_SHAPE_CUBE }; + if((v = yyjson_obj_get(shape, "halfExtentX"))) { + s.data.cube.halfExtents[0] = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "halfExtentY"))) { + s.data.cube.halfExtents[1] = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "halfExtentZ"))) { + s.data.cube.halfExtents[2] = (float_t)yyjson_get_num(v); + } + entityPhysicsSetShape(mgr, entityId, componentId, s); + } else if(stringEquals(shapeTypeName, "SPHERE")) { + physicsshape_t s = { .type = PHYSICS_SHAPE_SPHERE }; + if((v = yyjson_obj_get(shape, "radius"))) { + s.data.sphere.radius = (float_t)yyjson_get_num(v); + } + entityPhysicsSetShape(mgr, entityId, componentId, s); + } else if(stringEquals(shapeTypeName, "CAPSULE")) { + physicsshape_t s = { .type = PHYSICS_SHAPE_CAPSULE }; + if((v = yyjson_obj_get(shape, "radius"))) { + s.data.capsule.radius = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "halfHeight"))) { + s.data.capsule.halfHeight = (float_t)yyjson_get_num(v); + } + entityPhysicsSetShape(mgr, entityId, componentId, s); + } else if(stringEquals(shapeTypeName, "PLANE")) { + physicsshape_t s = { .type = PHYSICS_SHAPE_PLANE }; + if((v = yyjson_obj_get(shape, "normalX"))) { + s.data.plane.normal[0] = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "normalY"))) { + s.data.plane.normal[1] = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "normalZ"))) { + s.data.plane.normal[2] = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(shape, "distance"))) { + s.data.plane.distance = (float_t)yyjson_get_num(v); + } + entityPhysicsSetShape(mgr, entityId, componentId, s); + } else { + errorThrow("Unknown physics shape type '%s'", shapeTypeName); + } + } + + yyjson_val *v; + vec3 velocity; + entityPhysicsGetVelocity(mgr, entityId, componentId, velocity); + bool_t velocityChanged = false; + if((v = yyjson_obj_get(json, "velocityX"))) { + velocity[0] = (float_t)yyjson_get_num(v); + velocityChanged = true; + } + if((v = yyjson_obj_get(json, "velocityY"))) { + velocity[1] = (float_t)yyjson_get_num(v); + velocityChanged = true; + } + if((v = yyjson_obj_get(json, "velocityZ"))) { + velocity[2] = (float_t)yyjson_get_num(v); + velocityChanged = true; + } + if(velocityChanged) { + entityPhysicsSetVelocity(mgr, entityId, componentId, velocity); + } + + if((v = yyjson_obj_get(json, "gravityScale"))) { + phys->gravityScale = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "onGround"))) { + phys->onGround = yyjson_get_bool(v); + } + + errorOk(); +} diff --git a/src/dusk/entity/component/physics/entityphysics.h b/src/dusk/entity/component/physics/entityphysics.h index eb66f1b6..abf8b059 100644 --- a/src/dusk/entity/component/physics/entityphysics.h +++ b/src/dusk/entity/component/physics/entityphysics.h @@ -9,6 +9,8 @@ #include "entity/entitybase.h" #include "physics/physicsshape.h" #include "physics/physicsbodytype.h" +#include "error/error.h" +#include "yyjson.h" typedef struct { physicsbodytype_t type; @@ -170,3 +172,45 @@ physicsbodytype_t entityPhysicsGetBodyType( const entityid_t entityId, const componentid_t componentId ); + +/** + * Serializes the physics body into the given JSON object: "bodyType" + * ("STATIC"/"DYNAMIC"/"KINEMATIC"), "shape" (an object with its own + * "type" of "CUBE"/"SPHERE"/"CAPSULE"/"PLANE" plus that shape's fields), + * "velocity" ({"x","y","z"}), "gravityScale", and "onGround". Fails if + * the body's shape is PHYSICS_SHAPE_CUSTOM -- custom shapes carry a + * callback and opaque userData pointer that cannot be represented in + * JSON. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityPhysicsSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads "bodyType", "shape", "velocity", "gravityScale", and "onGround" + * (see entityPhysicsSerialize()) from the given JSON object and applies + * whichever are present, leaving the rest at their current values. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityPhysicsDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h index 9c0d2294..5e74e8cf 100644 --- a/src/dusk/entity/componentlist.h +++ b/src/dusk/entity/componentlist.h @@ -16,12 +16,18 @@ // Init function (optional) // Dispose function (optional) // Render function (optional) +// Serialize function (optional) +// Deserialize function (optional) -X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL) -X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL) +X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL, + entityPositionSerialize, entityPositionDeserialize) +X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL, + entityCameraSerialize, entityCameraDeserialize) X(RENDERABLE, entityrenderable_t, renderable, - entityRenderableInit, entityRenderableDispose, NULL) -X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL) + entityRenderableInit, entityRenderableDispose, NULL, + entityRenderableSerialize, entityRenderableDeserialize) +X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL, + entityPhysicsSerialize, entityPhysicsDeserialize) // Game-specific components #include "entity/gamecomponentlist.h" diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index 70026036..52329d30 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -8,6 +8,7 @@ #include "entitymanager.h" #include "component/display/entityposition.h" #include "util/memory.h" +#include "util/string.h" #include "assert/assert.h" void entityInit(entitymanager_t *mgr, const entityid_t entityId) { @@ -72,6 +73,32 @@ componentid_t entityGetComponent( return compId; } +void entitySetName( + entitymanager_t *mgr, + const entityid_t entityId, + const char_t *name +) { + stringCopy(mgr->entities[entityId].name, name, ENTITY_NAME_MAX); +} + +const char_t *entityGetName( + entitymanager_t *mgr, + const entityid_t entityId +) { + return mgr->entities[entityId].name; +} + +entityid_t entityFindByName( + entitymanager_t *mgr, + const char_t *name +) { + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue; + if(stringEquals(mgr->entities[i].name, name)) return i; + } + return ENTITY_ID_INVALID; +} + void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) { componentid_t posComp = entityGetComponent( mgr, entityId, COMPONENT_TYPE_POSITION @@ -186,3 +213,79 @@ void entityDisposeRemove( return; } } + +errorret_t entitySerialize( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + assertNotNull(mgr, "Entity manager cannot be null"); + assertNotNull(doc, "JSON document cannot be null"); + assertNotNull(json, "JSON object cannot be null"); + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + + const char_t *name = mgr->entities[entityId].name; + if(name[0] != '\0') yyjson_mut_obj_add_strcpy(doc, json, "name", name); + + yyjson_mut_val *components = yyjson_mut_arr(doc); + yyjson_mut_obj_add_val(doc, json, "components", components); + + for(componenttype_t type = 1; type < COMPONENT_TYPE_COUNT; type++) { + componentid_t componentId = entityGetComponent(mgr, entityId, type); + if(componentId == COMPONENT_ID_INVALID) continue; + + yyjson_mut_val *component = yyjson_mut_obj(doc); + yyjson_mut_arr_add_val(components, component); + yyjson_mut_obj_add_str( + doc, component, "type", COMPONENT_DEFINITIONS[type].enumName + ); + + errorChain( + componentSerialize(mgr, entityId, componentId, type, doc, component) + ); + } + + errorOk(); +} + +errorret_t entityDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_val *json +) { + assertNotNull(mgr, "Entity manager cannot be null"); + assertNotNull(json, "JSON object cannot be null"); + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + + yyjson_val *nameVal = yyjson_obj_get(json, "name"); + if(nameVal) entitySetName(mgr, entityId, yyjson_get_str(nameVal)); + + yyjson_val *components = yyjson_obj_get(json, "components"); + if(!components) errorOk(); + + size_t idx, max; + yyjson_val *component; + yyjson_arr_foreach(components, idx, max, component) { + yyjson_val *typeVal = yyjson_obj_get(component, "type"); + assertNotNull(typeVal, "Component JSON entry missing 'type' field"); + const char_t *typeName = yyjson_get_str(typeVal); + + componenttype_t type = COMPONENT_TYPE_NULL; + for(componenttype_t i = 1; i < COMPONENT_TYPE_COUNT; i++) { + if(!stringEquals(COMPONENT_DEFINITIONS[i].enumName, typeName)) continue; + type = i; + break; + } + if(type == COMPONENT_TYPE_NULL) { + errorThrow("Unknown component type '%s'", typeName); + } + + componentid_t componentId = entityAddComponent(mgr, entityId, type); + errorChain( + componentDeserialize(mgr, entityId, componentId, type, component) + ); + } + + errorOk(); +} diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h index b67a441c..9449d920 100644 --- a/src/dusk/entity/entity.h +++ b/src/dusk/entity/entity.h @@ -7,11 +7,14 @@ #pragma once #include "component.h" +#include "error/error.h" +#include "yyjson.h" #define ENTITY_STATE_ACTIVE (1 << 0) #define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5 #define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5 +#define ENTITY_NAME_MAX 32 typedef void (*entitycallback_t)( entitymanager_t *mgr, @@ -21,6 +24,7 @@ typedef void (*entitycallback_t)( ); typedef struct { + char_t name[ENTITY_NAME_MAX]; uint8_t state; uint8_t updateCount; uint8_t disposeCount; @@ -69,6 +73,47 @@ componentid_t entityGetComponent( const componenttype_t type ); +/** + * Sets an entity's name. Names are optional -- an entity's name is empty + * by default (entityInit zeroes it) -- but are required to make the + * entity referenceable by name, e.g. as a parent in POSITION's "parent" + * JSON field (see entityPositionSetParent()). + * + * @param mgr The entity manager that owns the entity. + * @param entityId The ID of the entity to name. + * @param name The name to give the entity. Must be shorter than + * ENTITY_NAME_MAX. + */ +void entitySetName( + entitymanager_t *mgr, + const entityid_t entityId, + const char_t *name +); + +/** + * Gets an entity's name, or an empty string if it has none. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The ID of the entity to get the name of. + * @return The entity's name. + */ +const char_t *entityGetName( + entitymanager_t *mgr, + const entityid_t entityId +); + +/** + * Finds the first active entity with the given name. + * + * @param mgr The entity manager to search. + * @param name The name to search for. + * @return The entity ID, or ENTITY_ID_INVALID if no active entity matches. + */ +entityid_t entityFindByName( + entitymanager_t *mgr, + const char_t *name +); + /** * Runs all registered update callbacks for the entity. * @@ -156,3 +201,47 @@ void entityDisposeRemove( const entityid_t entityId, const entitycallback_t callback ); + +/** + * Serializes an entity's name (if set) and components into the given + * JSON object, writing an optional "name" string and a "components" + * array of { "type": , ...fields } entries. Only component + * types the entity actually has are written; each entry's extra fields + * come from that component type's serialize callback (see + * componentSerialize()). "name" is omitted entirely for unnamed + * entities. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The ID of the entity to serialize. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the entity's "name"/"components" + * keys into. + * @return Error state. + */ +errorret_t entitySerialize( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Deserializes an entity's optional "name" and its components from the + * given JSON object's "components" array. For each component entry, + * adds a component of the type named by its "type" field (matched + * against COMPONENT_DEFINITIONS[].enumName) and hands the entry to that + * component type's deserialize callback (see componentDeserialize()). + * No-op for "components" if json has no such array. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The ID of the entity to deserialize into. Must not + * already have any of the components named in json. + * @param json The JSON object to read the entity's "name"/"components" + * keys from. + * @return Error state. + */ +errorret_t entityDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_val *json +); diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 9bc27f39..23ac9740 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -6,6 +6,7 @@ #include "scene.h" #include "assert/assert.h" #include "util/memory.h" +#include "util/string.h" #include "time/time.h" #include "display/screen/screen.h" #include "display/shader/shaderunlit.h" @@ -190,3 +191,134 @@ errorret_t sceneDispose(void) { } errorOk(); } + +errorret_t sceneSerialize( + const sceneid_t id, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB"); + assertTrue(SCENE_MANAGER.scenes[id].used, "Scene is not in use"); + assertNotNull(doc, "JSON document cannot be null"); + assertNotNull(json, "JSON object cannot be null"); + + entitymanager_t *mgr = &SCENE_MANAGER.scenes[id].entities; + + yyjson_mut_val *entities = yyjson_mut_arr(doc); + yyjson_mut_obj_add_val(doc, json, "entities", entities); + + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue; + + yyjson_mut_val *entity = yyjson_mut_obj(doc); + yyjson_mut_arr_add_val(entities, entity); + + errorChain(entitySerialize(mgr, i, doc, entity)); + + // entityPositionSerialize() doesn't know about sibling entities' names, + // so the "parent" cross-reference is written here instead, onto the + // POSITION object entitySerialize() just built. + componentid_t posComp = entityGetComponent(mgr, i, COMPONENT_TYPE_POSITION); + if(posComp == COMPONENT_ID_INVALID) continue; + entityid_t parentEntityId = entityPositionGetParent(mgr, i, posComp); + if(parentEntityId == ENTITY_ID_INVALID) continue; + const char_t *parentName = entityGetName(mgr, parentEntityId); + if(parentName[0] == '\0') continue; + + yyjson_mut_val *components = yyjson_mut_obj_get(entity, "components"); + size_t compIdx, compMax; + yyjson_mut_val *component; + yyjson_mut_arr_foreach(components, compIdx, compMax, component) { + yyjson_mut_val *typeVal = yyjson_mut_obj_get(component, "type"); + if(!stringEquals(yyjson_mut_get_str(typeVal), "POSITION")) continue; + yyjson_mut_obj_add_strcpy(doc, component, "parent", parentName); + break; + } + } + + errorOk(); +} + +errorret_t sceneDeserialize( + const sceneid_t id, + yyjson_val *json +) { + assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB"); + assertTrue(SCENE_MANAGER.scenes[id].used, "Scene is not in use"); + assertNotNull(json, "JSON object cannot be null"); + + entitymanager_t *mgr = &SCENE_MANAGER.scenes[id].entities; + + yyjson_val *entities = yyjson_obj_get(json, "entities"); + if(!entities) errorOk(); + + entityid_t indexToEntityId[ENTITY_COUNT_MAX]; + size_t idx, max; + yyjson_val *entity; + yyjson_arr_foreach(entities, idx, max, entity) { + entityid_t entityId = entityManagerAdd(mgr); + indexToEntityId[idx] = entityId; + errorChain(entityDeserialize(mgr, entityId, entity)); + } + + // Second pass: every entity now exists (and is findable by name), so + // POSITION's "parent" references (see entityPositionDeserialize()) can + // be resolved regardless of which order parent/child appeared in. + yyjson_arr_foreach(entities, idx, max, entity) { + errorChain(sceneDeserializeResolveParent( + mgr, indexToEntityId[idx], entity + )); + } + + errorOk(); +} + +errorret_t sceneDeserializeResolveParent( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_val *entityJson +) { + yyjson_val *components = yyjson_obj_get(entityJson, "components"); + if(!components) errorOk(); + + yyjson_val *positionObj = NULL; + size_t idx, max; + yyjson_val *component; + yyjson_arr_foreach(components, idx, max, component) { + yyjson_val *typeVal = yyjson_obj_get(component, "type"); + if(!stringEquals(yyjson_get_str(typeVal), "POSITION")) continue; + positionObj = component; + break; + } + if(!positionObj) errorOk(); + + yyjson_val *parentVal = yyjson_obj_get(positionObj, "parent"); + if(!parentVal) errorOk(); + + const char_t *parentName = yyjson_get_str(parentVal); + entityid_t parentEntityId = entityFindByName(mgr, parentName); + if(parentEntityId == ENTITY_ID_INVALID || parentEntityId == entityId) { + errorThrow("POSITION 'parent' references unknown entity '%s'", parentName); + } + + componentid_t componentId = entityGetComponent( + mgr, entityId, COMPONENT_TYPE_POSITION + ); + componentid_t parentComponentId = entityGetComponent( + mgr, parentEntityId, COMPONENT_TYPE_POSITION + ); + if( + componentId == COMPONENT_ID_INVALID || + parentComponentId == COMPONENT_ID_INVALID + ) { + errorThrow( + "POSITION 'parent' reference '%s' requires both entities to have a " + "POSITION component", parentName + ); + } + + entityPositionSetParent( + mgr, entityId, componentId, parentEntityId, parentComponentId + ); + errorOk(); +} diff --git a/src/dusk/scene/scene.h b/src/dusk/scene/scene.h index fafdf910..db9daec9 100644 --- a/src/dusk/scene/scene.h +++ b/src/dusk/scene/scene.h @@ -9,6 +9,8 @@ #include "scenebase.h" #include "entity/entitymanager.h" #include "physics/physicsworld.h" +#include "error/error.h" +#include "yyjson.h" typedef struct { bool_t used; @@ -119,3 +121,58 @@ errorret_t sceneRender(void); * @return An error if the dispose failed, or errorOk() if it succeeded. */ errorret_t sceneDispose(void); + +/** + * Serializes a scene's active entities into the given JSON object, + * writing an "entities" array of objects (see entitySerialize()). Also + * writes a "parent" name reference onto any entity's POSITION object + * whose entityPositionGetParent() resolves to a named entity (silently + * omitted if the parent has no name -- see entitySetName()). + * + * @param id The ID of the scene to serialize. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the scene's "entities" key into. + * @return Error state. + */ +errorret_t sceneSerialize( + const sceneid_t id, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Deserializes entities from the given JSON object's "entities" array into + * a scene, creating one new entity per array entry (see + * entityDeserialize()), then resolves every entity's POSITION "parent" + * name reference (see sceneDeserializeResolveParent()) now that all + * entities exist and are named. No-op if json has no "entities" array. + * + * @param id The ID of the scene to deserialize into. + * @param json The JSON object to read the scene's "entities" key from. + * @return Error state. + */ +errorret_t sceneDeserialize( + const sceneid_t id, + yyjson_val *json +); + +/** + * Internal. Resolves a single entity's POSITION "parent" reference (a + * name string, see entityPositionDeserialize()), if present, into a real + * parent link via entityPositionSetParent(). Called by sceneDeserialize() + * once every entity in the scene has been created and named, since + * resolving a name to an entityId requires entityFindByName() to be able + * to see the whole scene. No-op if entityJson has no POSITION component + * or its POSITION has no "parent" field. + * + * @param mgr The entity manager the entity belongs to. + * @param entityId The entity ID to resolve the parent reference for. + * @param entityJson The entity's own JSON object (as found in the + * scene's "entities" array). + * @return Error state. + */ +errorret_t sceneDeserializeResolveParent( + entitymanager_t *mgr, + const entityid_t entityId, + yyjson_val *entityJson +); diff --git a/src/duskrpg/entity/component/overworld/entityplayer.c b/src/duskrpg/entity/component/overworld/entityplayer.c index 63ef4991..8f1adc3e 100644 --- a/src/duskrpg/entity/component/overworld/entityplayer.c +++ b/src/duskrpg/entity/component/overworld/entityplayer.c @@ -9,12 +9,18 @@ #include "entity/entitymanager.h" #include "entity/component/physics/entityphysics.h" #include "entity/component/display/entitycamera.h" +#include "entity/component/display/entityposition.h" #include "input/input.h" #include "util/memory.h" #define ENTITY_PLAYER_MOVE_SPEED_DEFAULT 4.0f #define ENTITY_PLAYER_JUMP_IMPULSE_DEFAULT 6.0f +// Below this squared magnitude, movement input is treated as "not moving" +// and the player keeps facing whichever way it last faced, rather than +// snapping to some default orientation. +#define ENTITY_PLAYER_MOVE_ROTATE_THRESHOLD_SQ 0.0001f + void entityPlayerInit( entitymanager_t *mgr, const entityid_t entityId, @@ -85,4 +91,58 @@ void entityPlayerUpdate( velocity[0] = moveDir[0] * player->moveSpeed; velocity[2] = moveDir[1] * player->moveSpeed; entityPhysicsSetVelocity(mgr, entityId, physComp, velocity); + + // Face the direction of movement. Only updated while actually moving -- + // releasing input leaves the player facing whichever way it last moved. + float_t moveDirMagSq = moveDir[0] * moveDir[0] + moveDir[1] * moveDir[1]; + if(moveDirMagSq < ENTITY_PLAYER_MOVE_ROTATE_THRESHOLD_SQ) return; + + componentid_t posComp = entityGetComponent( + mgr, entityId, COMPONENT_TYPE_POSITION + ); + if(posComp == COMPONENT_ID_INVALID) return; + + vec3 rotation; + entityPositionGetLocalRotation(mgr, entityId, posComp, rotation); + // The player's "front" is +local Z, not -Z (entityCameraGetForward's + // convention is for cameras, which look down -Z -- the player model + // faces the opposite way), which for a pure yaw rotation works out to + // (sin(yaw), 0, cos(yaw)) -- invert that to solve for the yaw facing + // moveDir. + rotation[1] = atan2f(moveDir[0], moveDir[1]); + entityPositionSetLocalRotation(mgr, entityId, posComp, rotation); +} + +errorret_t entityPlayerSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +) { + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + + yyjson_mut_obj_add_real(doc, json, "moveSpeed", player->moveSpeed); + yyjson_mut_obj_add_real(doc, json, "jumpImpulse", player->jumpImpulse); + + errorOk(); +} + +errorret_t entityPlayerDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +) { + entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); + + yyjson_val *v; + if((v = yyjson_obj_get(json, "moveSpeed"))) { + player->moveSpeed = (float_t)yyjson_get_num(v); + } + if((v = yyjson_obj_get(json, "jumpImpulse"))) { + player->jumpImpulse = (float_t)yyjson_get_num(v); + } + + errorOk(); } diff --git a/src/duskrpg/entity/component/overworld/entityplayer.h b/src/duskrpg/entity/component/overworld/entityplayer.h index 183b9a45..6ae34ac4 100644 --- a/src/duskrpg/entity/component/overworld/entityplayer.h +++ b/src/duskrpg/entity/component/overworld/entityplayer.h @@ -7,6 +7,8 @@ #pragma once #include "entity/entitybase.h" +#include "error/error.h" +#include "yyjson.h" typedef struct { float_t moveSpeed; @@ -48,8 +50,14 @@ entityplayer_t *entityPlayerGet( * and drives the entity's physics velocity, relative to the current * camera's horizontal facing (so "up" always moves away from the camera * and "left"/"right" strafe relative to its view), rather than fixed world - * axes. No-op if the entity has no physics component. Registered - * automatically as an update callback by entityPlayerInit. + * axes. No-op if the entity has no physics component. + * + * Also yaws the entity's position component (if any) to face the + * direction of movement. Facing is only updated while there's actual + * movement input -- releasing input leaves the entity facing whichever + * way it last moved, rather than snapping to a default orientation. + * + * Registered automatically as an update callback by entityPlayerInit. * * @param mgr The entity manager that owns the entity. * @param entityId The entity ID. @@ -62,3 +70,40 @@ void entityPlayerUpdate( const componentid_t componentId, void *user ); + +/** + * Serializes the player's "moveSpeed" and "jumpImpulse" into the given + * JSON object. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param doc The mutable JSON document to allocate values from. + * @param json The JSON object to write the component's fields into. + * @return Error state. + */ +errorret_t entityPlayerSerialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_mut_doc *doc, + yyjson_mut_val *json +); + +/** + * Reads "moveSpeed" and "jumpImpulse" (see entityPlayerSerialize()) from + * the given JSON object and applies whichever are present, leaving the + * rest at their current values. + * + * @param mgr The entity manager that owns the entity. + * @param entityId The entity ID. + * @param componentId The component ID. + * @param json The JSON object to read the component's fields from. + * @return Error state. + */ +errorret_t entityPlayerDeserialize( + entitymanager_t *mgr, + const entityid_t entityId, + const componentid_t componentId, + yyjson_val *json +); diff --git a/src/duskrpg/entity/gamecomponentlist.h b/src/duskrpg/entity/gamecomponentlist.h index 7592b5b2..92fe2b4e 100644 --- a/src/duskrpg/entity/gamecomponentlist.h +++ b/src/duskrpg/entity/gamecomponentlist.h @@ -15,5 +15,8 @@ // Init function (optional) // Dispose function (optional) // Render function (optional) +// Serialize function (optional) +// Deserialize function (optional) -X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL) +X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL, + entityPlayerSerialize, entityPlayerDeserialize) diff --git a/src/duskrpg/game/game.c b/src/duskrpg/game/game.c index eed74381..baef87b6 100644 --- a/src/duskrpg/game/game.c +++ b/src/duskrpg/game/game.c @@ -8,9 +8,33 @@ #include "game/game.h" #include "scene/scene.h" #include "scene/overworldscene.h" +#include "assert/assert.h" +#include "asset/asset.h" +#include "yyjson.h" + +#define GAME_TEST_SCENE_ASSET "scenes/test.json" errorret_t gameInit(void) { - sceneSetActive(overworldSceneCreate()); + // overworldSceneCreate(); + + sceneid_t testSceneId = sceneCreate(); + + // Exercises sceneDeserialize() end-to-end: a camera looking at a + // colored cube that falls under gravity onto a static ground plane, + // described entirely as JSON rather than built with the entity API. + assetentry_t *sceneEntry = assetLock( + GAME_TEST_SCENE_ASSET, ASSET_LOADER_TYPE_JSON, NULL + ); + errorret_t ret = assetRequireLoaded(sceneEntry); + if(errorIsOk(ret)) { + ret = sceneDeserialize( + testSceneId, yyjson_doc_get_root(sceneEntry->data.json) + ); + } + assetUnlockEntry(sceneEntry); + errorChain(ret); + + sceneSetActive(testSceneId); errorOk(); } diff --git a/test/entity/test_entitymanager.c b/test/entity/test_entitymanager.c index 3317ea40..f4ad1759 100644 --- a/test/entity/test_entitymanager.c +++ b/test/entity/test_entitymanager.c @@ -71,6 +71,32 @@ static void test_entityAddGetDisposeComponent(void **state) { assert_int_equal(memoryGetAllocatedCount(), 0); } +static void test_entitySetGetFindByName(void **state) { + entitymanager_t mgr; + entityManagerInit(&mgr); + + entityid_t a = entityManagerAdd(&mgr); + entityid_t b = entityManagerAdd(&mgr); + + assert_string_equal(entityGetName(&mgr, a), ""); + assert_int_equal(entityFindByName(&mgr, "camera"), ENTITY_ID_INVALID); + + entitySetName(&mgr, a, "camera"); + assert_string_equal(entityGetName(&mgr, a), "camera"); + assert_int_equal(entityFindByName(&mgr, "camera"), a); + assert_int_equal(entityFindByName(&mgr, "nonexistent"), ENTITY_ID_INVALID); + + // Renaming doesn't leave the old name findable, and other entities stay + // unaffected. + entitySetName(&mgr, a, "player"); + assert_int_equal(entityFindByName(&mgr, "camera"), ENTITY_ID_INVALID); + assert_int_equal(entityFindByName(&mgr, "player"), a); + assert_string_equal(entityGetName(&mgr, b), ""); + + entityManagerDispose(&mgr); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + static void test_entityAddDuplicateComponentAsserts(void **state) { entitymanager_t mgr; entityManagerInit(&mgr); @@ -90,6 +116,7 @@ int main(int argc, char **argv) { cmocka_unit_test(test_entityManagerAddIsolatesEntities), cmocka_unit_test(test_entityManagerFullAsserts), cmocka_unit_test(test_entityAddGetDisposeComponent), + cmocka_unit_test(test_entitySetGetFindByName), cmocka_unit_test(test_entityAddDuplicateComponentAsserts), }; diff --git a/test/entity/test_entityposition.c b/test/entity/test_entityposition.c index 598b0784..a8b24bc3 100644 --- a/test/entity/test_entityposition.c +++ b/test/entity/test_entityposition.c @@ -94,6 +94,50 @@ static void test_entityPositionParentChildWorldTransform(void **state) { assert_int_equal(memoryGetAllocatedCount(), 0); } +static void test_entityPositionDeserializeLookAt(void **state) { + entitymanager_t mgr; + entityManagerInit(&mgr); + + entityid_t entity = entityManagerAdd(&mgr); + componentid_t comp = entityAddComponent( + &mgr, entity, COMPONENT_TYPE_POSITION + ); + + const char_t *json = + "{ \"x\": 0, \"y\": 5, \"z\": 10, " + "\"lookAt\": { \"x\": 0, \"y\": 0, \"z\": 0 } }"; + yyjson_doc *doc = yyjson_read(json, strlen(json), YYJSON_READ_NOFLAG); + assert_non_null(doc); + + errorret_t ret = entityPositionDeserialize( + &mgr, entity, comp, yyjson_doc_get_root(doc) + ); + yyjson_doc_free(doc); + assert_true(errorIsOk(ret)); + + // The eye point decoded back out of localTransform should match "x"/"y"/"z". + vec3 eye; + entityPositionGetLocalPosition(&mgr, entity, comp, eye); + assert_float_equal(eye[0], 0.0f, 0.0001f); + assert_float_equal(eye[1], 5.0f, 0.0001f); + assert_float_equal(eye[2], 10.0f, 0.0001f); + + // Forward (-local Z, see entityCameraGetForward's convention) should + // point from eye toward the lookAt target (the origin here). + mat4 transform; + entityPositionGetLocalTransform(&mgr, entity, comp, transform); + vec3 forward = { -transform[2][0], -transform[2][1], -transform[2][2] }; + glm_vec3_normalize(forward); + + vec3 toTarget = { -eye[0], -eye[1], -eye[2] }; + glm_vec3_normalize(toTarget); + + assert_float_equal(glm_vec3_dot(forward, toTarget), 1.0f, 0.001f); + + entityManagerDispose(&mgr); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + static void test_entityPositionDisposeDeep(void **state) { entitymanager_t mgr; entityManagerInit(&mgr); @@ -123,6 +167,7 @@ int main(int argc, char **argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_entityPositionLocalGetSet), cmocka_unit_test(test_entityPositionParentChildWorldTransform), + cmocka_unit_test(test_entityPositionDeserializeLookAt), cmocka_unit_test(test_entityPositionDisposeDeep), }; diff --git a/test/scene/test_scene.c b/test/scene/test_scene.c index 94eef9c6..7ce8e2f6 100644 --- a/test/scene/test_scene.c +++ b/test/scene/test_scene.c @@ -10,6 +10,8 @@ #include "time/time.h" #include "scene/scene.h" #include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "yyjson.h" static void countingUpdateCallback( entitymanager_t *mgr, @@ -137,6 +139,82 @@ static void test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames(void **state) assert_int_equal(memoryGetAllocatedCount(), 0); } +static void test_sceneSerializeDeserializeParentLink(void **state) { + sceneInit(); + + sceneid_t sceneA = sceneCreate(); + entitymanager_t *mgrA = sceneGetEntities(sceneA); + + entityid_t parent = entityManagerAdd(mgrA); + componentid_t parentPos = entityAddComponent( + mgrA, parent, COMPONENT_TYPE_POSITION + ); + entitySetName(mgrA, parent, "root"); + entityPositionSetLocalPosition(mgrA, parent, parentPos, (vec3){ 10, 0, 0 }); + + entityid_t child = entityManagerAdd(mgrA); + componentid_t childPos = entityAddComponent( + mgrA, child, COMPONENT_TYPE_POSITION + ); + entityPositionSetLocalPosition(mgrA, child, childPos, (vec3){ 1, 0, 0 }); + entityPositionSetParent(mgrA, child, childPos, parent, parentPos); + + vec3 childWorldBefore; + entityPositionGetWorldPosition(mgrA, child, childPos, childWorldBefore); + assert_float_equal(childWorldBefore[0], 11.0f, 0.0001f); + + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + errorret_t ret = sceneSerialize(sceneA, doc, root); + assert_true(errorIsOk(ret)); + + size_t len = 0; + char_t *jsonStr = yyjson_mut_write(doc, 0, &len); + assert_non_null(jsonStr); + yyjson_mut_doc_free(doc); + + yyjson_doc *readDoc = yyjson_read(jsonStr, len, 0); + free(jsonStr); + assert_non_null(readDoc); + + sceneid_t sceneB = sceneCreate(); + entitymanager_t *mgrB = sceneGetEntities(sceneB); + ret = sceneDeserialize(sceneB, yyjson_doc_get_root(readDoc)); + yyjson_doc_free(readDoc); + assert_true(errorIsOk(ret)); + + entityid_t reloadedParent = entityFindByName(mgrB, "root"); + assert_true(reloadedParent != ENTITY_ID_INVALID); + + entityid_t reloadedChild = ENTITY_ID_INVALID; + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if(!(mgrB->entities[i].state & ENTITY_STATE_ACTIVE)) continue; + if(i == reloadedParent) continue; + reloadedChild = i; + break; + } + assert_true(reloadedChild != ENTITY_ID_INVALID); + + componentid_t reloadedChildPos = entityGetComponent( + mgrB, reloadedChild, COMPONENT_TYPE_POSITION + ); + assert_true(reloadedChildPos != COMPONENT_ID_INVALID); + assert_int_equal( + entityPositionGetParent(mgrB, reloadedChild, reloadedChildPos), + reloadedParent + ); + + vec3 childWorldAfter; + entityPositionGetWorldPosition( + mgrB, reloadedChild, reloadedChildPos, childWorldAfter + ); + assert_float_equal(childWorldAfter[0], childWorldBefore[0], 0.0001f); + + sceneDispose(); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + static void test_sceneDestroyClearsActive(void **state) { sceneInit(); @@ -157,6 +235,7 @@ int main(int argc, char **argv) { cmocka_unit_test(test_sceneEntitiesAreIsolatedPerScene), cmocka_unit_test(test_sceneFixedUpdateOnlyTicksActiveScene), cmocka_unit_test(test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames), + cmocka_unit_test(test_sceneSerializeDeserializeParentLink), cmocka_unit_test(test_sceneDestroyClearsActive), };