Mercurial
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 }