view infinite_canvas/scene_store.c @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -0700
parents
children
line wrap: on
line source

#include "infinite_canvas/scene_store.h"

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(_WIN32)
#include <windows.h>
#elif !defined(PLATFORM_WEB)
#include <unistd.h>
#endif

#define CANVAS_SCENE_STORE_VERSION 1
#define CANVAS_SCENE_STORE_HEADER_SIZE 48
#define CANVAS_SCENE_STORE_ENTITY_SIZE 2240
#define CANVAS_SCENE_STORE_MAX_BYTES \
    (CANVAS_SCENE_STORE_HEADER_SIZE + \
        CANVAS_MAX_ENTITIES * CANVAS_SCENE_STORE_ENTITY_SIZE)

static const uint8 CANVAS_SCENE_STORE_MAGIC[8] = {
    'Z', 'E', 'N', 'B', 'U', 'M', 'A', 'P',
};

_Static_assert(sizeof(Color) == 4, "scene format requires RGBA8 Color");
_Static_assert(
    CANVAS_ENTITY_TEXT_CAPACITY == 2048,
    "scene format version must change when text capacity changes");
_Static_assert(
    CANVAS_SCENE_STORE_ENTITY_SIZE ==
        2 * 4 + 4 * 4 + 4 + 128 + 2048 + 3 * 4 + 6 * 4,
    "scene entity record size is inconsistent");

typedef struct {
    uint8 *p_data;
    size_t size;
    size_t offset;
    boolean valid;
} Canvas_Scene_Writer;

typedef struct {
    const uint8 *p_data;
    size_t size;
    size_t offset;
    boolean valid;
} Canvas_Scene_Reader;

static uint64 Canvas_Scene_Store_FNV(
    uint64 hash,
    const uint8 *p_data,
    size_t size)
{
    for (size_t index = 0; index < size; index++) {
        hash ^= p_data[index];
        hash *= 1099511628211ULL;
    }
    return hash;
}

static void Canvas_Scene_Store_Write(
    Canvas_Scene_Writer *p_writer,
    const void *p_data,
    size_t size)
{
    if (!p_writer->valid ||
        p_writer->offset > p_writer->size ||
        size > p_writer->size - p_writer->offset) {
        p_writer->valid = FALSE;
        return;
    }
    memcpy(p_writer->p_data + p_writer->offset, p_data, size);
    p_writer->offset += size;
}

static void Canvas_Scene_Store_Write_U32(
    Canvas_Scene_Writer *p_writer,
    uint32 value)
{
    uint8 bytes[4] = {
        (uint8)(value & 0xff),
        (uint8)((value >> 8) & 0xff),
        (uint8)((value >> 16) & 0xff),
        (uint8)((value >> 24) & 0xff),
    };
    Canvas_Scene_Store_Write(p_writer, bytes, sizeof(bytes));
}

static void Canvas_Scene_Store_Write_F32(
    Canvas_Scene_Writer *p_writer,
    float value)
{
    uint32 bits = 0;
    memcpy(&bits, &value, sizeof(bits));
    Canvas_Scene_Store_Write_U32(p_writer, bits);
}

static void Canvas_Scene_Store_Read(
    Canvas_Scene_Reader *p_reader,
    void *p_output,
    size_t size)
{
    if (!p_reader->valid ||
        p_reader->offset > p_reader->size ||
        size > p_reader->size - p_reader->offset) {
        p_reader->valid = FALSE;
        return;
    }
    memcpy(p_output, p_reader->p_data + p_reader->offset, size);
    p_reader->offset += size;
}

static uint32 Canvas_Scene_Store_Read_U32(
    Canvas_Scene_Reader *p_reader)
{
    uint8 bytes[4] = {0};
    Canvas_Scene_Store_Read(p_reader, bytes, sizeof(bytes));
    return (uint32)bytes[0] |
        ((uint32)bytes[1] << 8) |
        ((uint32)bytes[2] << 16) |
        ((uint32)bytes[3] << 24);
}

static float Canvas_Scene_Store_Read_F32(
    Canvas_Scene_Reader *p_reader)
{
    uint32 bits = Canvas_Scene_Store_Read_U32(p_reader);
    float value = 0.0f;
    memcpy(&value, &bits, sizeof(value));
    return value;
}

