add networking, 3d improvements, reorganize src structure
This commit is contained in:
parent
39b604b333
commit
415d424057
122 changed files with 5358 additions and 721 deletions
636
src/asset/pxl8_ase.c
Normal file
636
src/asset/pxl8_ase.c
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
#include "pxl8_ase.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MINIZ_NO_STDIO
|
||||
#define MINIZ_NO_TIME
|
||||
#define MINIZ_NO_ARCHIVE_APIS
|
||||
#define MINIZ_NO_ARCHIVE_WRITING_APIS
|
||||
#define MINIZ_NO_DEFLATE_APIS
|
||||
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
|
||||
|
||||
#include <miniz.h>
|
||||
|
||||
#include "pxl8_io.h"
|
||||
#include "pxl8_log.h"
|
||||
|
||||
static pxl8_result parse_ase_header(pxl8_stream* stream, pxl8_ase_header* header) {
|
||||
header->file_size = pxl8_read_u32(stream);
|
||||
header->magic = pxl8_read_u16(stream);
|
||||
header->frames = pxl8_read_u16(stream);
|
||||
header->width = pxl8_read_u16(stream);
|
||||
header->height = pxl8_read_u16(stream);
|
||||
header->color_depth = pxl8_read_u16(stream);
|
||||
header->flags = pxl8_read_u32(stream);
|
||||
header->speed = pxl8_read_u16(stream);
|
||||
pxl8_skip_bytes(stream, 4);
|
||||
header->transparent_index = pxl8_read_u32(stream);
|
||||
header->n_colors = pxl8_read_u8(stream);
|
||||
header->pixel_width = pxl8_read_u8(stream);
|
||||
header->pixel_height = pxl8_read_u8(stream);
|
||||
header->grid_x = pxl8_read_i16(stream);
|
||||
header->grid_y = pxl8_read_i16(stream);
|
||||
header->grid_width = pxl8_read_u16(stream);
|
||||
header->grid_height = pxl8_read_u16(stream);
|
||||
|
||||
if (header->magic != PXL8_ASE_MAGIC) {
|
||||
pxl8_error("Invalid ASE file magic: 0x%04X", header->magic);
|
||||
return PXL8_ERROR_ASE_INVALID_MAGIC;
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_old_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
|
||||
u16 packet_count = pxl8_read_u16(stream);
|
||||
|
||||
u32 total_colors = 0;
|
||||
u32 temp_pos = pxl8_stream_position(stream);
|
||||
for (u16 packet = 0; packet < packet_count; packet++) {
|
||||
u8 skip_colors = pxl8_read_u8(stream);
|
||||
u8 colors_in_packet = pxl8_read_u8(stream);
|
||||
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
|
||||
total_colors += skip_colors + actual_colors;
|
||||
pxl8_skip_bytes(stream, actual_colors * 3);
|
||||
}
|
||||
|
||||
palette->entry_count = total_colors;
|
||||
palette->first_color = 0;
|
||||
palette->last_color = total_colors - 1;
|
||||
palette->colors = (u32*)malloc(total_colors * sizeof(u32));
|
||||
if (!palette->colors) {
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
pxl8_stream_seek(stream, temp_pos);
|
||||
|
||||
u32 color_index = 0;
|
||||
for (u16 packet = 0; packet < packet_count; packet++) {
|
||||
u8 skip_colors = pxl8_read_u8(stream);
|
||||
u8 colors_in_packet = pxl8_read_u8(stream);
|
||||
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
|
||||
|
||||
for (u32 skip = 0; skip < skip_colors && color_index < total_colors; skip++) {
|
||||
palette->colors[color_index++] = 0xFF000000;
|
||||
}
|
||||
|
||||
for (u32 color = 0; color < actual_colors && color_index < total_colors; color++) {
|
||||
u8 r = pxl8_read_u8(stream);
|
||||
u8 g = pxl8_read_u8(stream);
|
||||
u8 b = pxl8_read_u8(stream);
|
||||
palette->colors[color_index++] = 0xFF000000 | ((u32)b << 16) | ((u32)g << 8) | r;
|
||||
}
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_layer_chunk(pxl8_stream* stream, pxl8_ase_layer* layer) {
|
||||
layer->flags = pxl8_read_u16(stream);
|
||||
layer->layer_type = pxl8_read_u16(stream);
|
||||
layer->child_level = pxl8_read_u16(stream);
|
||||
pxl8_skip_bytes(stream, 4);
|
||||
layer->blend_mode = pxl8_read_u16(stream);
|
||||
layer->opacity = pxl8_read_u8(stream);
|
||||
pxl8_skip_bytes(stream, 3);
|
||||
|
||||
u16 name_len = pxl8_read_u16(stream);
|
||||
if (name_len > 0) {
|
||||
layer->name = (char*)malloc(name_len + 1);
|
||||
if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
pxl8_read_bytes(stream, layer->name, name_len);
|
||||
layer->name[name_len] = '\0';
|
||||
} else {
|
||||
layer->name = NULL;
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
|
||||
pxl8_skip_bytes(stream, 4);
|
||||
palette->first_color = pxl8_read_u32(stream);
|
||||
palette->last_color = pxl8_read_u32(stream);
|
||||
pxl8_skip_bytes(stream, 8);
|
||||
|
||||
u32 color_count = palette->last_color - palette->first_color + 1;
|
||||
|
||||
if (color_count == 0 || color_count > 65536) {
|
||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
}
|
||||
|
||||
palette->colors = (u32*)malloc(color_count * sizeof(u32));
|
||||
if (!palette->colors) {
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
palette->entry_count = color_count;
|
||||
|
||||
for (u32 i = 0; i < color_count; i++) {
|
||||
u16 flags = pxl8_read_u16(stream);
|
||||
u8 r = pxl8_read_u8(stream);
|
||||
u8 g = pxl8_read_u8(stream);
|
||||
u8 b = pxl8_read_u8(stream);
|
||||
u8 a = pxl8_read_u8(stream);
|
||||
|
||||
palette->colors[i] = ((u32)a << 24) | ((u32)b << 16) | ((u32)g << 8) | r;
|
||||
|
||||
if (flags & 1) {
|
||||
u16 name_len = pxl8_read_u16(stream);
|
||||
pxl8_skip_bytes(stream, name_len);
|
||||
}
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_user_data_chunk(pxl8_stream* stream, pxl8_ase_user_data* user_data) {
|
||||
u32 flags = pxl8_read_u32(stream);
|
||||
|
||||
user_data->has_text = (flags & 1) != 0;
|
||||
user_data->has_color = (flags & 2) != 0;
|
||||
|
||||
if (user_data->has_text) {
|
||||
u16 text_len = pxl8_read_u16(stream);
|
||||
if (text_len > 0) {
|
||||
user_data->text = (char*)malloc(text_len + 1);
|
||||
if (!user_data->text) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
pxl8_read_bytes(stream, user_data->text, text_len);
|
||||
user_data->text[text_len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (user_data->has_color) {
|
||||
u8 r = pxl8_read_u8(stream);
|
||||
u8 g = pxl8_read_u8(stream);
|
||||
u8 b = pxl8_read_u8(stream);
|
||||
u8 a = pxl8_read_u8(stream);
|
||||
user_data->color = ((u32)a << 24) | ((u32)b << 16) | ((u32)g << 8) | r;
|
||||
}
|
||||
|
||||
if (flags & 4) {
|
||||
pxl8_skip_bytes(stream, 4);
|
||||
u32 num_properties = pxl8_read_u32(stream);
|
||||
|
||||
if (num_properties > 0) {
|
||||
user_data->properties = (pxl8_ase_property*)calloc(num_properties, sizeof(pxl8_ase_property));
|
||||
if (!user_data->properties) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
user_data->property_count = num_properties;
|
||||
|
||||
for (u32 i = 0; i < num_properties; i++) {
|
||||
u16 name_len = pxl8_read_u16(stream);
|
||||
if (name_len > 0) {
|
||||
user_data->properties[i].name = (char*)malloc(name_len + 1);
|
||||
if (!user_data->properties[i].name) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
pxl8_read_bytes(stream, user_data->properties[i].name, name_len);
|
||||
user_data->properties[i].name[name_len] = '\0';
|
||||
}
|
||||
|
||||
u16 type = pxl8_read_u16(stream);
|
||||
user_data->properties[i].type = type;
|
||||
|
||||
switch (type) {
|
||||
case 1:
|
||||
user_data->properties[i].bool_val = pxl8_read_u8(stream) != 0;
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
user_data->properties[i].int_val = pxl8_read_i32(stream);
|
||||
break;
|
||||
case 6:
|
||||
case 7:
|
||||
user_data->properties[i].float_val = pxl8_read_f32(stream);
|
||||
break;
|
||||
case 8: {
|
||||
u16 str_len = pxl8_read_u16(stream);
|
||||
if (str_len > 0) {
|
||||
user_data->properties[i].string_val = (char*)malloc(str_len + 1);
|
||||
if (!user_data->properties[i].string_val) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
pxl8_read_bytes(stream, user_data->properties[i].string_val, str_len);
|
||||
user_data->properties[i].string_val[str_len] = '\0';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
pxl8_skip_bytes(stream, 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_tileset_chunk(pxl8_stream* stream, pxl8_ase_tileset* tileset) {
|
||||
tileset->id = pxl8_read_u32(stream);
|
||||
tileset->flags = pxl8_read_u32(stream);
|
||||
tileset->tile_count = pxl8_read_u32(stream);
|
||||
tileset->tile_width = pxl8_read_u16(stream);
|
||||
tileset->tile_height = pxl8_read_u16(stream);
|
||||
tileset->base_index = pxl8_read_i16(stream);
|
||||
pxl8_skip_bytes(stream, 14);
|
||||
|
||||
u16 name_len = pxl8_read_u16(stream);
|
||||
if (name_len > 0) {
|
||||
tileset->name = (char*)malloc(name_len + 1);
|
||||
if (!tileset->name) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
pxl8_read_bytes(stream, tileset->name, name_len);
|
||||
tileset->name[name_len] = '\0';
|
||||
}
|
||||
|
||||
if (tileset->flags & 1) {
|
||||
pxl8_skip_bytes(stream, 8); // external_file_id + tileset_id
|
||||
}
|
||||
|
||||
if (tileset->flags & 2) {
|
||||
u32 compressed_size = pxl8_read_u32(stream);
|
||||
tileset->pixels_size = tileset->tile_width * tileset->tile_height * tileset->tile_count;
|
||||
tileset->pixels = (u8*)malloc(tileset->pixels_size);
|
||||
if (!tileset->pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||
mz_ulong dest_len = tileset->pixels_size;
|
||||
i32 result = mz_uncompress(tileset->pixels, &dest_len, compressed_data, compressed_size);
|
||||
if (result != MZ_OK) {
|
||||
pxl8_error("Failed to decompress tileset data: miniz error %d", result);
|
||||
free(tileset->pixels);
|
||||
tileset->pixels = NULL;
|
||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
}
|
||||
}
|
||||
|
||||
tileset->tile_user_data = (pxl8_ase_user_data*)calloc(tileset->tile_count, sizeof(pxl8_ase_user_data));
|
||||
if (!tileset->tile_user_data) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
|
||||
if (chunk_size < 9) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
|
||||
cel->layer_index = pxl8_read_u16(stream);
|
||||
cel->x = pxl8_read_i16(stream);
|
||||
cel->y = pxl8_read_i16(stream);
|
||||
cel->opacity = pxl8_read_u8(stream);
|
||||
cel->cel_type = pxl8_read_u16(stream);
|
||||
pxl8_skip_bytes(stream, 7);
|
||||
|
||||
if (cel->cel_type == 2) {
|
||||
if (chunk_size < 20) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
|
||||
cel->image.width = pxl8_read_u16(stream);
|
||||
cel->image.height = pxl8_read_u16(stream);
|
||||
|
||||
u32 pixels_size = cel->image.width * cel->image.height;
|
||||
u32 compressed_size = chunk_size - 20;
|
||||
|
||||
cel->image.pixels = (u8*)malloc(pixels_size);
|
||||
if (!cel->image.pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||
mz_ulong dest_len = pixels_size;
|
||||
i32 result = mz_uncompress(cel->image.pixels, &dest_len, compressed_data, compressed_size);
|
||||
if (result != MZ_OK) {
|
||||
pxl8_error("Failed to decompress cel data: miniz error %d", result);
|
||||
free(cel->image.pixels);
|
||||
cel->image.pixels = NULL;
|
||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
}
|
||||
|
||||
if (dest_len != pixels_size) {
|
||||
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixels_size, dest_len);
|
||||
}
|
||||
} else if (cel->cel_type == 3) {
|
||||
if (chunk_size < 36) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
|
||||
cel->tilemap.width = pxl8_read_u16(stream);
|
||||
cel->tilemap.height = pxl8_read_u16(stream);
|
||||
cel->tilemap.bits_per_tile = pxl8_read_u16(stream);
|
||||
cel->tilemap.tile_id_mask = pxl8_read_u32(stream);
|
||||
cel->tilemap.x_flip_mask = pxl8_read_u32(stream);
|
||||
cel->tilemap.y_flip_mask = pxl8_read_u32(stream);
|
||||
cel->tilemap.diag_flip_mask = pxl8_read_u32(stream);
|
||||
pxl8_skip_bytes(stream, 10);
|
||||
|
||||
u32 tile_count = cel->tilemap.width * cel->tilemap.height;
|
||||
u32 bytes_per_tile = (cel->tilemap.bits_per_tile + 7) / 8;
|
||||
u32 uncompressed_size = tile_count * bytes_per_tile;
|
||||
u32 compressed_size = chunk_size - 36;
|
||||
|
||||
u8* temp_buffer = (u8*)malloc(uncompressed_size);
|
||||
if (!temp_buffer) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||
mz_ulong dest_len = uncompressed_size;
|
||||
i32 result = mz_uncompress(temp_buffer, &dest_len, compressed_data, compressed_size);
|
||||
if (result != MZ_OK) {
|
||||
pxl8_error("Failed to decompress tilemap data: miniz error %d", result);
|
||||
free(temp_buffer);
|
||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
}
|
||||
|
||||
cel->tilemap.tiles = (u32*)calloc(tile_count, sizeof(u32));
|
||||
if (!cel->tilemap.tiles) {
|
||||
free(temp_buffer);
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < tile_count; i++) {
|
||||
if (cel->tilemap.bits_per_tile == 32) {
|
||||
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 4 + 0]) |
|
||||
((u32)temp_buffer[i * 4 + 1] << 8) |
|
||||
((u32)temp_buffer[i * 4 + 2] << 16) |
|
||||
((u32)temp_buffer[i * 4 + 3] << 24);
|
||||
} else if (cel->tilemap.bits_per_tile == 16) {
|
||||
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 2 + 0]) |
|
||||
((u32)temp_buffer[i * 2 + 1] << 8);
|
||||
} else if (cel->tilemap.bits_per_tile == 8) {
|
||||
cel->tilemap.tiles[i] = temp_buffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
free(temp_buffer);
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
|
||||
if (!filepath || !ase_file) {
|
||||
return PXL8_ERROR_NULL_POINTER;
|
||||
}
|
||||
|
||||
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
||||
|
||||
u8* file_data;
|
||||
size_t file_size;
|
||||
pxl8_result result = pxl8_io_read_binary_file(filepath, &file_data, &file_size);
|
||||
if (result != PXL8_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (file_size < 128) {
|
||||
pxl8_io_free_binary_data(file_data);
|
||||
return PXL8_ERROR_ASE_TRUNCATED_FILE;
|
||||
}
|
||||
|
||||
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
|
||||
|
||||
result = parse_ase_header(&stream, &ase_file->header);
|
||||
if (result != PXL8_OK) {
|
||||
pxl8_io_free_binary_data(file_data);
|
||||
return result;
|
||||
}
|
||||
|
||||
ase_file->frame_count = ase_file->header.frames;
|
||||
ase_file->frames = (pxl8_ase_frame*)calloc(ase_file->frame_count, sizeof(pxl8_ase_frame));
|
||||
if (!ase_file->frames) {
|
||||
pxl8_io_free_binary_data(file_data);
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
pxl8_stream_seek(&stream, 128);
|
||||
|
||||
for (u16 frame_idx = 0; frame_idx < ase_file->header.frames; frame_idx++) {
|
||||
u32 frame_start = pxl8_stream_position(&stream);
|
||||
|
||||
pxl8_ase_frame_header frame_header;
|
||||
frame_header.frame_bytes = pxl8_read_u32(&stream);
|
||||
frame_header.magic = pxl8_read_u16(&stream);
|
||||
u16 old_chunks = pxl8_read_u16(&stream);
|
||||
frame_header.duration = pxl8_read_u16(&stream);
|
||||
pxl8_skip_bytes(&stream, 2);
|
||||
|
||||
if (frame_header.magic != PXL8_ASE_FRAME_MAGIC) {
|
||||
pxl8_error("Invalid frame magic: 0x%04X", frame_header.magic);
|
||||
result = PXL8_ERROR_ASE_INVALID_FRAME_MAGIC;
|
||||
break;
|
||||
}
|
||||
|
||||
if (old_chunks == 0xFFFF || old_chunks == 0xFFFE) {
|
||||
frame_header.chunks = pxl8_read_u32(&stream);
|
||||
} else {
|
||||
frame_header.chunks = old_chunks;
|
||||
pxl8_skip_bytes(&stream, 4);
|
||||
}
|
||||
|
||||
pxl8_ase_frame* frame = &ase_file->frames[frame_idx];
|
||||
frame->frame_id = frame_idx;
|
||||
frame->width = ase_file->header.width;
|
||||
frame->height = ase_file->header.height;
|
||||
frame->duration = frame_header.duration;
|
||||
|
||||
u32 pixel_count = frame->width * frame->height;
|
||||
frame->pixels = (u8*)calloc(pixel_count, sizeof(u8));
|
||||
if (!frame->pixels) {
|
||||
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||
break;
|
||||
}
|
||||
|
||||
u32 last_tileset_idx = 0xFFFFFFFF;
|
||||
u32 tileset_tile_idx = 0;
|
||||
|
||||
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
|
||||
u32 chunk_start = pxl8_stream_position(&stream);
|
||||
|
||||
pxl8_ase_chunk_header chunk_header;
|
||||
chunk_header.chunk_size = pxl8_read_u32(&stream);
|
||||
chunk_header.chunk_type = pxl8_read_u16(&stream);
|
||||
|
||||
if (chunk_header.chunk_size < 6 || chunk_header.chunk_size > frame_header.frame_bytes) {
|
||||
pxl8_error("Invalid chunk size %u in ASE file (frame_bytes=%u)",
|
||||
chunk_header.chunk_size, frame_header.frame_bytes);
|
||||
result = PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (chunk_header.chunk_type) {
|
||||
case PXL8_ASE_CHUNK_OLD_PALETTE:
|
||||
if (!ase_file->palette.colors) {
|
||||
result = parse_old_palette_chunk(&stream, &ase_file->palette);
|
||||
}
|
||||
break;
|
||||
|
||||
case PXL8_ASE_CHUNK_LAYER: {
|
||||
pxl8_ase_layer* new_layers =
|
||||
(pxl8_ase_layer*)realloc(ase_file->layers,
|
||||
(ase_file->layer_count + 1) * sizeof(pxl8_ase_layer));
|
||||
if (!new_layers) {
|
||||
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||
break;
|
||||
}
|
||||
ase_file->layers = new_layers;
|
||||
|
||||
result = parse_layer_chunk(&stream, &ase_file->layers[ase_file->layer_count]);
|
||||
if (result == PXL8_OK) {
|
||||
ase_file->layer_count++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PXL8_ASE_CHUNK_CEL: {
|
||||
pxl8_ase_cel cel = {0};
|
||||
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
|
||||
if (result == PXL8_OK) {
|
||||
pxl8_ase_cel* new_cels = (pxl8_ase_cel*)realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel));
|
||||
if (!new_cels) {
|
||||
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||
break;
|
||||
}
|
||||
frame->cels = new_cels;
|
||||
frame->cels[frame->cel_count] = cel;
|
||||
frame->cel_count++;
|
||||
|
||||
if (cel.cel_type == 2 && cel.image.pixels) {
|
||||
u32 copy_width = (cel.image.width < frame->width) ? cel.image.width : frame->width;
|
||||
u32 copy_height = (cel.image.height < frame->height) ? cel.image.height : frame->height;
|
||||
|
||||
for (u32 y = 0; y < copy_height; y++) {
|
||||
u32 src_offset = y * cel.image.width;
|
||||
u32 dst_offset = (y + cel.y) * frame->width + cel.x;
|
||||
if (dst_offset + copy_width <= pixel_count) {
|
||||
for (u32 x = 0; x < copy_width; x++) {
|
||||
u8 src_pixel = cel.image.pixels[src_offset + x];
|
||||
bool is_transparent = false;
|
||||
|
||||
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
|
||||
u32 color = ase_file->palette.colors[src_pixel];
|
||||
is_transparent = ((color >> 24) & 0xFF) == 0;
|
||||
}
|
||||
|
||||
if (!is_transparent) {
|
||||
frame->pixels[dst_offset + x] = src_pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PXL8_ASE_CHUNK_PALETTE:
|
||||
if (ase_file->palette.colors) {
|
||||
free(ase_file->palette.colors);
|
||||
ase_file->palette.colors = NULL;
|
||||
}
|
||||
result = parse_palette_chunk(&stream, &ase_file->palette);
|
||||
break;
|
||||
|
||||
case PXL8_ASE_CHUNK_TILESET: {
|
||||
pxl8_ase_tileset* new_tilesets = (pxl8_ase_tileset*)realloc(ase_file->tilesets,
|
||||
(ase_file->tileset_count + 1) * sizeof(pxl8_ase_tileset));
|
||||
if (!new_tilesets) {
|
||||
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||
break;
|
||||
}
|
||||
ase_file->tilesets = new_tilesets;
|
||||
|
||||
memset(&ase_file->tilesets[ase_file->tileset_count], 0, sizeof(pxl8_ase_tileset));
|
||||
result = parse_tileset_chunk(&stream, &ase_file->tilesets[ase_file->tileset_count]);
|
||||
if (result == PXL8_OK) {
|
||||
last_tileset_idx = ase_file->tileset_count;
|
||||
tileset_tile_idx = 0;
|
||||
ase_file->tileset_count++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PXL8_ASE_CHUNK_USER_DATA: {
|
||||
if (last_tileset_idx != 0xFFFFFFFF) {
|
||||
pxl8_ase_tileset* tileset = &ase_file->tilesets[last_tileset_idx];
|
||||
if (tileset_tile_idx < tileset->tile_count) {
|
||||
result = parse_user_data_chunk(&stream, &tileset->tile_user_data[tileset_tile_idx]);
|
||||
tileset_tile_idx++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != PXL8_OK) break;
|
||||
|
||||
pxl8_stream_seek(&stream, chunk_start + chunk_header.chunk_size);
|
||||
}
|
||||
|
||||
if (result != PXL8_OK) break;
|
||||
|
||||
pxl8_stream_seek(&stream, frame_start + frame_header.frame_bytes);
|
||||
}
|
||||
|
||||
pxl8_io_free_binary_data(file_data);
|
||||
|
||||
if (result != PXL8_OK) {
|
||||
pxl8_ase_destroy(ase_file);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
|
||||
if (!ase_file) return;
|
||||
|
||||
if (ase_file->frames) {
|
||||
for (u32 i = 0; i < ase_file->frame_count; i++) {
|
||||
if (ase_file->frames[i].pixels) free(ase_file->frames[i].pixels);
|
||||
if (ase_file->frames[i].cels) {
|
||||
for (u32 j = 0; j < ase_file->frames[i].cel_count; j++) {
|
||||
pxl8_ase_cel* cel = &ase_file->frames[i].cels[j];
|
||||
if (cel->cel_type == 2 && cel->image.pixels) {
|
||||
free(cel->image.pixels);
|
||||
} else if (cel->cel_type == 3 && cel->tilemap.tiles) {
|
||||
free(cel->tilemap.tiles);
|
||||
}
|
||||
}
|
||||
free(ase_file->frames[i].cels);
|
||||
}
|
||||
}
|
||||
free(ase_file->frames);
|
||||
}
|
||||
|
||||
if (ase_file->palette.colors) {
|
||||
free(ase_file->palette.colors);
|
||||
}
|
||||
|
||||
if (ase_file->layers) {
|
||||
for (u32 i = 0; i < ase_file->layer_count; i++) {
|
||||
if (ase_file->layers[i].name) {
|
||||
free(ase_file->layers[i].name);
|
||||
}
|
||||
}
|
||||
free(ase_file->layers);
|
||||
}
|
||||
|
||||
if (ase_file->tilesets) {
|
||||
for (u32 i = 0; i < ase_file->tileset_count; i++) {
|
||||
if (ase_file->tilesets[i].name) free(ase_file->tilesets[i].name);
|
||||
if (ase_file->tilesets[i].pixels) free(ase_file->tilesets[i].pixels);
|
||||
if (ase_file->tilesets[i].tile_user_data) {
|
||||
for (u32 j = 0; j < ase_file->tilesets[i].tile_count; j++) {
|
||||
pxl8_ase_user_data* ud = &ase_file->tilesets[i].tile_user_data[j];
|
||||
if (ud->text) free(ud->text);
|
||||
if (ud->properties) {
|
||||
for (u32 k = 0; k < ud->property_count; k++) {
|
||||
if (ud->properties[k].name) free(ud->properties[k].name);
|
||||
if (ud->properties[k].type == 8 && ud->properties[k].string_val) {
|
||||
free(ud->properties[k].string_val);
|
||||
}
|
||||
}
|
||||
free(ud->properties);
|
||||
}
|
||||
}
|
||||
free(ase_file->tilesets[i].tile_user_data);
|
||||
}
|
||||
}
|
||||
free(ase_file->tilesets);
|
||||
}
|
||||
|
||||
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
||||
}
|
||||
153
src/asset/pxl8_ase.h
Normal file
153
src/asset/pxl8_ase.h
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_types.h"
|
||||
|
||||
#define PXL8_ASE_MAGIC 0xA5E0
|
||||
#define PXL8_ASE_FRAME_MAGIC 0xF1FA
|
||||
#define PXL8_ASE_CHUNK_CEL 0x2005
|
||||
#define PXL8_ASE_CHUNK_LAYER 0x2004
|
||||
#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004
|
||||
#define PXL8_ASE_CHUNK_PALETTE 0x2019
|
||||
#define PXL8_ASE_CHUNK_TILESET 0x2023
|
||||
#define PXL8_ASE_CHUNK_USER_DATA 0x2020
|
||||
|
||||
typedef struct pxl8_ase_header {
|
||||
u32 file_size;
|
||||
u16 magic;
|
||||
u16 frames;
|
||||
u16 width;
|
||||
u16 height;
|
||||
u16 color_depth;
|
||||
u32 flags;
|
||||
u16 speed;
|
||||
u32 transparent_index;
|
||||
u8 n_colors;
|
||||
u8 pixel_width;
|
||||
u8 pixel_height;
|
||||
i16 grid_x;
|
||||
i16 grid_y;
|
||||
u16 grid_width;
|
||||
u16 grid_height;
|
||||
} pxl8_ase_header;
|
||||
|
||||
typedef struct pxl8_ase_frame_header {
|
||||
u32 frame_bytes;
|
||||
u16 magic;
|
||||
u16 chunks;
|
||||
u16 duration;
|
||||
} pxl8_ase_frame_header;
|
||||
|
||||
typedef struct pxl8_ase_chunk_header {
|
||||
u32 chunk_size;
|
||||
u16 chunk_type;
|
||||
} pxl8_ase_chunk_header;
|
||||
|
||||
typedef struct pxl8_ase_cel {
|
||||
u16 layer_index;
|
||||
i16 x;
|
||||
i16 y;
|
||||
u8 opacity;
|
||||
u16 cel_type;
|
||||
|
||||
union {
|
||||
struct {
|
||||
u16 width;
|
||||
u16 height;
|
||||
u8* pixels;
|
||||
} image;
|
||||
|
||||
struct {
|
||||
u16 width;
|
||||
u16 height;
|
||||
u16 bits_per_tile;
|
||||
u32 tile_id_mask;
|
||||
u32 x_flip_mask;
|
||||
u32 y_flip_mask;
|
||||
u32 diag_flip_mask;
|
||||
u32* tiles;
|
||||
} tilemap;
|
||||
};
|
||||
} pxl8_ase_cel;
|
||||
|
||||
typedef struct pxl8_ase_layer {
|
||||
u16 flags;
|
||||
u16 layer_type;
|
||||
u16 child_level;
|
||||
u16 blend_mode;
|
||||
u8 opacity;
|
||||
char* name;
|
||||
} pxl8_ase_layer;
|
||||
|
||||
typedef struct pxl8_ase_palette {
|
||||
u32 entry_count;
|
||||
u32 first_color;
|
||||
u32 last_color;
|
||||
u32* colors;
|
||||
} pxl8_ase_palette;
|
||||
|
||||
typedef struct pxl8_ase_frame {
|
||||
u16 frame_id;
|
||||
u16 width;
|
||||
u16 height;
|
||||
i16 x;
|
||||
i16 y;
|
||||
u16 duration;
|
||||
u8* pixels;
|
||||
u32 cel_count;
|
||||
pxl8_ase_cel* cels;
|
||||
} pxl8_ase_frame;
|
||||
|
||||
typedef struct pxl8_ase_property {
|
||||
char* name;
|
||||
u32 type;
|
||||
union {
|
||||
bool bool_val;
|
||||
i32 int_val;
|
||||
f32 float_val;
|
||||
char* string_val;
|
||||
};
|
||||
} pxl8_ase_property;
|
||||
|
||||
typedef struct pxl8_ase_user_data {
|
||||
char* text;
|
||||
u32 color;
|
||||
bool has_color;
|
||||
bool has_text;
|
||||
u32 property_count;
|
||||
pxl8_ase_property* properties;
|
||||
} pxl8_ase_user_data;
|
||||
|
||||
typedef struct pxl8_ase_tileset {
|
||||
u32 id;
|
||||
u32 flags;
|
||||
u32 tile_count;
|
||||
u16 tile_width;
|
||||
u16 tile_height;
|
||||
i16 base_index;
|
||||
char* name;
|
||||
u32 pixels_size;
|
||||
u8* pixels;
|
||||
pxl8_ase_user_data* tile_user_data;
|
||||
} pxl8_ase_tileset;
|
||||
|
||||
typedef struct pxl8_ase_file {
|
||||
u32 frame_count;
|
||||
pxl8_ase_frame* frames;
|
||||
pxl8_ase_header header;
|
||||
u32 layer_count;
|
||||
pxl8_ase_layer* layers;
|
||||
pxl8_ase_palette palette;
|
||||
u32 tileset_count;
|
||||
pxl8_ase_tileset* tilesets;
|
||||
} pxl8_ase_file;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file);
|
||||
void pxl8_ase_destroy(pxl8_ase_file* ase_file);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
634
src/asset/pxl8_cart.c
Normal file
634
src/asset/pxl8_cart.c
Normal file
|
|
@ -0,0 +1,634 @@
|
|||
#include "pxl8_cart.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "pxl8_log.h"
|
||||
|
||||
#define PXL8_CART_MAGIC 0x43585850
|
||||
#define PXL8_CART_VERSION 1
|
||||
#define PXL8_CART_TRAILER_MAGIC 0x544E4250
|
||||
|
||||
typedef struct {
|
||||
u32 magic;
|
||||
u16 version;
|
||||
u16 flags;
|
||||
u32 file_count;
|
||||
u32 toc_size;
|
||||
} pxl8_cart_header;
|
||||
|
||||
typedef struct {
|
||||
u32 offset;
|
||||
u32 size;
|
||||
u16 path_len;
|
||||
} pxl8_cart_entry;
|
||||
|
||||
typedef struct {
|
||||
u32 magic;
|
||||
u32 cart_offset;
|
||||
u32 cart_size;
|
||||
} pxl8_cart_trailer;
|
||||
|
||||
typedef struct {
|
||||
char* path;
|
||||
u32 offset;
|
||||
u32 size;
|
||||
} pxl8_cart_file;
|
||||
|
||||
struct pxl8_cart {
|
||||
u8* data;
|
||||
u32 data_size;
|
||||
pxl8_cart_file* files;
|
||||
u32 file_count;
|
||||
char* base_path;
|
||||
char* title;
|
||||
pxl8_resolution resolution;
|
||||
pxl8_size window_size;
|
||||
pxl8_pixel_mode pixel_mode;
|
||||
bool is_folder;
|
||||
bool is_mounted;
|
||||
};
|
||||
|
||||
static pxl8_cart* pxl8_current_cart = NULL;
|
||||
static char* pxl8_original_cwd = NULL;
|
||||
|
||||
static bool is_directory(const char* path) {
|
||||
struct stat st;
|
||||
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
static bool is_pxc_file(const char* path) {
|
||||
size_t len = strlen(path);
|
||||
return len > 4 && strcmp(path + len - 4, ".pxc") == 0;
|
||||
}
|
||||
|
||||
static bool has_main_script(const char* base_path) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/main.fnl", base_path);
|
||||
if (access(path, F_OK) == 0) return true;
|
||||
snprintf(path, sizeof(path), "%s/main.lua", base_path);
|
||||
return access(path, F_OK) == 0;
|
||||
}
|
||||
|
||||
static void collect_files_recursive(const char* dir_path, const char* prefix,
|
||||
char*** paths, u32* count, u32* capacity) {
|
||||
DIR* dir = opendir(dir_path);
|
||||
if (!dir) return;
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
||||
|
||||
char full_path[1024];
|
||||
char rel_path[1024];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
snprintf(rel_path, sizeof(rel_path), "%s%s", prefix, entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char new_prefix[1025];
|
||||
snprintf(new_prefix, sizeof(new_prefix), "%s/", rel_path);
|
||||
collect_files_recursive(full_path, new_prefix, paths, count, capacity);
|
||||
} else {
|
||||
if (*count >= *capacity) {
|
||||
*capacity = (*capacity == 0) ? 64 : (*capacity * 2);
|
||||
*paths = realloc(*paths, *capacity * sizeof(char*));
|
||||
}
|
||||
(*paths)[(*count)++] = strdup(rel_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
static pxl8_cart_file* find_file(const pxl8_cart* cart, const char* path) {
|
||||
if (!cart || !cart->files) return NULL;
|
||||
for (u32 i = 0; i < cart->file_count; i++) {
|
||||
if (strcmp(cart->files[i].path, path) == 0) {
|
||||
return &cart->files[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pxl8_result load_packed_cart(pxl8_cart* cart, const u8* data, u32 size) {
|
||||
if (size < sizeof(pxl8_cart_header)) return PXL8_ERROR_INVALID_FORMAT;
|
||||
|
||||
const pxl8_cart_header* header = (const pxl8_cart_header*)data;
|
||||
if (header->magic != PXL8_CART_MAGIC) return PXL8_ERROR_INVALID_FORMAT;
|
||||
if (header->version > PXL8_CART_VERSION) return PXL8_ERROR_INVALID_FORMAT;
|
||||
|
||||
cart->data = malloc(size);
|
||||
if (!cart->data) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
memcpy(cart->data, data, size);
|
||||
cart->data_size = size;
|
||||
|
||||
cart->file_count = header->file_count;
|
||||
cart->files = calloc(cart->file_count, sizeof(pxl8_cart_file));
|
||||
if (!cart->files) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
const u8* toc = cart->data + sizeof(pxl8_cart_header);
|
||||
for (u32 i = 0; i < cart->file_count; i++) {
|
||||
const pxl8_cart_entry* entry = (const pxl8_cart_entry*)toc;
|
||||
toc += sizeof(pxl8_cart_entry);
|
||||
|
||||
cart->files[i].path = malloc(entry->path_len + 1);
|
||||
memcpy(cart->files[i].path, toc, entry->path_len);
|
||||
cart->files[i].path[entry->path_len] = '\0';
|
||||
toc += entry->path_len;
|
||||
|
||||
cart->files[i].offset = entry->offset;
|
||||
cart->files[i].size = entry->size;
|
||||
}
|
||||
|
||||
cart->is_folder = false;
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_cart* pxl8_cart_create(void) {
|
||||
pxl8_cart* cart = calloc(1, sizeof(pxl8_cart));
|
||||
if (cart) {
|
||||
cart->resolution = PXL8_RESOLUTION_640x360;
|
||||
cart->window_size = (pxl8_size){1280, 720};
|
||||
cart->pixel_mode = PXL8_PIXEL_INDEXED;
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
pxl8_cart* pxl8_get_cart(void) {
|
||||
return pxl8_current_cart;
|
||||
}
|
||||
|
||||
void pxl8_cart_destroy(pxl8_cart* cart) {
|
||||
if (!cart) return;
|
||||
pxl8_cart_unload(cart);
|
||||
free(cart);
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
|
||||
if (!cart || !path) return PXL8_ERROR_NULL_POINTER;
|
||||
pxl8_cart_unload(cart);
|
||||
|
||||
if (is_directory(path)) {
|
||||
cart->base_path = realpath(path, NULL);
|
||||
if (!cart->base_path) {
|
||||
pxl8_error("Failed to resolve cart path: %s", path);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
cart->is_folder = true;
|
||||
|
||||
if (!has_main_script(cart->base_path)) {
|
||||
pxl8_error("No main.fnl or main.lua found in cart: %s", path);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
pxl8_info("Loaded cart");
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
if (is_pxc_file(path)) {
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (!file) {
|
||||
pxl8_error("Failed to open cart: %s", path);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
fseek(file, 0, SEEK_END);
|
||||
u32 size = (u32)ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
|
||||
u8* data = malloc(size);
|
||||
if (!data || fread(data, 1, size, file) != size) {
|
||||
free(data);
|
||||
fclose(file);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
pxl8_result result = load_packed_cart(cart, data, size);
|
||||
free(data);
|
||||
|
||||
if (result == PXL8_OK) {
|
||||
pxl8_info("Loaded cart");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pxl8_error("Unknown cart format: %s", path);
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path) {
|
||||
if (!cart || !exe_path) return PXL8_ERROR_NULL_POINTER;
|
||||
|
||||
FILE* file = fopen(exe_path, "rb");
|
||||
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
|
||||
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
|
||||
pxl8_cart_trailer trailer;
|
||||
if (fread(&trailer, sizeof(trailer), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
if (trailer.magic != PXL8_CART_TRAILER_MAGIC) {
|
||||
fclose(file);
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
fseek(file, trailer.cart_offset, SEEK_SET);
|
||||
u8* data = malloc(trailer.cart_size);
|
||||
if (!data || fread(data, 1, trailer.cart_size, file) != trailer.cart_size) {
|
||||
free(data);
|
||||
fclose(file);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
pxl8_result result = load_packed_cart(cart, data, trailer.cart_size);
|
||||
free(data);
|
||||
|
||||
if (result == PXL8_OK) {
|
||||
pxl8_info("Loaded embedded cart");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void pxl8_cart_unload(pxl8_cart* cart) {
|
||||
if (!cart) return;
|
||||
|
||||
if (cart->title) {
|
||||
pxl8_info("Unloaded cart: %s", cart->title);
|
||||
pxl8_cart_unmount(cart);
|
||||
free(cart->title);
|
||||
cart->title = NULL;
|
||||
} else if (cart->base_path || cart->data) {
|
||||
pxl8_info("Unloaded cart");
|
||||
pxl8_cart_unmount(cart);
|
||||
}
|
||||
|
||||
if (cart->files) {
|
||||
for (u32 i = 0; i < cart->file_count; i++) {
|
||||
free(cart->files[i].path);
|
||||
}
|
||||
free(cart->files);
|
||||
cart->files = NULL;
|
||||
}
|
||||
cart->file_count = 0;
|
||||
|
||||
free(cart->data);
|
||||
cart->data = NULL;
|
||||
cart->data_size = 0;
|
||||
|
||||
free(cart->base_path);
|
||||
cart->base_path = NULL;
|
||||
cart->is_folder = false;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_mount(pxl8_cart* cart) {
|
||||
if (!cart) return PXL8_ERROR_NULL_POINTER;
|
||||
if (cart->is_mounted) return PXL8_OK;
|
||||
|
||||
if (pxl8_current_cart) {
|
||||
pxl8_cart_unmount(pxl8_current_cart);
|
||||
}
|
||||
|
||||
if (cart->is_folder) {
|
||||
pxl8_original_cwd = getcwd(NULL, 0);
|
||||
if (chdir(cart->base_path) != 0) {
|
||||
pxl8_error("Failed to change to cart directory: %s", cart->base_path);
|
||||
free(pxl8_original_cwd);
|
||||
pxl8_original_cwd = NULL;
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
cart->is_mounted = true;
|
||||
pxl8_current_cart = cart;
|
||||
if (cart->title) {
|
||||
pxl8_info("Mounted cart: %s", cart->title);
|
||||
} else {
|
||||
pxl8_info("Mounted cart");
|
||||
}
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
void pxl8_cart_unmount(pxl8_cart* cart) {
|
||||
if (!cart || !cart->is_mounted) return;
|
||||
|
||||
if (pxl8_original_cwd) {
|
||||
chdir(pxl8_original_cwd);
|
||||
free(pxl8_original_cwd);
|
||||
pxl8_original_cwd = NULL;
|
||||
}
|
||||
|
||||
cart->is_mounted = false;
|
||||
if (pxl8_current_cart == cart) {
|
||||
pxl8_current_cart = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const char* pxl8_cart_get_base_path(const pxl8_cart* cart) {
|
||||
return cart ? cart->base_path : NULL;
|
||||
}
|
||||
|
||||
const char* pxl8_cart_get_title(const pxl8_cart* cart) {
|
||||
return cart ? cart->title : NULL;
|
||||
}
|
||||
|
||||
void pxl8_cart_set_title(pxl8_cart* cart, const char* title) {
|
||||
if (!cart || !title) return;
|
||||
free(cart->title);
|
||||
cart->title = strdup(title);
|
||||
}
|
||||
|
||||
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart) {
|
||||
return cart ? cart->resolution : PXL8_RESOLUTION_640x360;
|
||||
}
|
||||
|
||||
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution) {
|
||||
if (cart) cart->resolution = resolution;
|
||||
}
|
||||
|
||||
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart) {
|
||||
return cart ? cart->window_size : (pxl8_size){1280, 720};
|
||||
}
|
||||
|
||||
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size) {
|
||||
if (cart) cart->window_size = size;
|
||||
}
|
||||
|
||||
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart) {
|
||||
return cart ? cart->pixel_mode : PXL8_PIXEL_INDEXED;
|
||||
}
|
||||
|
||||
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode) {
|
||||
if (cart) cart->pixel_mode = mode;
|
||||
}
|
||||
|
||||
bool pxl8_cart_is_packed(const pxl8_cart* cart) {
|
||||
return cart && !cart->is_folder;
|
||||
}
|
||||
|
||||
bool pxl8_cart_has_embedded(const char* exe_path) {
|
||||
FILE* file = fopen(exe_path, "rb");
|
||||
if (!file) return false;
|
||||
|
||||
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
|
||||
pxl8_cart_trailer trailer;
|
||||
bool has = (fread(&trailer, sizeof(trailer), 1, file) == 1 &&
|
||||
trailer.magic == PXL8_CART_TRAILER_MAGIC);
|
||||
fclose(file);
|
||||
return has;
|
||||
}
|
||||
|
||||
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
|
||||
if (!cart || !path) return false;
|
||||
|
||||
if (cart->is_folder) {
|
||||
char full_path[512];
|
||||
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
|
||||
return access(full_path, F_OK) == 0;
|
||||
}
|
||||
|
||||
return find_file(cart, path) != NULL;
|
||||
}
|
||||
|
||||
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) {
|
||||
if (!cart || !relative_path || !out_path || out_size == 0) return false;
|
||||
|
||||
if (cart->is_folder && cart->base_path) {
|
||||
i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path);
|
||||
return written >= 0 && (size_t)written < out_size;
|
||||
}
|
||||
|
||||
i32 written = snprintf(out_path, out_size, "%s", relative_path);
|
||||
return written >= 0 && (size_t)written < out_size;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out) {
|
||||
if (!cart || !path || !data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
|
||||
|
||||
if (cart->is_folder) {
|
||||
char full_path[512];
|
||||
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) {
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
FILE* file = fopen(full_path, "rb");
|
||||
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
|
||||
fseek(file, 0, SEEK_END);
|
||||
*size_out = (u32)ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
|
||||
*data_out = malloc(*size_out);
|
||||
if (!*data_out || fread(*data_out, 1, *size_out, file) != *size_out) {
|
||||
free(*data_out);
|
||||
*data_out = NULL;
|
||||
fclose(file);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
fclose(file);
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_cart_file* cf = find_file(cart, path);
|
||||
if (!cf) return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
|
||||
*size_out = cf->size;
|
||||
*data_out = malloc(cf->size);
|
||||
if (!*data_out) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
memcpy(*data_out, cart->data + cf->offset, cf->size);
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
void pxl8_cart_free_file(u8* data) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
|
||||
if (!folder_path || !output_path) return PXL8_ERROR_NULL_POINTER;
|
||||
|
||||
if (!is_directory(folder_path)) {
|
||||
pxl8_error("Cart folder not found: %s", folder_path);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!has_main_script(folder_path)) {
|
||||
pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
char** paths = NULL;
|
||||
u32 count = 0, capacity = 0;
|
||||
collect_files_recursive(folder_path, "", &paths, &count, &capacity);
|
||||
|
||||
if (count == 0) {
|
||||
pxl8_error("No files found in cart folder");
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
u32 toc_size = 0;
|
||||
for (u32 i = 0; i < count; i++) {
|
||||
toc_size += sizeof(pxl8_cart_entry) + strlen(paths[i]);
|
||||
}
|
||||
|
||||
u32 data_offset = sizeof(pxl8_cart_header) + toc_size;
|
||||
u32 total_size = data_offset;
|
||||
|
||||
u32* file_sizes = malloc(count * sizeof(u32));
|
||||
for (u32 i = 0; i < count; i++) {
|
||||
char full_path[1024];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0) {
|
||||
file_sizes[i] = (u32)st.st_size;
|
||||
total_size += file_sizes[i];
|
||||
} else {
|
||||
file_sizes[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
u8* buffer = calloc(1, total_size);
|
||||
if (!buffer) {
|
||||
free(file_sizes);
|
||||
for (u32 i = 0; i < count; i++) free(paths[i]);
|
||||
free(paths);
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
pxl8_cart_header* header = (pxl8_cart_header*)buffer;
|
||||
header->magic = PXL8_CART_MAGIC;
|
||||
header->version = PXL8_CART_VERSION;
|
||||
header->flags = 0;
|
||||
header->file_count = count;
|
||||
header->toc_size = toc_size;
|
||||
|
||||
u8* toc = buffer + sizeof(pxl8_cart_header);
|
||||
u32 file_offset = data_offset;
|
||||
|
||||
pxl8_info("Packing cart: %s -> %s", folder_path, output_path);
|
||||
|
||||
for (u32 i = 0; i < count; i++) {
|
||||
pxl8_cart_entry* entry = (pxl8_cart_entry*)toc;
|
||||
entry->offset = file_offset;
|
||||
entry->size = file_sizes[i];
|
||||
entry->path_len = (u16)strlen(paths[i]);
|
||||
toc += sizeof(pxl8_cart_entry);
|
||||
|
||||
memcpy(toc, paths[i], entry->path_len);
|
||||
toc += entry->path_len;
|
||||
|
||||
char full_path[1024];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
|
||||
FILE* file = fopen(full_path, "rb");
|
||||
if (file) {
|
||||
fread(buffer + file_offset, 1, file_sizes[i], file);
|
||||
fclose(file);
|
||||
pxl8_info(" %s (%u bytes)", paths[i], file_sizes[i]);
|
||||
}
|
||||
|
||||
file_offset += file_sizes[i];
|
||||
free(paths[i]);
|
||||
}
|
||||
free(paths);
|
||||
free(file_sizes);
|
||||
|
||||
FILE* out = fopen(output_path, "wb");
|
||||
if (!out) {
|
||||
free(buffer);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
fwrite(buffer, 1, total_size, out);
|
||||
fclose(out);
|
||||
free(buffer);
|
||||
|
||||
pxl8_info("Cart packed: %u files, %u bytes", count, total_size);
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path) {
|
||||
if (!input_path || !output_path || !exe_path) return PXL8_ERROR_NULL_POINTER;
|
||||
|
||||
u8* cart_data = NULL;
|
||||
u32 cart_size = 0;
|
||||
bool free_cart = false;
|
||||
|
||||
if (is_directory(input_path)) {
|
||||
char temp_pxc[256];
|
||||
snprintf(temp_pxc, sizeof(temp_pxc), "/tmp/pxl8_bundle_%d.pxc", getpid());
|
||||
pxl8_result result = pxl8_cart_pack(input_path, temp_pxc);
|
||||
if (result != PXL8_OK) return result;
|
||||
|
||||
FILE* f = fopen(temp_pxc, "rb");
|
||||
if (!f) return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
fseek(f, 0, SEEK_END);
|
||||
cart_size = (u32)ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
cart_data = malloc(cart_size);
|
||||
fread(cart_data, 1, cart_size, f);
|
||||
fclose(f);
|
||||
unlink(temp_pxc);
|
||||
free_cart = true;
|
||||
} else if (is_pxc_file(input_path)) {
|
||||
FILE* f = fopen(input_path, "rb");
|
||||
if (!f) return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
fseek(f, 0, SEEK_END);
|
||||
cart_size = (u32)ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
cart_data = malloc(cart_size);
|
||||
fread(cart_data, 1, cart_size, f);
|
||||
fclose(f);
|
||||
free_cart = true;
|
||||
} else {
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
FILE* exe = fopen(exe_path, "rb");
|
||||
if (!exe) {
|
||||
if (free_cart) free(cart_data);
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
fseek(exe, 0, SEEK_END);
|
||||
u32 exe_size = (u32)ftell(exe);
|
||||
fseek(exe, 0, SEEK_SET);
|
||||
|
||||
u8* exe_data = malloc(exe_size);
|
||||
fread(exe_data, 1, exe_size, exe);
|
||||
fclose(exe);
|
||||
|
||||
FILE* out = fopen(output_path, "wb");
|
||||
if (!out) {
|
||||
free(exe_data);
|
||||
if (free_cart) free(cart_data);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
fwrite(exe_data, 1, exe_size, out);
|
||||
free(exe_data);
|
||||
|
||||
u32 cart_offset = exe_size;
|
||||
fwrite(cart_data, 1, cart_size, out);
|
||||
if (free_cart) free(cart_data);
|
||||
|
||||
pxl8_cart_trailer trailer = {
|
||||
.magic = PXL8_CART_TRAILER_MAGIC,
|
||||
.cart_offset = cart_offset,
|
||||
.cart_size = cart_size
|
||||
};
|
||||
fwrite(&trailer, sizeof(trailer), 1, out);
|
||||
fclose(out);
|
||||
|
||||
chmod(output_path, 0755);
|
||||
|
||||
pxl8_info("Bundle created: %s", output_path);
|
||||
return PXL8_OK;
|
||||
}
|
||||
43
src/asset/pxl8_cart.h
Normal file
43
src/asset/pxl8_cart.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_types.h"
|
||||
|
||||
typedef struct pxl8_cart pxl8_cart;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
pxl8_cart* pxl8_cart_create(void);
|
||||
void pxl8_cart_destroy(pxl8_cart* cart);
|
||||
|
||||
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path);
|
||||
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
|
||||
|
||||
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
|
||||
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path);
|
||||
void pxl8_cart_unload(pxl8_cart* cart);
|
||||
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
|
||||
void pxl8_cart_unmount(pxl8_cart* cart);
|
||||
|
||||
pxl8_cart* pxl8_get_cart(void);
|
||||
const char* pxl8_cart_get_base_path(const pxl8_cart* cart);
|
||||
const char* pxl8_cart_get_title(const pxl8_cart* cart);
|
||||
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart);
|
||||
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart);
|
||||
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart);
|
||||
void pxl8_cart_set_title(pxl8_cart* cart, const char* title);
|
||||
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution);
|
||||
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size);
|
||||
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode);
|
||||
bool pxl8_cart_is_packed(const pxl8_cart* cart);
|
||||
bool pxl8_cart_has_embedded(const char* exe_path);
|
||||
|
||||
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path);
|
||||
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size);
|
||||
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out);
|
||||
void pxl8_cart_free_file(u8* data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
103
src/asset/pxl8_embed.h
Normal file
103
src/asset/pxl8_embed.h
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_types.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char embed_fennel[] = {
|
||||
#embed "lib/fennel/fennel.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8[] = {
|
||||
#embed "src/lua/pxl8.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_anim[] = {
|
||||
#embed "src/lua/pxl8/anim.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_bytes[] = {
|
||||
#embed "src/lua/pxl8/bytes.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_core[] = {
|
||||
#embed "src/lua/pxl8/core.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_effects[] = {
|
||||
#embed "src/lua/pxl8/effects.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_gfx2d[] = {
|
||||
#embed "src/lua/pxl8/gfx2d.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_gfx3d[] = {
|
||||
#embed "src/lua/pxl8/gfx3d.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_gui[] = {
|
||||
#embed "src/lua/pxl8/gui.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_input[] = {
|
||||
#embed "src/lua/pxl8/input.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_math[] = {
|
||||
#embed "src/lua/pxl8/math.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_net[] = {
|
||||
#embed "src/lua/pxl8/net.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_particles[] = {
|
||||
#embed "src/lua/pxl8/particles.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_sfx[] = {
|
||||
#embed "src/lua/pxl8/sfx.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_tilemap[] = {
|
||||
#embed "src/lua/pxl8/tilemap.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_transition[] = {
|
||||
#embed "src/lua/pxl8/transition.lua"
|
||||
, 0 };
|
||||
|
||||
static const char embed_pxl8_world[] = {
|
||||
#embed "src/lua/pxl8/world.lua"
|
||||
, 0 };
|
||||
|
||||
#define PXL8_EMBED_ENTRY(var, name) {name, var, sizeof(var) - 1}
|
||||
|
||||
typedef struct { const char* name; const char* data; u32 size; } pxl8_embed;
|
||||
|
||||
static const pxl8_embed pxl8_embeds[] = {
|
||||
PXL8_EMBED_ENTRY(embed_fennel, "fennel"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8, "pxl8"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_anim, "pxl8.anim"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_bytes, "pxl8.bytes"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_core, "pxl8.core"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_effects, "pxl8.effects"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_gfx2d, "pxl8.gfx2d"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_gfx3d, "pxl8.gfx3d"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_gui, "pxl8.gui"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_input, "pxl8.input"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_net, "pxl8.net"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_particles, "pxl8.particles"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_sfx, "pxl8.sfx"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_tilemap, "pxl8.tilemap"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"),
|
||||
PXL8_EMBED_ENTRY(embed_pxl8_world, "pxl8.world"),
|
||||
{0}
|
||||
};
|
||||
|
||||
static inline const pxl8_embed* pxl8_embed_find(const char* name) {
|
||||
for (const pxl8_embed* e = pxl8_embeds; e->name; e++)
|
||||
if (strcmp(e->name, name) == 0) return e;
|
||||
return NULL;
|
||||
}
|
||||
272
src/asset/pxl8_save.c
Normal file
272
src/asset/pxl8_save.c
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
#include "pxl8_save.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <shlobj.h>
|
||||
#define PATH_SEP '\\'
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#define PATH_SEP '/'
|
||||
#endif
|
||||
|
||||
#include "pxl8_log.h"
|
||||
|
||||
typedef struct {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 size;
|
||||
u32 checksum;
|
||||
} pxl8_save_header;
|
||||
|
||||
struct pxl8_save {
|
||||
char directory[PXL8_SAVE_MAX_PATH];
|
||||
u32 magic;
|
||||
u32 version;
|
||||
};
|
||||
|
||||
static u32 pxl8_save_checksum(const u8* data, u32 size) {
|
||||
u32 hash = 2166136261u;
|
||||
for (u32 i = 0; i < size; i++) {
|
||||
hash ^= data[i];
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void pxl8_save_get_slot_path(pxl8_save* save, u8 slot, char* path, size_t path_size) {
|
||||
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
snprintf(path, path_size, "%s%chotreload.sav", save->directory, PATH_SEP);
|
||||
} else {
|
||||
snprintf(path, path_size, "%s%csave%d.sav", save->directory, PATH_SEP, slot);
|
||||
}
|
||||
}
|
||||
|
||||
static pxl8_result pxl8_save_ensure_directory(const char* path) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
return S_ISDIR(st.st_mode) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (_mkdir(path) != 0 && errno != EEXIST) {
|
||||
#else
|
||||
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
|
||||
#endif
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version) {
|
||||
if (!game_name) return NULL;
|
||||
|
||||
pxl8_save* save = (pxl8_save*)calloc(1, sizeof(pxl8_save));
|
||||
if (!save) return NULL;
|
||||
|
||||
save->magic = magic;
|
||||
save->version = version;
|
||||
|
||||
char base_dir[PXL8_SAVE_MAX_PATH];
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, base_dir))) {
|
||||
snprintf(save->directory, sizeof(save->directory),
|
||||
"%s%cpxl8%c%s", base_dir, PATH_SEP, PATH_SEP, game_name);
|
||||
} else {
|
||||
free(save);
|
||||
return NULL;
|
||||
}
|
||||
#else
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) home = pw->pw_dir;
|
||||
}
|
||||
if (!home) {
|
||||
free(save);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
snprintf(base_dir, sizeof(base_dir), "%s/.local/share", home);
|
||||
pxl8_save_ensure_directory(base_dir);
|
||||
|
||||
snprintf(base_dir, sizeof(base_dir), "%s/.local/share/pxl8", home);
|
||||
pxl8_save_ensure_directory(base_dir);
|
||||
|
||||
snprintf(save->directory, sizeof(save->directory),
|
||||
"%s/.local/share/pxl8/%s", home, game_name);
|
||||
#endif
|
||||
|
||||
if (pxl8_save_ensure_directory(save->directory) != PXL8_OK) {
|
||||
free(save);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pxl8_info("Save system initialized: %s", save->directory);
|
||||
return save;
|
||||
}
|
||||
|
||||
void pxl8_save_destroy(pxl8_save* save) {
|
||||
if (save) {
|
||||
free(save);
|
||||
}
|
||||
}
|
||||
|
||||
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size) {
|
||||
if (!save) return PXL8_ERROR_NULL_POINTER;
|
||||
if (!data || size == 0) return PXL8_ERROR_NULL_POINTER;
|
||||
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
char path[PXL8_SAVE_MAX_PATH];
|
||||
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
|
||||
|
||||
FILE* file = fopen(path, "wb");
|
||||
if (!file) {
|
||||
pxl8_error("Failed to open save file for writing: %s", path);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
pxl8_save_header header = {
|
||||
.magic = save->magic,
|
||||
.version = save->version,
|
||||
.size = size,
|
||||
.checksum = pxl8_save_checksum(data, size)
|
||||
};
|
||||
|
||||
bool success = true;
|
||||
if (fwrite(&header, sizeof(header), 1, file) != 1) success = false;
|
||||
if (success && fwrite(data, 1, size, file) != size) success = false;
|
||||
|
||||
fclose(file);
|
||||
|
||||
if (!success) {
|
||||
pxl8_error("Failed to write save data");
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
pxl8_debug("Hot reload state saved (%u bytes)", size);
|
||||
} else {
|
||||
pxl8_info("Game saved to slot %d (%u bytes)", slot, size);
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out) {
|
||||
if (!save) return PXL8_ERROR_NULL_POINTER;
|
||||
if (!data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
|
||||
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
*data_out = NULL;
|
||||
*size_out = 0;
|
||||
|
||||
char path[PXL8_SAVE_MAX_PATH];
|
||||
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
|
||||
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (!file) {
|
||||
return PXL8_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
pxl8_save_header header;
|
||||
if (fread(&header, sizeof(header), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
if (header.magic != save->magic) {
|
||||
fclose(file);
|
||||
pxl8_error("Invalid save file magic");
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
if (header.version > save->version) {
|
||||
fclose(file);
|
||||
pxl8_error("Save file version too new: %u", header.version);
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
u8* data = (u8*)malloc(header.size);
|
||||
if (!data) {
|
||||
fclose(file);
|
||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (fread(data, 1, header.size, file) != header.size) {
|
||||
free(data);
|
||||
fclose(file);
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
|
||||
u32 checksum = pxl8_save_checksum(data, header.size);
|
||||
if (checksum != header.checksum) {
|
||||
free(data);
|
||||
pxl8_error("Save file checksum mismatch");
|
||||
return PXL8_ERROR_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
*data_out = data;
|
||||
*size_out = header.size;
|
||||
|
||||
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
pxl8_debug("Hot reload state loaded (%u bytes)", header.size);
|
||||
} else {
|
||||
pxl8_info("Game loaded from slot %d (%u bytes)", slot, header.size);
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
void pxl8_save_free(u8* data) {
|
||||
if (data) {
|
||||
free(data);
|
||||
}
|
||||
}
|
||||
|
||||
bool pxl8_save_exists(pxl8_save* save, u8 slot) {
|
||||
if (!save) return false;
|
||||
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char path[PXL8_SAVE_MAX_PATH];
|
||||
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
|
||||
|
||||
struct stat st;
|
||||
return stat(path, &st) == 0;
|
||||
}
|
||||
|
||||
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot) {
|
||||
if (!save) return PXL8_ERROR_NULL_POINTER;
|
||||
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
|
||||
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
char path[PXL8_SAVE_MAX_PATH];
|
||||
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
|
||||
|
||||
if (remove(path) != 0 && errno != ENOENT) {
|
||||
return PXL8_ERROR_SYSTEM_FAILURE;
|
||||
}
|
||||
|
||||
return PXL8_OK;
|
||||
}
|
||||
|
||||
const char* pxl8_save_get_directory(pxl8_save* save) {
|
||||
return save ? save->directory : NULL;
|
||||
}
|
||||
27
src/asset/pxl8_save.h
Normal file
27
src/asset/pxl8_save.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_types.h"
|
||||
|
||||
#define PXL8_SAVE_MAX_SLOTS 10
|
||||
#define PXL8_SAVE_HOTRELOAD_SLOT 255
|
||||
#define PXL8_SAVE_MAX_PATH 512
|
||||
|
||||
typedef struct pxl8_save pxl8_save;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version);
|
||||
void pxl8_save_destroy(pxl8_save* save);
|
||||
|
||||
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot);
|
||||
bool pxl8_save_exists(pxl8_save* save, u8 slot);
|
||||
void pxl8_save_free(u8* data);
|
||||
const char* pxl8_save_get_directory(pxl8_save* save);
|
||||
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out);
|
||||
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue