techkaczynski.dev / code

summaryrefslogtreecommitdiff
path: root/save.c
diff options
context:
space:
mode:
authortech kaczynski <tk@techkaczynski.dev>2026-09-22 22:38:57 +1000
committertech kaczynski <tk@techkaczynski.dev>2026-09-22 22:38:57 +1000
commit08cb8d6a585dd552b69a488d9db1be123f01467a (patch)
tree77d50ce30bbc102b08486929ba654b5bef70af43 /save.c
initial commit.
Diffstat (limited to 'save.c')
-rw-r--r--save.c1056
1 files changed, 1056 insertions, 0 deletions
diff --git a/save.c b/save.c
new file mode 100644
index 0000000..9d46fe3
--- /dev/null
+++ b/save.c
@@ -0,0 +1,1056 @@
+#include "save.h"
+#include "ent.h"
+
+typedef struct {
+ i64 size_pos;
+ i64 data_pos;
+} SaveChunk;
+
+typedef struct {
+ u64 next_pid;
+} SaveLoadContext;
+
+static bool
+read_u8_le(FILE *fp, u8 *val) {
+ return fread(val, 1, 1, fp) == 1;
+}
+
+static bool
+write_u8_le(FILE *fp, u8 val) {
+ return fwrite(&val, 1, 1, fp) == 1;
+}
+
+static bool
+read_u16_le(FILE *fp, u16 *val) {
+ u8 bytes[2];
+
+ if (fread(bytes, 1, 2, fp) != 2) {
+ return false;
+ }
+
+ *val = (u16)bytes[0]
+ | (u16)bytes[1] << 8;
+
+ return true;
+}
+
+static bool
+write_u16_le(FILE *fp, u16 val) {
+ u8 bytes[2] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ };
+
+ return fwrite(bytes, 1, 2, fp) == 2;
+}
+
+static bool
+read_u32_le(FILE *fp, u32 *val) {
+ u8 bytes[4];
+
+ if (fread(bytes, 1, 4, fp) != 4) {
+ return false;
+ }
+
+ *val = (u32)bytes[0] << 0
+ | (u32)bytes[1] << 8
+ | (u32)bytes[2] << 16
+ | (u32)bytes[3] << 24;
+
+ return true;
+}
+
+static bool
+write_u32_le(FILE *fp, u32 val) {
+ u8 bytes[4] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ (u8)(val >> 16),
+ (u8)(val >> 24),
+ };
+
+ return fwrite(bytes, 1, 4, fp) == 4;
+}
+
+static bool
+read_u64_le(FILE *fp, u64 *val) {
+ u8 bytes[8];
+
+ if (fread(bytes, 1, 8, fp) != 8) {
+ return false;
+ }
+
+ *val = (u64)bytes[0] << 0
+ | (u64)bytes[1] << 8
+ | (u64)bytes[2] << 16
+ | (u64)bytes[3] << 24
+ | (u64)bytes[4] << 32
+ | (u64)bytes[5] << 40
+ | (u64)bytes[6] << 48
+ | (u64)bytes[7] << 56;
+
+ return true;
+}
+
+static bool
+write_u64_le(FILE *fp, u64 val) {
+ u8 bytes[8] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ (u8)(val >> 16),
+ (u8)(val >> 24),
+ (u8)(val >> 32),
+ (u8)(val >> 40),
+ (u8)(val >> 48),
+ (u8)(val >> 56),
+ };
+
+ return fwrite(bytes, 1, 8, fp) == 8;
+}
+
+static bool
+read_f32_le(FILE *fp, f32 *val) {
+ u32 bits;
+ if (!read_u32_le(fp, &bits)) {
+ return false;
+ }
+
+ memcpy(val, &bits, sizeof(*val));
+ return true;
+}
+
+static bool
+write_f32_le(FILE *fp, f32 value) {
+ u32 bits;
+ memcpy(&bits, &value, 4);
+ return write_u32_le(fp, bits);
+}
+
+static bool
+read_vec3(FILE *fp, Vec3 *val) {
+ return read_f32_le(fp, &val->x) &&
+ read_f32_le(fp, &val->y) &&
+ read_f32_le(fp, &val->z);
+}
+
+static bool
+write_vec3(FILE *fp, Vec3 val) {
+ return write_f32_le(fp, val.x) &&
+ write_f32_le(fp, val.y) &&
+ write_f32_le(fp, val.z);
+}
+
+static bool
+read_quat(FILE *fp, Quaternion *val) {
+ return read_f32_le(fp, &val->x) &&
+ read_f32_le(fp, &val->y) &&
+ read_f32_le(fp, &val->z) &&
+ read_f32_le(fp, &val->w);
+}
+
+static bool
+write_quat(FILE *fp, Quaternion val) {
+ return write_f32_le(fp, val.x) &&
+ write_f32_le(fp, val.y) &&
+ write_f32_le(fp, val.z) &&
+ write_f32_le(fp, val.w);
+}
+
+static bool
+write_byte_arr(FILE *fp, const void *data, u32 len) {
+ return fwrite(data, 1, len, fp) == len;
+}
+
+/* some entities are 'transient', so we need a persistent entity count */
+static u32
+persistent_entity_count(void) {
+ u32 cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ if (entity_data()[i].pid != 0) {
+ cnt++;
+ }
+ }
+
+ return cnt;
+}
+
+static bool
+chunk_begin(FILE *fp, const char label[4], SaveChunk *chunk) {
+ if (!write_byte_arr(fp, label, 4)) {
+ return false;
+ }
+
+ chunk->size_pos = ftell(fp);
+ if (chunk->size_pos < 0 || !write_u32_le(fp, 0)) {
+ return false;
+ }
+
+ chunk->data_pos = ftell(fp);
+ return chunk->data_pos >= 0;
+}
+
+static bool
+chunk_end(FILE *fp, SaveChunk *chunk) {
+ u32 end_pos = ftell(fp);
+ if (end_pos < chunk->data_pos) {
+ return false;
+ }
+
+ u64 size = (u64)(end_pos - chunk->data_pos);
+ if (size > UINT32_MAX) {
+ return false;
+ }
+
+ if (fseek(fp, chunk->size_pos, SEEK_SET) != 0 ||
+ !write_u32_le(fp, (u32)size) ||
+ fseek(fp, end_pos, SEEK_SET) != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+read_header(FILE *fp) {
+ char magic[4];
+ u16 major, minor;
+
+ if (fread(magic, 1, 4, fp) != 4 ||
+ !read_u16_le(fp, &major) ||
+ !read_u16_le(fp, &minor)) {
+ return false;
+ }
+
+ if (memcmp(magic, "FGSF", 4) != 0) {
+ TraceLog(LOG_ERROR, "invalid save file magic.");
+ return false;
+ }
+
+ if (major != SAVE_VERSION_MAJOR ||
+ minor != SAVE_VERSION_MINOR) {
+ TraceLog(LOG_ERROR, "unsupported save file version %u.%u.",
+ major, minor);
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+write_header(FILE *fp) {
+ return fwrite("FGSF", 1, 4, fp) == 4 &&
+ write_u16_le(fp, SAVE_VERSION_MAJOR) &&
+ write_u16_le(fp, SAVE_VERSION_MINOR);
+}
+
+static bool
+read_entity_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u64 next_pid;
+ u32 entity_cnt;
+
+ if (!read_u64_le(fp, &next_pid) ||
+ !read_u32_le(fp, &entity_cnt)) {
+ return false;
+ }
+
+ if (next_pid == 0) {
+ return false;
+ }
+
+ ctx->next_pid = next_pid;
+
+ for (u32 i = 0; i < entity_cnt; ++i) {
+ u64 pid = 0;
+ read_u64_le(fp, &pid);
+
+ if (pid == 0 || pid >= next_pid) {
+ TraceLog(LOG_ERROR, "entity PID invalid.");
+ return false;
+ }
+
+ u32 name_len = 0;
+ read_u32_le(fp, &name_len);
+
+ u64 id = entity_restore(pid);
+ if (!id) {
+ TraceLog(LOG_ERROR, "entity pid already exists.");
+ return false;
+ }
+ Entity *ent = entity_get(id);
+ ent->name = malloc(name_len + 1);
+
+ if (name_len > 0 &&
+ fread(ent->name, 1, name_len, fp) != name_len) {
+ TraceLog(LOG_ERROR, "failed to read entity name.");
+ return false;
+ }
+
+ ent->name[name_len] = '\0';
+ }
+
+ entity_system_set_next_pid(next_pid);
+ return true;
+}
+
+static bool
+write_entity_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+
+ if (!chunk_begin(fp, "ENTT", &chunk) ||
+ !write_u64_le(fp, entity_next_pid()) ||
+ !write_u32_le(fp, persistent_entity_count())) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid == 0) {
+ continue;
+ }
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, strlen(ent->name)) ||
+ !write_byte_arr(fp, ent->name, strlen(ent->name))) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_transform_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 tr_cnt;
+ read_u32_le(fp, &tr_cnt);
+
+ u64 *parent_pids = calloc(ctx->next_pid, sizeof(u64));
+ if (!parent_pids) {
+ TraceLog(LOG_FATAL, "calloc failed.");
+ }
+
+ for (u32 i = 0; i < tr_cnt; ++i) {
+ u64 pid, parent_pid;
+
+ if (!read_u64_le(fp, &pid) ||
+ !read_u64_le(fp, &parent_pid) ||
+ pid == 0 || pid >= ctx->next_pid ||
+ parent_pid >= ctx->next_pid) {
+ goto fail;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ TraceLog(LOG_ERROR, "failed to properly load entity");
+ goto fail;
+ }
+
+ if (!read_vec3(fp, &ent->tr.pos) ||
+ !read_quat(fp, &ent->tr.rot) ||
+ !read_vec3(fp, &ent->tr.scale)) {
+ goto fail;
+ }
+
+ parent_pids[pid] = parent_pid;
+ }
+
+ /* Build hierarchy only after all transforms exist. */
+ for (u64 pid = 1; pid < ctx->next_pid; ++pid) {
+ u64 parent_pid = parent_pids[pid];
+
+ if (!parent_pid) {
+ continue;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ u64 parent_id = entity_id_from_pid(parent_pid);
+
+ if (!id || !parent_id || !entity_set_parent(id, parent_id)) {
+ TraceLog(LOG_ERROR, "entity hierarchy build failed.");
+ goto fail;
+ }
+ }
+
+ free(parent_pids);
+ return true;
+
+fail:
+ free(parent_pids);
+ return false;
+}
+
+static bool
+write_transform_chunk(FILE *fp) {
+ SaveChunk chunk;
+
+ u32 tr_cnt = 0;
+ for (u32 i = 0; i < entity_count(); ++i) {
+ if (entity_data()[i].pid) {
+ tr_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "TRFM", &chunk) ||
+ !write_u32_le(fp, tr_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid) {
+ continue;
+ }
+
+ u64 parent_pid = 0;
+ if (ent->tr.parent) {
+ Entity *parent = entity_get(ent->tr.parent);
+ if (!parent) {
+ return false;
+ }
+ parent_pid = parent->pid;
+ }
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u64_le(fp, parent_pid) ||
+ !write_vec3(fp, ent->tr.pos) ||
+ !write_quat(fp, ent->tr.rot) ||
+ !write_vec3(fp, ent->tr.scale)) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_model_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 mdl_cnt;
+ if (!read_u32_le(fp, &mdl_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < mdl_cnt; ++i) {
+ u64 pid;
+ u32 key_len;
+ u8 draw;
+
+ if (!read_u64_le(fp, &pid) || !read_u32_le(fp, &key_len)) {
+ return false;
+ }
+
+ if (pid == 0 || pid >= ctx->next_pid) {
+ return false;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+
+ if (!ent || ent->mdl.has) {
+ return false;
+ }
+
+ char *key = malloc(key_len + 1);
+ if (!key) {
+ return false;
+ }
+
+ if (key_len && fread(key, 1, key_len, fp) != key_len) {
+ free(key);
+ return false;
+ }
+
+ key[key_len] = '\0';
+
+ if (!read_u8_le(fp, &draw)) {
+ free(key);
+ return false;
+ }
+
+ ent->mdl.key = key;
+ ent->mdl.draw = draw;
+ ent->mdl.has = true;
+ }
+
+ return true;
+}
+
+static bool
+write_model_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+ u32 mdl_cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid && ent->mdl.has) {
+ mdl_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "MODL", &chunk) ||
+ !write_u32_le(fp, mdl_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid || !ent->mdl.has) {
+ continue;
+ }
+
+ u32 key_len = (u32)strlen(ent->mdl.key);
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, key_len) ||
+ !write_byte_arr(fp, ent->mdl.key, key_len) ||
+ !write_u8_le(fp, ent->mdl.draw ? 1 : 0)) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_physics_shape(FILE *fp, PhysicsShape *shape) {
+ *shape = (PhysicsShape){0};
+
+ u8 tag = 0;
+ if (!read_u8_le(fp, &tag)) {
+ TraceLog(LOG_ERROR, "failed to read physics shape tag");
+ return false;
+ }
+ shape->tag = tag;
+
+ if (!read_vec3(fp, &shape->pos) ||
+ !read_quat(fp, &shape->rot) ||
+ !read_f32_le(fp, &shape->density) ||
+ !read_f32_le(fp, &shape->friction) ||
+ !read_f32_le(fp, &shape->restitution) ||
+ !read_u32_le(fp, &shape->category_bits) ||
+ !read_u32_le(fp, &shape->mask_bits)) {
+ TraceLog(LOG_ERROR, "failed to read physics shape.");
+ return false;
+ }
+
+ switch (shape->tag) {
+ case PHYS_SHAPE_BOX:
+ return read_vec3(fp, &shape->Box.sz);
+ break;
+
+ case PHYS_SHAPE_SPHERE:
+ return read_f32_le(fp, &shape->Sphere.radius);
+ break;
+
+ case PHYS_SHAPE_CAPSULE:
+ return read_f32_le(fp, &shape->Capsule.height) &&
+ read_f32_le(fp, &shape->Capsule.radius);
+ break;
+
+ case PHYS_SHAPE_CYLINDER:
+ return read_f32_le(fp, &shape->Cylinder.height) &&
+ read_f32_le(fp, &shape->Cylinder.radius);
+ break;
+
+ case PHYS_SHAPE_CONVEX_HULL:
+ if (!read_u32_le(fp, &shape->ConvexHull.point_cnt)) {
+ return false;
+ }
+
+ if (shape->ConvexHull.point_cnt) {
+ shape->ConvexHull.points =
+ calloc(shape->ConvexHull.point_cnt,
+ sizeof(Vec3));
+
+ if (!shape->ConvexHull.points) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->ConvexHull.point_cnt; ++i) {
+ if (!read_vec3(fp, &shape->ConvexHull.points[i])) {
+ return false;
+ }
+ }
+ break;
+
+ case PHYS_SHAPE_STATIC_TRIMESH:
+ if (!read_u32_le(fp, &shape->TriMesh.point_cnt) ||
+ !read_u32_le(fp, &shape->TriMesh.idx_cnt)) {
+ return false;
+ }
+
+ if (shape->TriMesh.point_cnt) {
+ shape->TriMesh.points =
+ calloc(shape->TriMesh.point_cnt,
+ sizeof(Vec3));
+
+ if (!shape->TriMesh.points) {
+ return false;
+ }
+ }
+
+ if (shape->TriMesh.idx_cnt) {
+ shape->TriMesh.indices =
+ calloc(shape->TriMesh.idx_cnt, sizeof(u32));
+
+ if (!shape->TriMesh.indices) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.point_cnt; ++i) {
+ if (!read_vec3(fp, &shape->TriMesh.points[i])) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.idx_cnt; ++i) {
+ if (!read_u32_le(fp, &shape->TriMesh.indices[i])) {
+ return false;
+ }
+ }
+ break;
+ };
+
+ return true;
+}
+
+static bool
+write_physics_shape(FILE *fp, PhysicsShape *shape) {
+ if (!write_u8_le(fp, (u8)shape->tag) ||
+ !write_vec3(fp, shape->pos) ||
+ !write_quat(fp, shape->rot) ||
+ !write_f32_le(fp, shape->density) ||
+ !write_f32_le(fp, shape->friction) ||
+ !write_f32_le(fp, shape->restitution) ||
+ !write_u32_le(fp, shape->category_bits) ||
+ !write_u32_le(fp, shape->mask_bits)) {
+ return false;
+ }
+
+ switch (shape->tag) {
+ case PHYS_SHAPE_BOX:
+ return write_vec3(fp, shape->Box.sz);
+ break;
+
+ case PHYS_SHAPE_SPHERE:
+ return write_f32_le(fp, shape->Sphere.radius);
+ break;
+
+ case PHYS_SHAPE_CAPSULE:
+ return write_f32_le(fp, shape->Capsule.height) &&
+ write_f32_le(fp, shape->Capsule.radius);
+ break;
+
+ case PHYS_SHAPE_CYLINDER:
+ return write_f32_le(fp, shape->Cylinder.height) &&
+ write_f32_le(fp, shape->Cylinder.radius);
+ break;
+
+ case PHYS_SHAPE_CONVEX_HULL:
+ if (shape->ConvexHull.point_cnt &&
+ !shape->ConvexHull.points) {
+ return false;
+ }
+
+ if (!write_u32_le(fp, shape->ConvexHull.point_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < shape->ConvexHull.point_cnt; ++i) {
+ if (!write_vec3(fp, shape->ConvexHull.points[i])) {
+ return false;
+ }
+ }
+ break;
+
+ case PHYS_SHAPE_STATIC_TRIMESH:
+ if ((shape->TriMesh.point_cnt && !shape->TriMesh.points) ||
+ (shape->TriMesh.idx_cnt && !shape->TriMesh.indices)) {
+ return false;
+ }
+
+ if (!write_u32_le(fp, shape->TriMesh.point_cnt) ||
+ !write_u32_le(fp, shape->TriMesh.idx_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.point_cnt; ++i) {
+ if (!write_vec3(fp, shape->TriMesh.points[i])) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.idx_cnt; ++i) {
+ if (!write_u32_le(fp, shape->TriMesh.indices[i])) {
+ return false;
+ }
+ }
+ break;
+ }
+
+ return true;
+}
+
+static bool
+read_physics_body(FILE *fp, PhysicsBody *body) {
+ u8 mode, enable_sleep;
+ *body = (PhysicsBody){0};
+
+ if (!read_vec3(fp, &body->pos) ||
+ !read_quat(fp, &body->rot) ||
+ !read_f32_le(fp, &body->linear_damping) ||
+ !read_f32_le(fp, &body->angular_damping) ||
+ !read_f32_le(fp, &body->gravity_scale) ||
+ !read_u8_le(fp, &mode) ||
+ !read_u8_le(fp, &enable_sleep) ||
+ !read_u32_le(fp, &body->shape_cnt)) {
+ return false;
+ }
+
+ body->mode = (PhysicsMode)mode;
+ body->enable_sleep = enable_sleep;
+
+ if (body->shape_cnt) {
+ body->shapes = calloc(body->shape_cnt, sizeof(PhysicsShape));
+ if (!body->shapes) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < body->shape_cnt; ++i) {
+ if (!read_physics_shape(fp, &body->shapes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+write_physics_body(FILE *fp, PhysicsBody *body) {
+ if (!write_vec3(fp, body->pos) ||
+ !write_quat(fp, body->rot) ||
+ !write_f32_le(fp, body->linear_damping) ||
+ !write_f32_le(fp, body->angular_damping) ||
+ !write_f32_le(fp, body->gravity_scale) ||
+ !write_u8_le(fp, (u8)body->mode) ||
+ !write_u8_le(fp, body->enable_sleep ? 1 : 0) ||
+ !write_u32_le(fp, body->shape_cnt)) {
+ return false;
+ }
+
+ if (body->shape_cnt && !body->shapes) {
+ return false;
+ }
+
+ for (u32 i = 0; i < body->shape_cnt; ++i) {
+ if (!write_physics_shape(fp, &body->shapes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+read_physics_joint(FILE *fp, PhysicsJoint *joint, u32 body_cnt) {
+ u8 tag;
+ *joint = (PhysicsJoint){0};
+
+ if (!read_u8_le(fp, &tag) ||
+ tag > PHYS_JOINT_DISTANCE ||
+ !read_u32_le(fp, &joint->body_a) ||
+ !read_u32_le(fp, &joint->body_b) ||
+ joint->body_a >= body_cnt ||
+ joint->body_b >= body_cnt ||
+ !read_u32_le(fp, &joint->flags) ||
+ !read_vec3(fp, &joint->anchor_a) ||
+ !read_vec3(fp, &joint->anchor_b) ||
+ !read_vec3(fp, &joint->axis_a) ||
+ !read_vec3(fp, &joint->axis_b) ||
+ !read_f32_le(fp, &joint->lower) ||
+ !read_f32_le(fp, &joint->upper) ||
+ !read_f32_le(fp, &joint->spring_hz) ||
+ !read_f32_le(fp, &joint->spring_damping) ||
+ !read_f32_le(fp, &joint->motor_speed) ||
+ !read_f32_le(fp, &joint->motor_force)) {
+ return false;
+ }
+
+ joint->tag = (PhysicsJointTag)tag;
+ return true;
+}
+
+static bool
+write_physics_joint(FILE *fp, PhysicsJoint *joint, u32 body_cnt) {
+ if (joint->body_a >= body_cnt ||
+ joint->body_b >= body_cnt) {
+ return false;
+ }
+
+ return write_u8_le(fp, (u8)joint->tag) &&
+ write_u32_le(fp, joint->body_a) &&
+ write_u32_le(fp, joint->body_b) &&
+ write_u32_le(fp, joint->flags) &&
+ write_vec3(fp, joint->anchor_a) &&
+ write_vec3(fp, joint->anchor_b) &&
+ write_vec3(fp, joint->axis_a) &&
+ write_vec3(fp, joint->axis_b) &&
+ write_f32_le(fp, joint->lower) &&
+ write_f32_le(fp, joint->upper) &&
+ write_f32_le(fp, joint->spring_hz) &&
+ write_f32_le(fp, joint->spring_damping) &&
+ write_f32_le(fp, joint->motor_speed) &&
+ write_f32_le(fp, joint->motor_force);
+}
+
+static bool
+read_physics_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 phys_cnt = 0;
+ if (!read_u32_le(fp, &phys_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < phys_cnt; ++i) {
+ u64 pid;
+ u32 body_cnt, joint_cnt;
+
+ if (!read_u64_le(fp, &pid) || !pid || pid >= ctx->next_pid) {
+ return false;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+
+ if (!read_u32_le(fp, &body_cnt) || !ent || ent->phys.has) {
+ return false;
+ }
+
+ ent->phys.body_cnt = body_cnt;
+
+ if (body_cnt) {
+ ent->phys.bodies =
+ calloc(body_cnt, sizeof(PhysicsBody));
+ if (!ent->phys.bodies) {
+ return false;
+ }
+ }
+
+ for (u32 j = 0; j < body_cnt; ++j) {
+ if (!read_physics_body(fp, &ent->phys.bodies[j])) {
+ return false;
+ }
+ }
+
+ if (!read_u32_le(fp, &joint_cnt)) {
+ return false;
+ }
+
+ ent->phys.joint_cnt = joint_cnt;
+
+ if (joint_cnt) {
+ ent->phys.joints =
+ calloc(joint_cnt, sizeof(PhysicsJoint));
+ if (!ent->phys.joints) {
+ return false;
+ }
+ }
+
+ for (u32 j = 0; j < joint_cnt; ++j) {
+ if (!read_physics_joint(fp,
+ &ent->phys.joints[j], body_cnt)) {
+ return false;
+ }
+ }
+
+ ent->phys.has = true;
+ }
+
+ return true;
+}
+
+static bool
+write_physics_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+ u32 phys_cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid && ent->phys.has) {
+ phys_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "PHYS", &chunk) ||
+ !write_u32_le(fp, phys_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+ PhysicsCmp *phys = &ent->phys;
+
+ if (!ent->pid || !phys->has) {
+ continue;
+ }
+
+ if ((phys->body_cnt && !phys->bodies) ||
+ (phys->joint_cnt && !phys->joints) ||
+ !write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, phys->body_cnt)) {
+ return false;
+ }
+
+ for (u32 j = 0; j < phys->body_cnt; ++j) {
+ if (!write_physics_body(fp, &phys->bodies[j])) {
+ return false;
+ }
+ }
+
+ if (!write_u32_le(fp, phys->joint_cnt)) {
+ return false;
+ }
+
+ for (u32 j = 0; j < phys->joint_cnt; ++j) {
+ if (!write_physics_joint(fp,
+ &phys->joints[j], phys->body_cnt)) {
+ return false;
+ }
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+bool
+save_file_read(cstr path) {
+ FILE *fp = fopen(path, "rb");
+ if (!fp) {
+ TraceLog(LOG_ERROR, "save file '%s' failed to load.", path);
+ return false;
+ }
+
+ SaveLoadContext ctx = {0};
+ bool success = read_header(fp);
+ bool read_entt = false;
+
+ while (success) {
+ char label[4];
+ size_t read_cnt = fread(label, 1, 4, fp);
+ if (read_cnt == 0 && feof(fp)) {
+ break;
+ }
+
+ if (read_cnt != 4) {
+ TraceLog(LOG_ERROR, "failed to read chunk label.");
+ success = false;
+ break;
+ }
+
+ u32 chunk_size;
+ if (!read_u32_le(fp, &chunk_size)) {
+ TraceLog(LOG_ERROR, "failed to read chunk size.");
+ success = false;
+ break;
+ }
+
+ u32 data_pos = ftell(fp);
+ if (data_pos < 0) {
+ success = false;
+ break;
+ }
+
+ u32 end_pos = data_pos + chunk_size;
+
+ if (memcmp(label, "ENTT", 4) == 0) {
+ if (read_entt) {
+ TraceLog(LOG_ERROR, "duplicate ENTT chunk.");
+ success = false;
+ break;
+ }
+
+ success = read_entity_chunk(fp, &ctx);
+ read_entt = success;
+ } else if (memcmp(label, "TRFM", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "TRFM found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_transform_chunk(fp, &ctx);
+ } else if (memcmp(label, "MODL", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "TRFM found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_model_chunk(fp, &ctx);
+ } else if (memcmp(label, "PHYS", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "PHYS found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_physics_chunk(fp, &ctx);
+ } else {
+ TraceLog(LOG_INFO, "skipped unknown chunk.");
+ }
+
+ if (!success) {
+ break;
+ }
+
+ u32 pos = ftell(fp);
+ if (pos < 0 || pos > end_pos) {
+ TraceLog(LOG_ERROR,
+ "chunk reader exceeded chunk size.");
+ success = false;
+ break;
+ }
+
+ if (fseek(fp, end_pos, SEEK_SET) != 0) {
+ success = false;
+ break;
+ }
+ }
+
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "save contains no ENTT chunk.");
+ success = false;
+ }
+
+ if (fclose(fp) != 0) {
+ success = false;
+ }
+
+ return success;
+}
+
+bool
+save_file_write(cstr path) {
+ FILE *fp = fopen(path, "wb");
+ if (!fp) {
+ return false;
+ }
+
+ bool success =
+ write_header(fp) &&
+ write_entity_chunk(fp) &&
+ write_transform_chunk(fp) &&
+ write_model_chunk(fp) &&
+ write_physics_chunk(fp);
+
+ if (fclose(fp) != 0) {
+ success = false;
+ }
+
+ return success;
+}
+