static uint32 Canvas_Scene_Store_Entity_Flags(
    const Canvas_Entity *p_entity)
{
    uint32 flags = 0;
    if (p_entity->active) flags |= 1u << 0;
    if (p_entity->pinned) flags |= 1u << 1;
    if (p_entity->text_muted) flags |= 1u << 2;
    if (p_entity->parked) flags |= 1u << 3;
    return flags;
}

static void Canvas_Scene_Store_Write_Entity(
    Canvas_Scene_Writer *p_writer,
    const Canvas_Entity *p_entity)
{
    Canvas_Scene_Store_Write_U32(p_writer, p_entity->id);
    Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->type);
    Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.x);
    Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.y);
    Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.x);
    Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.y);
    Canvas_Scene_Store_Write(p_writer, &p_entity->color, 4);
    Canvas_Scene_Store_Write(
        p_writer, p_entity->label, sizeof(p_entity->label));
    Canvas_Scene_Store_Write(
        p_writer, p_entity->text, sizeof(p_entity->text));
    Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->value);
    Canvas_Scene_Store_Write_F32(p_writer, p_entity->text_scroll_y);
    Canvas_Scene_Store_Write_U32(
        p_writer, Canvas_Scene_Store_Entity_Flags(p_entity));
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->pinned_screen_position.x);
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->pinned_screen_position.y);
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->pinned_screen_size.x);
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->pinned_screen_size.y);
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->parked_restore_position.x);
    Canvas_Scene_Store_Write_F32(
        p_writer, p_entity->parked_restore_position.y);
}

static Canvas_Entity Canvas_Scene_Store_Read_Entity(
    Canvas_Scene_Reader *p_reader)
{
    Canvas_Entity entity = {0};
    entity.id = Canvas_Scene_Store_Read_U32(p_reader);
    entity.type =
        (Canvas_Entity_Type)Canvas_Scene_Store_Read_U32(p_reader);
    entity.position.x = Canvas_Scene_Store_Read_F32(p_reader);
    entity.position.y = Canvas_Scene_Store_Read_F32(p_reader);
    entity.size.x = Canvas_Scene_Store_Read_F32(p_reader);
    entity.size.y = Canvas_Scene_Store_Read_F32(p_reader);
    Canvas_Scene_Store_Read(p_reader, &entity.color, 4);
    Canvas_Scene_Store_Read(
        p_reader, entity.label, sizeof(entity.label));
    Canvas_Scene_Store_Read(
        p_reader, entity.text, sizeof(entity.text));
    entity.label[sizeof(entity.label) - 1] = '\0';
    entity.text[sizeof(entity.text) - 1] = '\0';
    entity.value = (int32)Canvas_Scene_Store_Read_U32(p_reader);
    entity.text_scroll_y = Canvas_Scene_Store_Read_F32(p_reader);
    uint32 flags = Canvas_Scene_Store_Read_U32(p_reader);
    entity.active = flags & (1u << 0) ? TRUE : FALSE;
    entity.pinned = flags & (1u << 1) ? TRUE : FALSE;
    entity.text_muted = flags & (1u << 2) ? TRUE : FALSE;
    entity.parked = flags & (1u << 3) ? TRUE : FALSE;
    entity.pinned_screen_position.x =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.pinned_screen_position.y =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.pinned_screen_size.x =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.pinned_screen_size.y =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.parked_restore_position.x =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.parked_restore_position.y =
        Canvas_Scene_Store_Read_F32(p_reader);
    entity.text_cursor = (int32)strlen(entity.text);
    entity.text_selection_anchor = entity.text_cursor;
    entity.visibility_amount = 1.0f;
    entity.screen_space_initialized = entity.pinned;
    entity.animation_amount =
        entity.type == CANVAS_ENTITY_CONVERSATION
            ? (entity.active ? 0.0f : 1.0f)
            : (entity.active ? 1.0f : 0.0f);
    return entity;
}

static boolean Canvas_Scene_Store_Entity_Valid(
    const Canvas_Entity *p_entity)
{
    boolean base_valid = p_entity->id != 0 &&
        p_entity->type >= 0 &&
        p_entity->type < CANVAS_ENTITY_TYPE_COUNT &&
        isfinite(p_entity->position.x) &&
        isfinite(p_entity->position.y) &&
        isfinite(p_entity->size.x) &&
        isfinite(p_entity->size.y) &&
        p_entity->size.x > 0.0f &&
        p_entity->size.y > 0.0f &&
        fabsf(p_entity->position.x) <= 1000000000.0f &&
        fabsf(p_entity->position.y) <= 1000000000.0f &&
        p_entity->size.x <= 10000000.0f &&
        p_entity->size.y <= 10000000.0f;
    if (!base_valid) return FALSE;
    if (p_entity->pinned &&
        (!isfinite(p_entity->pinned_screen_position.x) ||
            !isfinite(p_entity->pinned_screen_position.y) ||
            !isfinite(p_entity->pinned_screen_size.x) ||
            !isfinite(p_entity->pinned_screen_size.y) ||
            p_entity->pinned_screen_size.x <= 0.0f ||
            p_entity->pinned_screen_size.y <= 0.0f)) {
        return FALSE;
    }
    return !p_entity->parked ||
        (isfinite(p_entity->parked_restore_position.x) &&
            isfinite(p_entity->parked_restore_position.y));
}

boolean Canvas_Scene_Store_Path(
    const char *p_name,
    char *p_path,
    size_t path_capacity)
{
#if defined(PLATFORM_WEB)
    (void)p_name;
    (void)p_path;
    (void)path_capacity;
    return FALSE;
#else
    if (!p_name || !p_name[0] || !p_path || path_capacity == 0) {
        return FALSE;
    }
    size_t name_length = strlen(p_name);
    if (name_length >= CANVAS_SCENE_STORE_NAME_CAPACITY) return FALSE;
    for (size_t index = 0; index < name_length; index++) {
        uint8 character = (uint8)p_name[index];
        if (!isalnum(character) &&
            character != '-' &&
            character != '_') {
            return FALSE;
        }
    }
    const char *p_root = getenv("XDG_STATE_HOME");
    const char *p_suffix = "/zenbu/infinite-canvas/snapshots";
    char fallback[CANVAS_SCENE_STORE_PATH_CAPACITY];
    if (!p_root || !p_root[0]) {
        const char *p_home = getenv("HOME");
        if (!p_home || !p_home[0]) return FALSE;
        int fallback_written = snprintf(
            fallback,
            sizeof(fallback),
            "%s/.local/state",
            p_home);
        if (fallback_written <= 0 ||
            (size_t)fallback_written >= sizeof(fallback)) {
            return FALSE;
        }
        p_root = fallback;
    }
    int written = snprintf(
        p_path,
        path_capacity,
        "%s%s/%s.zmap",
        p_root,
        p_suffix,
        p_name);
    return written > 0 && (size_t)written < path_capacity;
#endif
}

boolean Canvas_Scene_Store_Save(
    const char *p_path,
    const Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera)
{
#if defined(PLATFORM_WEB)
    (void)p_path;
    (void)p_scene;
    (void)p_camera;
    return FALSE;
#else
    if (!p_path || !p_scene || !p_camera) return FALSE;
    size_t entity_count = 0;
    for (size_t index = 0;
        index < Dowa_Array_Length(p_scene->p_entities);
        index++) {
        if (!p_scene->p_entities[index].removing) entity_count++;
    }
    if (entity_count > CANVAS_MAX_ENTITIES) return FALSE;
    size_t size = CANVAS_SCENE_STORE_HEADER_SIZE +
        entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE;
    uint8 *p_data = (uint8 *)calloc(size, 1);
    if (!p_data) return FALSE;
    Canvas_Scene_Writer writer = {
        .p_data = p_data,
        .size = size,
        .valid = TRUE,
    };
    Canvas_Scene_Store_Write(
        &writer, CANVAS_SCENE_STORE_MAGIC, sizeof(CANVAS_SCENE_STORE_MAGIC));
    Canvas_Scene_Store_Write_U32(&writer, CANVAS_SCENE_STORE_VERSION);
    Canvas_Scene_Store_Write_U32(&writer, (uint32)entity_count);
    Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity);
    Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity_id);
    Canvas_Scene_Store_Write_U32(
        &writer, p_scene->show_grid ? 1u : 0u);
    Canvas_Scene_Store_Write_F32(&writer, p_camera->target.x);
    Canvas_Scene_Store_Write_F32(&writer, p_camera->target.y);
    Canvas_Scene_Store_Write_F32(&writer, p_camera->zoom);
    Canvas_Scene_Store_Write_U32(
        &writer, (uint32)(size - CANVAS_SCENE_STORE_HEADER_SIZE));
    Canvas_Scene_Store_Write_U32(&writer, 0);
    for (size_t index = 0;
        index < Dowa_Array_Length(p_scene->p_entities);
        index++) {
        if (!p_scene->p_entities[index].removing) {
            Canvas_Scene_Store_Write_Entity(
                &writer, &p_scene->p_entities[index]);
        }
    }
    if (!writer.valid || writer.offset != size) {
        Dowa_Free(p_data);
        return FALSE;
    }
    uint64 checksum_hash = Canvas_Scene_Store_FNV(
        1469598103934665603ULL,
        p_data,
        44);
    checksum_hash = Canvas_Scene_Store_FNV(
        checksum_hash,
        p_data + CANVAS_SCENE_STORE_HEADER_SIZE,
        size - CANVAS_SCENE_STORE_HEADER_SIZE);
    uint32 checksum = (uint32)checksum_hash;
    p_data[44] = (uint8)(checksum & 0xff);
    p_data[45] = (uint8)((checksum >> 8) & 0xff);
    p_data[46] = (uint8)((checksum >> 16) & 0xff);
    p_data[47] = (uint8)((checksum >> 24) & 0xff);

    char directory[CANVAS_SCENE_STORE_PATH_CAPACITY];
    char temporary[CANVAS_SCENE_STORE_PATH_CAPACITY];
    snprintf(directory, sizeof(directory), "%s", p_path);
    char *p_separator = strrchr(directory, '/');
#if defined(_WIN32)
    char *p_windows_separator = strrchr(directory, '\\');
    if (!p_separator || p_windows_separator > p_separator) {
        p_separator = p_windows_separator;
    }
#endif
    if (!p_separator) {
        Dowa_Free(p_data);
        return FALSE;
    }
    *p_separator = '\0';
    if (MakeDirectory(directory) != 0 && !DirectoryExists(directory)) {
        Dowa_Free(p_data);
        return FALSE;
    }
    int temporary_written = snprintf(
        temporary, sizeof(temporary), "%s.tmp", p_path);
    if (temporary_written <= 0 ||
        (size_t)temporary_written >= sizeof(temporary)) {
        Dowa_Free(p_data);
        return FALSE;
    }
    FILE *p_file = fopen(temporary, "wb");
    if (!p_file) {
        Dowa_Free(p_data);
        return FALSE;
    }
    boolean success =
        fwrite(p_data, 1, size, p_file) == size &&
        fflush(p_file) == 0;
#if !defined(_WIN32)
    if (success) success = fsync(fileno(p_file)) == 0;
#endif
    if (fclose(p_file) != 0) success = FALSE;
    Dowa_Free(p_data);
    if (!success) {
        remove(temporary);
        return FALSE;
    }
#if defined(_WIN32)
    success = MoveFileExA(
        temporary,
        p_path,
        MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)
        ? TRUE
        : FALSE;
#else
    success = rename(temporary, p_path) == 0 ? TRUE : FALSE;
#endif
    if (!success) remove(temporary);
    return success;
#endif
}

boolean Canvas_Scene_Store_Load(
    const char *p_path,
    Canvas_Scene *p_scene,
    Canvas_Camera *p_camera)
{
#if defined(PLATFORM_WEB)
    (void)p_path;
    (void)p_scene;
    (void)p_camera;
    return FALSE;
#else
    if (!p_path || !p_scene || !p_camera) return FALSE;
    FILE *p_file = fopen(p_path, "rb");
    if (!p_file) return FALSE;
    if (fseek(p_file, 0, SEEK_END) != 0) {
        fclose(p_file);
        return FALSE;
    }
    long file_size = ftell(p_file);
    if (file_size < CANVAS_SCENE_STORE_HEADER_SIZE ||
        file_size > CANVAS_SCENE_STORE_MAX_BYTES ||
        fseek(p_file, 0, SEEK_SET) != 0) {
        fclose(p_file);
        return FALSE;
    }
    uint8 *p_data = (uint8 *)malloc((size_t)file_size);
    if (!p_data) {
        fclose(p_file);
        return FALSE;
    }
    boolean success =
        fread(p_data, 1, (size_t)file_size, p_file) == (size_t)file_size;
    fclose(p_file);
    if (!success) {
        Dowa_Free(p_data);
        return FALSE;
    }

    Canvas_Scene_Reader reader = {
        .p_data = p_data,
        .size = (size_t)file_size,
        .valid = TRUE,
    };
    uint8 magic[8] = {0};
    Canvas_Scene_Store_Read(&reader, magic, sizeof(magic));
    uint32 version = Canvas_Scene_Store_Read_U32(&reader);
    uint32 entity_count = Canvas_Scene_Store_Read_U32(&reader);
    uint32 next_entity = Canvas_Scene_Store_Read_U32(&reader);
    uint32 next_entity_id = Canvas_Scene_Store_Read_U32(&reader);
    uint32 scene_flags = Canvas_Scene_Store_Read_U32(&reader);
    float camera_x = Canvas_Scene_Store_Read_F32(&reader);
    float camera_y = Canvas_Scene_Store_Read_F32(&reader);
    float camera_zoom = Canvas_Scene_Store_Read_F32(&reader);
    uint32 payload_size = Canvas_Scene_Store_Read_U32(&reader);
    uint32 stored_checksum = Canvas_Scene_Store_Read_U32(&reader);
    size_t expected_size = CANVAS_SCENE_STORE_HEADER_SIZE +
        (size_t)entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE;
    uint64 checksum_hash = Canvas_Scene_Store_FNV(
        1469598103934665603ULL,
        p_data,
        44);
    checksum_hash = Canvas_Scene_Store_FNV(
        checksum_hash,
        p_data + CANVAS_SCENE_STORE_HEADER_SIZE,
        (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE);
    uint32 actual_checksum = (uint32)checksum_hash;
    if (!reader.valid ||
        memcmp(magic, CANVAS_SCENE_STORE_MAGIC, sizeof(magic)) != 0 ||
        version != CANVAS_SCENE_STORE_VERSION ||
        entity_count > CANVAS_MAX_ENTITIES ||
        expected_size != (size_t)file_size ||
        payload_size != (uint32)(
            (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE) ||
        stored_checksum != actual_checksum ||
        next_entity == 0xffffffffu ||
        next_entity_id == 0xffffffffu ||
        !isfinite(camera_x) ||
        !isfinite(camera_y) ||
        !isfinite(camera_zoom) ||
        fabsf(camera_x) > 1000000000.0f ||
        fabsf(camera_y) > 1000000000.0f ||
        camera_zoom < p_camera->min_zoom ||
        camera_zoom > p_camera->max_zoom) {
        Dowa_Free(p_data);
        return FALSE;
    }

    Canvas_Entity *p_entities = (Canvas_Entity *)calloc(
        entity_count ? entity_count : 1,
        sizeof(*p_entities));
    if (!p_entities) {
        Dowa_Free(p_data);
        return FALSE;
    }
    uint32 max_id = 0;
    for (uint32 index = 0; index < entity_count; index++) {
        p_entities[index] = Canvas_Scene_Store_Read_Entity(&reader);
        if (!reader.valid ||
            !Canvas_Scene_Store_Entity_Valid(&p_entities[index])) {
            Dowa_Free(p_entities);
            Dowa_Free(p_data);
            return FALSE;
        }
        for (uint32 previous = 0; previous < index; previous++) {
            if (p_entities[previous].id == p_entities[index].id) {
                Dowa_Free(p_entities);
                Dowa_Free(p_data);
                return FALSE;
            }
        }
        if (p_entities[index].id > max_id) {
            max_id = p_entities[index].id;
        }
    }
    Dowa_Free(p_data);
    if (!reader.valid || reader.offset != (size_t)file_size) {
        Dowa_Free(p_entities);
        return FALSE;
    }

    Canvas_Scene_Clear(p_scene);
    for (uint32 index = 0; index < entity_count; index++) {
        Dowa_Array_Push_Arena(
            p_scene->p_entities, p_entities[index], p_scene->p_arena);
    }
    Dowa_Free(p_entities);
    p_scene->next_entity =
        next_entity < entity_count ? entity_count : next_entity;
    p_scene->next_entity_id =
        next_entity_id < max_id ? max_id : next_entity_id;
    p_scene->show_grid = scene_flags & 1u ? TRUE : FALSE;
    p_camera->target = (Vector2){camera_x, camera_y};
    p_camera->zoom = camera_zoom;
    return TRUE;
#endif
}