From 04d3af11a9c8a5cacb03720a1039eaea636d2753 Mon Sep 17 00:00:00 2001 From: asrael Date: Fri, 28 Nov 2025 23:42:57 -0600 Subject: [PATCH] add save and bundle pxl8 with game for standalone game distribution --- pxl8.sh | 6 +- src/lua/pxl8.lua | 5 + src/lua/pxl8/gfx2d.lua | 20 ++ src/pxl8.c | 109 +++++--- src/pxl8_ase.c | 1 + src/pxl8_atlas.c | 14 +- src/pxl8_atlas.h | 6 +- src/pxl8_cart.c | 609 +++++++++++++++++++++++++++++------------ src/pxl8_cart.h | 22 +- src/pxl8_color.h | 4 +- src/pxl8_game.h | 2 - src/pxl8_gfx.c | 108 ++++++-- src/pxl8_gfx.h | 37 +-- src/pxl8_hal.h | 6 +- src/pxl8_io.c | 30 +- src/pxl8_save.c | 272 ++++++++++++++++++ src/pxl8_save.h | 27 ++ src/pxl8_script.c | 182 +++++++++++- src/pxl8_script.h | 2 +- src/pxl8_sdl3.c | 22 +- src/pxl8_sys.h | 2 - src/pxl8_tilemap.c | 4 +- src/pxl8_tilesheet.c | 14 +- src/pxl8_transition.c | 4 +- src/pxl8_types.h | 11 +- 25 files changed, 1173 insertions(+), 346 deletions(-) create mode 100644 src/pxl8_save.c create mode 100644 src/pxl8_save.h diff --git a/pxl8.sh b/pxl8.sh index 299de1a..f920079 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -173,7 +173,7 @@ timestamp() { update_fennel() { print_info "Fetching Fennel" - local version="1.5.3" + local version="1.6.0" if curl -sL --max-time 5 -o lib/fennel/fennel.lua "https://fennel-lang.org/downloads/fennel-${version}.lua" 2>/dev/null; then if [[ -f "lib/fennel/fennel.lua" ]] && [[ -s "lib/fennel/fennel.lua" ]]; then @@ -217,11 +217,10 @@ update_luajit() { print_info "Updated LuaJIT (${version})" } - update_miniz() { print_info "Fetching miniz" - local version="3.0.2" + local version="3.1.0" if curl -sL --max-time 5 -o /tmp/miniz.zip "https://github.com/richgel999/miniz/releases/download/${version}/miniz-${version}.zip" 2>/dev/null; then unzip -qjo /tmp/miniz.zip miniz.c miniz.h -d lib/miniz/ 2>/dev/null @@ -344,6 +343,7 @@ case "$COMMAND" in src/pxl8_io.c src/pxl8_math.c src/pxl8_rec.c + src/pxl8_save.c src/pxl8_script.c src/pxl8_sdl3.c src/pxl8_tilemap.c diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index 2927384..7863ac9 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -42,6 +42,11 @@ pxl8.create_texture = gfx2d.create_texture pxl8.upload_atlas = gfx2d.upload_atlas pxl8.gfx_color_ramp = gfx2d.color_ramp pxl8.gfx_fade_palette = gfx2d.fade_palette +pxl8.gfx_cycle_palette = gfx2d.cycle_palette +pxl8.add_palette_cycle = gfx2d.add_palette_cycle +pxl8.remove_palette_cycle = gfx2d.remove_palette_cycle +pxl8.set_palette_cycle_speed = gfx2d.set_palette_cycle_speed +pxl8.clear_palette_cycles = gfx2d.clear_palette_cycles pxl8.key_down = input.key_down pxl8.key_pressed = input.key_pressed diff --git a/src/lua/pxl8/gfx2d.lua b/src/lua/pxl8/gfx2d.lua index 0c1d65a..6b2951f 100644 --- a/src/lua/pxl8/gfx2d.lua +++ b/src/lua/pxl8/gfx2d.lua @@ -82,4 +82,24 @@ function graphics.fade_palette(start, count, amount, target_color) C.pxl8_gfx_fade_palette(core.gfx, start, count, amount, target_color) end +function graphics.cycle_palette(start, count, step) + C.pxl8_gfx_cycle_palette(core.gfx, start, count, step or 1) +end + +function graphics.add_palette_cycle(start_index, end_index, speed) + return C.pxl8_gfx_add_palette_cycle(core.gfx, start_index, end_index, speed or 1.0) +end + +function graphics.remove_palette_cycle(cycle_id) + C.pxl8_gfx_remove_palette_cycle(core.gfx, cycle_id) +end + +function graphics.set_palette_cycle_speed(cycle_id, speed) + C.pxl8_gfx_set_palette_cycle_speed(core.gfx, cycle_id, speed) +end + +function graphics.clear_palette_cycles() + C.pxl8_gfx_clear_palette_cycles(core.gfx) +end + return graphics diff --git a/src/pxl8.c b/src/pxl8.c index f075145..e5eafc1 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -73,10 +73,11 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { pxl8_game* game = sys->game; - game->color_mode = PXL8_COLOR_MODE_MEGA; - game->resolution = PXL8_RESOLUTION_640x360; + pxl8_pixel_mode pixel_mode = PXL8_PIXEL_INDEXED; + pxl8_resolution resolution = PXL8_RESOLUTION_640x360; const char* script_arg = NULL; + bool bundle_mode = false; bool pack_mode = false; const char* pack_input = NULL; const char* pack_output = NULL; @@ -84,6 +85,15 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { for (i32 i = 1; i < argc; i++) { if (strcmp(argv[i], "--repl") == 0) { game->repl_mode = true; + } else if (strcmp(argv[i], "--bundle") == 0) { + bundle_mode = true; + if (i + 2 < argc) { + pack_input = argv[++i]; + pack_output = argv[++i]; + } else { + pxl8_error("--bundle requires "); + return PXL8_ERROR_INVALID_ARGUMENT; + } } else if (strcmp(argv[i], "--pack") == 0) { pack_mode = true; if (i + 2 < argc) { @@ -98,6 +108,18 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { } } + if (bundle_mode) { + char exe_path[1024]; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len == -1) { + pxl8_error("Failed to resolve executable path"); + return PXL8_ERROR_SYSTEM_FAILURE; + } + exe_path[len] = '\0'; + pxl8_result result = pxl8_cart_bundle(pack_input, pack_output, exe_path); + return result; + } + if (pack_mode) { pxl8_result result = pxl8_cart_pack(pack_input, pack_output); return result; @@ -109,13 +131,13 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { pxl8_info("Starting up"); - sys->platform_data = sys->hal->create(game->color_mode, game->resolution, "pxl8", 1280, 720); + sys->platform_data = sys->hal->create(pixel_mode, resolution, "pxl8", 1280, 720); if (!sys->platform_data) { pxl8_error("Failed to create platform context"); return PXL8_ERROR_INITIALIZATION_FAILED; } - game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, game->color_mode, game->resolution); + game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, pixel_mode, resolution); if (!game->gfx) { pxl8_error("Failed to create graphics context"); return PXL8_ERROR_INITIALIZATION_FAILED; @@ -126,39 +148,58 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { return PXL8_ERROR_INITIALIZATION_FAILED; } - game->script = pxl8_script_create(); + game->script = pxl8_script_create(game->repl_mode); if (!game->script) { pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(game->script)); return PXL8_ERROR_INITIALIZATION_FAILED; } - const char* cart_path = script_arg ? script_arg : "demo"; + bool has_embedded = pxl8_cart_has_embedded(argv[0]); + const char* cart_path = script_arg; - struct stat st; - bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) || - (cart_path && strstr(cart_path, ".pxc")); - - if (is_cart) { - char* original_cwd = getcwd(NULL, 0); + if (has_embedded && !script_arg) { sys->cart = pxl8_cart_create(); if (!sys->cart) { pxl8_error("Failed to create cart"); return PXL8_ERROR_INITIALIZATION_FAILED; } - if (pxl8_cart_load(sys->cart, cart_path) == PXL8_OK) { - pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd); + if (pxl8_cart_load_embedded(sys->cart, argv[0]) == PXL8_OK) { pxl8_cart_mount(sys->cart); strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1); game->script_path[sizeof(game->script_path) - 1] = '\0'; - pxl8_info("Loaded cart: %s", pxl8_cart_get_name(sys->cart)); + pxl8_info("Running embedded cart"); } else { - pxl8_error("Failed to load cart: %s", cart_path); + pxl8_error("Failed to load embedded cart"); return PXL8_ERROR_INITIALIZATION_FAILED; } - free(original_cwd); - } else if (script_arg) { - strncpy(game->script_path, script_arg, sizeof(game->script_path) - 1); - game->script_path[sizeof(game->script_path) - 1] = '\0'; + } else if (cart_path || !has_embedded) { + if (!cart_path) cart_path = "demo"; + + struct stat st; + bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) || + (cart_path && strstr(cart_path, ".pxc")); + + if (is_cart) { + char* original_cwd = getcwd(NULL, 0); + sys->cart = pxl8_cart_create(); + if (!sys->cart) { + pxl8_error("Failed to create cart"); + return PXL8_ERROR_INITIALIZATION_FAILED; + } + if (pxl8_cart_load(sys->cart, cart_path) == PXL8_OK) { + pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd); + pxl8_cart_mount(sys->cart); + strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1); + game->script_path[sizeof(game->script_path) - 1] = '\0'; + } else { + pxl8_error("Failed to load cart: %s", cart_path); + return PXL8_ERROR_INITIALIZATION_FAILED; + } + free(original_cwd); + } else if (script_arg) { + strncpy(game->script_path, script_arg, sizeof(game->script_path) - 1); + game->script_path[sizeof(game->script_path) - 1] = '\0'; + } } pxl8_script_set_gfx(game->script, game->gfx); @@ -245,6 +286,8 @@ pxl8_result pxl8_update(pxl8* sys) { } } + pxl8_gfx_update(game->gfx, dt); + if (game->script_loaded) { pxl8_script_call_function_f32(game->script, "update", dt); } @@ -268,19 +311,18 @@ pxl8_result pxl8_frame(pxl8* sys) { } else { pxl8_clear(game->gfx, 32); - pxl8_size render_size = pxl8_get_resolution_dimensions(game->resolution); + i32 render_w = pxl8_gfx_get_width(game->gfx); + i32 render_h = pxl8_gfx_get_height(game->gfx); - for (i32 y = 0; y < render_size.h; y += 24) { - for (i32 x = 0; x < render_size.w; x += 32) { + for (i32 y = 0; y < render_h; y += 24) { + for (i32 x = 0; x < render_w; x += 32) { u32 color = ((x / 32) + (y / 24) + (i32)(game->time * 2)) % 8; pxl8_rect_fill(game->gfx, x, y, 31, 23, color); } } } - pxl8_size render_size = pxl8_get_resolution_dimensions(game->resolution); - - pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, render_size.w, render_size.h)); + pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, pxl8_gfx_get_width(game->gfx), pxl8_gfx_get_height(game->gfx))); pxl8_gfx_upload_framebuffer(game->gfx); pxl8_gfx_upload_atlas(game->gfx); pxl8_gfx_present(game->gfx); @@ -343,10 +385,6 @@ pxl8_input_state* pxl8_get_input(const pxl8* sys) { return (sys && sys->game) ? &sys->game->input : NULL; } -pxl8_resolution pxl8_get_resolution(const pxl8* sys) { - return (sys && sys->game) ? sys->game->resolution : PXL8_RESOLUTION_640x360; -} - void pxl8_center_cursor(pxl8* sys) { if (!sys || !sys->hal || !sys->hal->center_cursor) return; sys->hal->center_cursor(sys->platform_data); @@ -365,17 +403,6 @@ void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled) { } } -u32 pxl8_get_palette_size(pxl8_color_mode mode) { - switch (mode) { - case PXL8_COLOR_MODE_HICOLOR: return 0; - case PXL8_COLOR_MODE_FAMI: return 64; - case PXL8_COLOR_MODE_MEGA: return 512; - case PXL8_COLOR_MODE_GBA: - case PXL8_COLOR_MODE_SNES: return 32768; - default: return 256; - } -} - pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution) { switch (resolution) { case PXL8_RESOLUTION_240x160: return (pxl8_size){240, 160}; diff --git a/src/pxl8_ase.c b/src/pxl8_ase.c index 3c2c31e..cf277ec 100644 --- a/src/pxl8_ase.c +++ b/src/pxl8_ase.c @@ -8,6 +8,7 @@ #define MINIZ_NO_ARCHIVE_APIS #define MINIZ_NO_ARCHIVE_WRITING_APIS #define MINIZ_NO_DEFLATE_APIS +#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES #include diff --git a/src/pxl8_atlas.c b/src/pxl8_atlas.c index e561540..1da4c97 100644 --- a/src/pxl8_atlas.c +++ b/src/pxl8_atlas.c @@ -141,14 +141,14 @@ static void pxl8_skyline_compact(pxl8_skyline* skyline) { } } -pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode) { +pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode) { pxl8_atlas* atlas = (pxl8_atlas*)calloc(1, sizeof(pxl8_atlas)); if (!atlas) return NULL; atlas->height = height; atlas->width = width; - i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode); + i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode); atlas->pixels = (u8*)calloc(width * height, bytes_per_pixel); if (!atlas->pixels) { free(atlas); @@ -199,10 +199,10 @@ void pxl8_atlas_destroy(pxl8_atlas* atlas) { free(atlas); } -bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode) { +bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode) { if (!atlas || atlas->width >= 4096) return false; - i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode); + i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode); u32 new_size = atlas->width * 2; u32 old_width = atlas->width; @@ -278,14 +278,14 @@ u32 pxl8_atlas_add_texture( const u8* pixels, u32 w, u32 h, - pxl8_color_mode color_mode + pxl8_pixel_mode pixel_mode ) { if (!atlas || !pixels) return UINT32_MAX; pxl8_skyline_fit fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h); if (!fit.found) { - if (!pxl8_atlas_expand(atlas, color_mode)) { + if (!pxl8_atlas_expand(atlas, pixel_mode)) { return UINT32_MAX; } @@ -319,7 +319,7 @@ u32 pxl8_atlas_add_texture( entry->w = w; entry->h = h; - i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode); + i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode); for (u32 y = 0; y < h; y++) { for (u32 x = 0; x < w; x++) { u32 src_idx = y * w + x; diff --git a/src/pxl8_atlas.h b/src/pxl8_atlas.h index 3e841c0..7097901 100644 --- a/src/pxl8_atlas.h +++ b/src/pxl8_atlas.h @@ -14,7 +14,7 @@ typedef struct pxl8_atlas_entry { extern "C" { #endif -pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode); +pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode); void pxl8_atlas_destroy(pxl8_atlas* atlas); const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id); @@ -26,8 +26,8 @@ bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas); void pxl8_atlas_mark_clean(pxl8_atlas* atlas); -u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_color_mode color_mode); -bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode); +u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_pixel_mode pixel_mode); +bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode); #ifdef __cplusplus } diff --git a/src/pxl8_cart.c b/src/pxl8_cart.c index 7986285..96acf39 100644 --- a/src/pxl8_cart.c +++ b/src/pxl8_cart.c @@ -1,78 +1,58 @@ #include "pxl8_cart.h" +#include #include #include #include - -#include #include #include -#include - #include "pxl8_macros.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 { - void* archive_data; - size_t archive_size; + u8* data; + u32 data_size; + pxl8_cart_file* files; + u32 file_count; char* base_path; + char* name; bool is_folder; bool is_mounted; - char* name; }; static pxl8_cart* pxl8_current_cart = NULL; static char* pxl8_original_cwd = NULL; -static void pxl8_add_file_recursive(mz_zip_archive* zip, const char* dir_path, const char* prefix) { - 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 zip_path[1024]; - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); - snprintf(zip_path, sizeof(zip_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/", zip_path); - pxl8_add_file_recursive(zip, full_path, new_prefix); - } else { - pxl8_info("Adding: %s", zip_path); - if (!mz_zip_writer_add_file(zip, zip_path, full_path, NULL, 0, MZ_BEST_COMPRESSION)) { - pxl8_warn("Failed to add file: %s", zip_path); - } - } - } - } - - closedir(dir); -} - -static char* get_cart_name(const char* path) { - char* name = strdup(path); - size_t len = strlen(name); - - if (len > 4 && strcmp(name + len - 4, ".pxc") == 0) { - name[len - 4] = '\0'; - } - - char* last_slash = strrchr(name, '/'); - if (last_slash) { - char* result = strdup(last_slash + 1); - free(name); - return result; - } - - return name; -} - static bool is_directory(const char* path) { struct stat st; return stat(path, &st) == 0 && S_ISDIR(st.st_mode); @@ -83,9 +63,99 @@ static bool is_pxc_file(const char* path) { return len > 4 && strcmp(path + len - 4, ".pxc") == 0; } +static char* get_cart_name(const char* path) { + char* name = strdup(path); + size_t len = strlen(name); + if (len > 4 && strcmp(name + len - 4, ".pxc") == 0) { + name[len - 4] = '\0'; + } + char* last_slash = strrchr(name, '/'); + if (last_slash) { + char* result = strdup(last_slash + 1); + free(name); + return result; + } + return name; +} + +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)); - return cart; + return calloc(1, sizeof(pxl8_cart)); } pxl8_cart* pxl8_cart_current(void) { @@ -98,19 +168,9 @@ void pxl8_cart_destroy(pxl8_cart* cart) { free(cart); } -const char* pxl8_cart_get_base_path(const pxl8_cart* cart) { - return cart ? cart->base_path : NULL; -} - -const char* pxl8_cart_get_name(const pxl8_cart* cart) { - return cart ? cart->name : NULL; -} - pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) { if (!cart || !path) return PXL8_ERROR_NULL_POINTER; - pxl8_cart_unload(cart); - cart->name = get_cart_name(path); if (is_directory(path)) { @@ -138,137 +198,120 @@ pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) { if (is_pxc_file(path)) { FILE* file = fopen(path, "rb"); if (!file) { - pxl8_error("Failed to open cart archive: %s", path); + pxl8_error("Failed to open cart: %s", path); return PXL8_ERROR_FILE_NOT_FOUND; } fseek(file, 0, SEEK_END); - cart->archive_size = ftell(file); + u32 size = (u32)ftell(file); fseek(file, 0, SEEK_SET); - cart->archive_data = malloc(cart->archive_size); - if (!cart->archive_data) { - fclose(file); - return PXL8_ERROR_OUT_OF_MEMORY; - } - - if (fread(cart->archive_data, 1, cart->archive_size, file) != cart->archive_size) { - free(cart->archive_data); - cart->archive_data = NULL; + u8* data = malloc(size); + if (!data || fread(data, 1, size, file) != size) { + free(data); fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } - fclose(file); - char temp_dir[256]; - snprintf(temp_dir, sizeof(temp_dir), "/tmp/pxl8_%s_%d", cart->name, getpid()); + pxl8_result result = load_packed_cart(cart, data, size); + free(data); - if (mkdir(temp_dir, 0755) != 0) { - pxl8_error("Failed to create temp directory: %s", temp_dir); - return PXL8_ERROR_SYSTEM_FAILURE; + if (result == PXL8_OK) { + pxl8_info("Loaded cart: %s", cart->name); } - - mz_zip_archive zip = {0}; - if (!mz_zip_reader_init_mem(&zip, cart->archive_data, cart->archive_size, 0)) { - pxl8_error("Failed to open cart archive as zip"); - return PXL8_ERROR_INVALID_FORMAT; - } - - i32 num_files = mz_zip_reader_get_num_files(&zip); - for (i32 i = 0; i < num_files; i++) { - mz_zip_archive_file_stat file_stat; - if (!mz_zip_reader_file_stat(&zip, i, &file_stat)) continue; - - char extract_path[1024]; - snprintf(extract_path, sizeof(extract_path), "%s/%s", temp_dir, file_stat.m_filename); - - if (file_stat.m_is_directory) { - mkdir(extract_path, 0755); - } else { - char* last_slash = strrchr(extract_path, '/'); - if (last_slash) { - *last_slash = '\0'; - mkdir(extract_path, 0755); - *last_slash = '/'; - } - - if (!mz_zip_reader_extract_to_file(&zip, i, extract_path, 0)) { - pxl8_warn("Failed to extract: %s", file_stat.m_filename); - } - } - } - - mz_zip_reader_end(&zip); - - cart->base_path = strdup(temp_dir); - cart->is_folder = false; - - pxl8_info("Loaded cart archive: %s", cart->name); - return PXL8_OK; + 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); + + cart->name = get_cart_name(exe_path); + pxl8_result result = load_packed_cart(cart, data, trailer.cart_size); + free(data); + + if (result == PXL8_OK) { + pxl8_info("Loaded embedded cart: %s", cart->name); + } + return result; +} + void pxl8_cart_unload(pxl8_cart* cart) { if (!cart) return; - pxl8_cart_unmount(cart); - if (!cart->is_folder && cart->base_path) { - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", cart->base_path); - system(cmd); + 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; - char* cart_name = cart->name ? strdup(cart->name) : NULL; - - if (cart->base_path) { - free(cart->base_path); - cart->base_path = NULL; - } + free(cart->data); + cart->data = NULL; + cart->data_size = 0; if (cart->name) { + pxl8_info("Unloaded cart: %s", cart->name); free(cart->name); cart->name = NULL; } - if (cart->archive_data) { - free(cart->archive_data); - cart->archive_data = NULL; - } - - cart->archive_size = 0; + free(cart->base_path); + cart->base_path = NULL; cart->is_folder = false; - cart->is_mounted = false; - - if (cart_name) { - pxl8_info("Unloaded cart: %s", cart_name); - free(cart_name); - } } pxl8_result pxl8_cart_mount(pxl8_cart* cart) { - if (!cart || !cart->base_path) return PXL8_ERROR_NULL_POINTER; + if (!cart) return PXL8_ERROR_NULL_POINTER; if (cart->is_mounted) return PXL8_OK; if (pxl8_current_cart) { pxl8_cart_unmount(pxl8_current_cart); } - 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; + 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; - pxl8_info("Mounted cart: %s", cart->name); return PXL8_OK; } @@ -286,24 +329,97 @@ void pxl8_cart_unmount(pxl8_cart* cart) { if (pxl8_current_cart == cart) { pxl8_current_cart = NULL; } - pxl8_info("Unmounted cart: %s", cart->name); } -bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) { - if (!cart || !cart->base_path || !relative_path || !out_path || out_size == 0) return false; +const char* pxl8_cart_get_base_path(const pxl8_cart* cart) { + return cart ? cart->base_path : NULL; +} - i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path); - return written >= 0 && (size_t)written < out_size; +const char* pxl8_cart_get_name(const pxl8_cart* cart) { + return cart ? cart->name : NULL; +} + +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 || !cart->base_path || !path) return false; + if (!cart || !path) return false; - char full_path[512]; - if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_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 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) { @@ -324,22 +440,169 @@ pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) { } } - mz_zip_archive zip = {0}; - if (!mz_zip_writer_init_file(&zip, output_path, 0)) { - pxl8_error("Failed to create archive: %s", output_path); - return PXL8_ERROR_SYSTEM_FAILURE; + 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); - pxl8_add_file_recursive(&zip, folder_path, ""); - if (!mz_zip_writer_finalize_archive(&zip)) { - pxl8_error("Failed to finalize archive"); - mz_zip_writer_end(&zip); + 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; } - mz_zip_writer_end(&zip); - pxl8_info("Cart packed successfully!"); + 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; } diff --git a/src/pxl8_cart.h b/src/pxl8_cart.h index 2fae5e4..c9b5f0b 100644 --- a/src/pxl8_cart.h +++ b/src/pxl8_cart.h @@ -11,15 +11,25 @@ extern "C" { pxl8_cart* pxl8_cart_create(void); pxl8_cart* pxl8_cart_current(void); void pxl8_cart_destroy(pxl8_cart* cart); -bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path); + +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); + const char* pxl8_cart_get_base_path(const pxl8_cart* cart); const char* pxl8_cart_get_name(const pxl8_cart* cart); -pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path); -pxl8_result pxl8_cart_mount(pxl8_cart* cart); -pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path); +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); -void pxl8_cart_unload(pxl8_cart* cart); -void pxl8_cart_unmount(pxl8_cart* cart); +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 } diff --git a/src/pxl8_color.h b/src/pxl8_color.h index 128bfc4..c1fa455 100644 --- a/src/pxl8_color.h +++ b/src/pxl8_color.h @@ -2,8 +2,8 @@ #include "pxl8_types.h" -static inline i32 pxl8_bytes_per_pixel(pxl8_color_mode mode) { - return (mode == PXL8_COLOR_MODE_HICOLOR) ? 2 : 1; +static inline i32 pxl8_bytes_per_pixel(pxl8_pixel_mode mode) { + return (mode == PXL8_PIXEL_HICOLOR) ? 2 : 1; } static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) { diff --git a/src/pxl8_game.h b/src/pxl8_game.h index 28d5732..90bb8a5 100644 --- a/src/pxl8_game.h +++ b/src/pxl8_game.h @@ -5,9 +5,7 @@ #include "pxl8_types.h" typedef struct pxl8_game { - pxl8_color_mode color_mode; pxl8_gfx* gfx; - pxl8_resolution resolution; pxl8_script* script; i32 frame_count; diff --git a/src/pxl8_gfx.c b/src/pxl8_gfx.c index 06be282..32c42fe 100644 --- a/src/pxl8_gfx.c +++ b/src/pxl8_gfx.c @@ -14,6 +14,19 @@ #include "pxl8_sys.h" #include "pxl8_types.h" +typedef struct pxl8_palette_cycle { + bool active; + u8 end_index; + f32 speed; + u8 start_index; + f32 timer; +} pxl8_palette_cycle; + +typedef struct pxl8_effects { + pxl8_palette_cycle palette_cycles[8]; + f32 time; +} pxl8_effects; + typedef struct pxl8_sprite_cache_entry { char path[256]; u32 sprite_id; @@ -29,7 +42,7 @@ struct pxl8_gfx { u32 sprite_cache_capacity; u32 sprite_cache_count; - pxl8_color_mode color_mode; + pxl8_pixel_mode pixel_mode; u8* framebuffer; i32 framebuffer_height; i32 framebuffer_width; @@ -40,6 +53,8 @@ struct pxl8_gfx { pxl8_viewport viewport; + pxl8_effects effects; + bool backface_culling; pxl8_mat4 model; pxl8_mat4 projection; @@ -66,17 +81,17 @@ pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) { return bounds; } -pxl8_color_mode pxl8_gfx_get_color_mode(pxl8_gfx* gfx) { - return gfx ? gfx->color_mode : PXL8_COLOR_MODE_FAMI; +pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx) { + return gfx ? gfx->pixel_mode : PXL8_PIXEL_INDEXED; } u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx) { - if (!gfx || gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) return NULL; + if (!gfx || gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return NULL; return gfx->framebuffer; } u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx) { - if (!gfx || gfx->color_mode != PXL8_COLOR_MODE_HICOLOR) return NULL; + if (!gfx || gfx->pixel_mode != PXL8_PIXEL_HICOLOR) return NULL; return (u16*)gfx->framebuffer; } @@ -95,7 +110,7 @@ u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx) { pxl8_gfx* pxl8_gfx_create( const pxl8_hal* hal, void* platform_data, - pxl8_color_mode mode, + pxl8_pixel_mode mode, pxl8_resolution resolution ) { pxl8_gfx* gfx = (pxl8_gfx*)calloc(1, sizeof(pxl8_gfx)); @@ -107,7 +122,7 @@ pxl8_gfx* pxl8_gfx_create( gfx->hal = hal; gfx->platform_data = platform_data; - gfx->color_mode = mode; + gfx->pixel_mode = mode; pxl8_size size = pxl8_get_resolution_dimensions(resolution); gfx->framebuffer_width = size.w; gfx->framebuffer_height = size.h; @@ -118,7 +133,7 @@ pxl8_gfx* pxl8_gfx_create( return NULL; } - i32 bytes_per_pixel = pxl8_bytes_per_pixel(gfx->color_mode); + i32 bytes_per_pixel = pxl8_bytes_per_pixel(gfx->pixel_mode); i32 fb_size = gfx->framebuffer_width * gfx->framebuffer_height * bytes_per_pixel; gfx->framebuffer = (u8*)calloc(1, fb_size); if (!gfx->framebuffer) { @@ -127,7 +142,7 @@ pxl8_gfx* pxl8_gfx_create( return NULL; } - gfx->palette_size = pxl8_get_palette_size(mode); + gfx->palette_size = (mode == PXL8_PIXEL_HICOLOR) ? 0 : PXL8_MAX_PALETTE_SIZE; if (gfx->palette_size > 0) { gfx->palette = (u32*)calloc(gfx->palette_size, sizeof(u32)); if (!gfx->palette) { @@ -136,8 +151,8 @@ pxl8_gfx* pxl8_gfx_create( return NULL; } - for (u32 i = 0; i < 256 && i < gfx->palette_size; i++) { - u8 gray = (u8)(i * 255 / 255); + for (u32 i = 0; i < gfx->palette_size; i++) { + u8 gray = (u8)i; gfx->palette[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray; } } @@ -177,7 +192,7 @@ void pxl8_gfx_destroy(pxl8_gfx* gfx) { static pxl8_result pxl8_gfx_ensure_atlas(pxl8_gfx* gfx) { if (gfx->atlas) return PXL8_OK; - gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->color_mode); + gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->pixel_mode); return gfx->atlas ? PXL8_OK : PXL8_ERROR_OUT_OF_MEMORY; } @@ -187,7 +202,7 @@ pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, pxl8_result result = pxl8_gfx_ensure_atlas(gfx); if (result != PXL8_OK) return result; - u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->color_mode); + u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->pixel_mode); if (texture_id == UINT32_MAX) { pxl8_error("Texture doesn't fit in atlas"); return PXL8_ERROR_INVALID_SIZE; @@ -234,7 +249,7 @@ pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) { ase_file.frames[0].pixels, ase_file.header.width, ase_file.header.height, - gfx->color_mode + gfx->pixel_mode ); pxl8_ase_destroy(&ase_file); @@ -273,7 +288,7 @@ pxl8_atlas* pxl8_gfx_get_atlas(pxl8_gfx* gfx) { pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path) { if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) return PXL8_OK; + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return PXL8_OK; pxl8_debug("Loading palette from: %s", path); @@ -316,7 +331,7 @@ void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized || !gfx->atlas) return; if (gfx->hal && gfx->hal->upload_atlas) { - gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->color_mode); + gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->pixel_mode); pxl8_atlas_mark_clean(gfx->atlas); } } @@ -330,7 +345,7 @@ void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) { gfx->framebuffer_width, gfx->framebuffer_height, gfx->palette, - gfx->color_mode + gfx->pixel_mode ); } @@ -364,7 +379,7 @@ void pxl8_clear(pxl8_gfx* gfx, u32 color) { i32 size = gfx->framebuffer_width * gfx->framebuffer_height; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { u16* fb16 = (u16*)gfx->framebuffer; u16 color16 = pxl8_rgba32_to_rgb565(color); for (i32 i = 0; i < size; i++) { @@ -380,7 +395,7 @@ void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return; i32 idx = y * gfx->framebuffer_width + x; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { ((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color); } else { gfx->framebuffer[idx] = color & 0xFF; @@ -392,7 +407,7 @@ u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) { if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0; i32 idx = y * gfx->framebuffer_width + x; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { return pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx]); } else { return gfx->framebuffer[idx]; @@ -436,7 +451,7 @@ void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { i32 idx = y * gfx->framebuffer_width + x; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { ((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color); } else { gfx->framebuffer[idx] = color & 0xFF; @@ -534,7 +549,7 @@ void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) { if (pixel_bit) { i32 fb_idx = py * gfx->framebuffer_width + px; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { ((u16*)gfx->framebuffer)[fb_idx] = pxl8_rgba32_to_rgb565(color); } else { gfx->framebuffer[fb_idx] = (u8)color; @@ -574,7 +589,7 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas); if (is_1to1_scale && is_unclipped) { - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { const u16* sprite_data = (const u16*)atlas_pixels + entry->y * atlas_width + entry->x; pxl8_blit_hicolor( (u16*)gfx->framebuffer, @@ -601,7 +616,7 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { i32 src_idx = src_y * atlas_width + src_x; i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px); - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { u16 pixel = ((const u16*)atlas_pixels)[src_idx]; if (pixel != 0) { ((u16*)gfx->framebuffer)[dest_idx] = pixel; @@ -709,7 +724,7 @@ void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 } } -void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) { +static void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) { if (!gfx || !effects) return; effects->time += dt; @@ -727,6 +742,45 @@ void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) { } } +void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt) { + if (!gfx) return; + pxl8_gfx_process_effects(gfx, &gfx->effects, dt); +} + +i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed) { + if (!gfx) return -1; + if (start_index >= end_index) return -1; + + for (i32 i = 0; i < 8; i++) { + if (!gfx->effects.palette_cycles[i].active) { + gfx->effects.palette_cycles[i].active = true; + gfx->effects.palette_cycles[i].start_index = start_index; + gfx->effects.palette_cycles[i].end_index = end_index; + gfx->effects.palette_cycles[i].speed = speed; + gfx->effects.palette_cycles[i].timer = 0.0f; + return i; + } + } + return -1; +} + +void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id) { + if (!gfx || cycle_id < 0 || cycle_id >= 8) return; + gfx->effects.palette_cycles[cycle_id].active = false; +} + +void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed) { + if (!gfx || cycle_id < 0 || cycle_id >= 8) return; + gfx->effects.palette_cycles[cycle_id].speed = speed; +} + +void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx) { + if (!gfx) return; + for (i32 i = 0; i < 8; i++) { + gfx->effects.palette_cycles[i].active = false; + } +} + static bool pxl8_3d_init_zbuffer(pxl8_gfx* gfx) { if (gfx->zbuffer) return true; @@ -863,7 +917,7 @@ static inline void pxl8_fill_scanline(pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32 i32 zbuf_offset = y * gfx->zbuffer_width; i32 fb_offset = y * gfx->framebuffer_width; - if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { u16* fb = (u16*)gfx->framebuffer; u16 color16 = pxl8_rgba32_to_rgb565(color); if (width == 0) { @@ -939,7 +993,7 @@ static inline void pxl8_fill_scanline_textured( i32 tex_mask = tex_w - 1; i32 atlas_x_base = entry->x; i32 atlas_y_base = entry->y; - bool is_hicolor = gfx->color_mode == PXL8_COLOR_MODE_HICOLOR; + bool is_hicolor = gfx->pixel_mode == PXL8_PIXEL_HICOLOR; bool affine = gfx->affine_textures; i32 span = xe - xs; diff --git a/src/pxl8_gfx.h b/src/pxl8_gfx.h index 1b8e9a2..3743acc 100644 --- a/src/pxl8_gfx.h +++ b/src/pxl8_gfx.h @@ -7,26 +7,6 @@ typedef struct pxl8_atlas pxl8_atlas; typedef struct pxl8_gfx pxl8_gfx; -typedef enum pxl8_blend_mode { - PXL8_BLEND_ADD, - PXL8_BLEND_ALPHA, - PXL8_BLEND_MULTIPLY, - PXL8_BLEND_NONE -} pxl8_blend_mode; - -typedef struct pxl8_palette_cycle { - bool active; - u8 end_index; - f32 speed; - u8 start_index; - f32 timer; -} pxl8_palette_cycle; - -typedef struct pxl8_scanline_effect { - bool active; - void (*process)(pxl8_gfx* gfx, i32 line, f32 time); -} pxl8_scanline_effect; - typedef struct pxl8_vertex { u32 color; pxl8_vec3 normal; @@ -34,12 +14,6 @@ typedef struct pxl8_vertex { f32 u, v; } pxl8_vertex; -typedef struct pxl8_effects { - pxl8_palette_cycle palette_cycles[8]; - pxl8_scanline_effect scanline_effects[4]; - f32 time; -} pxl8_effects; - typedef struct pxl8_triangle { pxl8_vertex v[3]; u32 texture_id; @@ -49,11 +23,11 @@ typedef struct pxl8_triangle { extern "C" { #endif -pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_color_mode mode, pxl8_resolution resolution); +pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution); void pxl8_gfx_destroy(pxl8_gfx* gfx); pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx); -pxl8_color_mode pxl8_gfx_get_color_mode(pxl8_gfx* gfx); +pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx); u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx); u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx); i32 pxl8_gfx_get_height(const pxl8_gfx* gfx); @@ -67,7 +41,6 @@ pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx); pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path); pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path); void pxl8_gfx_present(pxl8_gfx* gfx); -void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt); void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom); void pxl8_gfx_upload_atlas(pxl8_gfx* gfx); void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx); @@ -79,6 +52,12 @@ void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 ta void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t); void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors); +i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed); +void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id); +void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed); +void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx); +void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt); + void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color); void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color); void pxl8_clear(pxl8_gfx* gfx, u32 color); diff --git a/src/pxl8_hal.h b/src/pxl8_hal.h index 2b6363a..fa10532 100644 --- a/src/pxl8_hal.h +++ b/src/pxl8_hal.h @@ -6,7 +6,7 @@ typedef struct pxl8_atlas pxl8_atlas; typedef struct pxl8_game pxl8_game; typedef struct pxl8_hal { - void* (*create)(pxl8_color_mode mode, pxl8_resolution res, + void* (*create)(pxl8_pixel_mode mode, pxl8_resolution res, const char* title, i32 win_w, i32 win_h); void (*destroy)(void* platform_data); @@ -17,8 +17,8 @@ typedef struct pxl8_hal { void (*set_cursor)(void* platform_data, pxl8_cursor cursor); void (*set_relative_mouse_mode)(void* platform_data, bool enabled); void (*upload_atlas)(void* platform_data, const pxl8_atlas* atlas, - const u32* palette, pxl8_color_mode mode); + const u32* palette, pxl8_pixel_mode mode); void (*upload_framebuffer)(void* platform_data, const u8* fb, i32 w, i32 h, const u32* palette, - pxl8_color_mode mode); + pxl8_pixel_mode mode); } pxl8_hal; diff --git a/src/pxl8_io.c b/src/pxl8_io.c index b9b78c2..f112bb9 100644 --- a/src/pxl8_io.c +++ b/src/pxl8_io.c @@ -3,6 +3,7 @@ #include #include +#include "pxl8_cart.h" #include "pxl8_types.h" static inline char pxl8_to_lower(char c) { @@ -11,31 +12,48 @@ static inline char pxl8_to_lower(char c) { pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) { if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER; - + + pxl8_cart* cart = pxl8_cart_current(); + if (cart && pxl8_cart_is_packed(cart)) { + u8* data = NULL; + u32 cart_size = 0; + pxl8_result result = pxl8_cart_read_file(cart, path, &data, &cart_size); + if (result == PXL8_OK) { + *content = realloc(data, cart_size + 1); + if (!*content) { + pxl8_cart_free_file(data); + return PXL8_ERROR_OUT_OF_MEMORY; + } + (*content)[cart_size] = '\0'; + *size = cart_size; + return PXL8_OK; + } + } + FILE* file = fopen(path, "rb"); if (!file) { return PXL8_ERROR_FILE_NOT_FOUND; } - + fseek(file, 0, SEEK_END); long file_size = ftell(file); fseek(file, 0, SEEK_SET); - + if (file_size < 0) { fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } - + *content = malloc(file_size + 1); if (!*content) { fclose(file); return PXL8_ERROR_OUT_OF_MEMORY; } - + size_t bytes_read = fread(*content, 1, file_size, file); (*content)[bytes_read] = '\0'; *size = bytes_read; - + fclose(file); return PXL8_OK; } diff --git a/src/pxl8_save.c b/src/pxl8_save.c new file mode 100644 index 0000000..936427b --- /dev/null +++ b/src/pxl8_save.c @@ -0,0 +1,272 @@ +#include "pxl8_save.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#define PATH_SEP '\\' +#else +#include +#include +#define PATH_SEP '/' +#endif + +#include "pxl8_macros.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; +} diff --git a/src/pxl8_save.h b/src/pxl8_save.h new file mode 100644 index 0000000..f84af48 --- /dev/null +++ b/src/pxl8_save.h @@ -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 diff --git a/src/pxl8_script.c b/src/pxl8_script.c index 36bf563..410126d 100644 --- a/src/pxl8_script.c +++ b/src/pxl8_script.c @@ -12,6 +12,7 @@ #include #include +#include "pxl8_cart.h" #include "pxl8_embed.h" #include "pxl8_macros.h" #include "pxl8_gui.h" @@ -24,10 +25,91 @@ struct pxl8_script { char main_path[PXL8_MAX_PATH]; char watch_dir[PXL8_MAX_PATH]; time_t latest_mod_time; + bool repl_mode; }; #define PXL8_MAX_REPL_COMMAND_SIZE 4096 +static int pxl8_cart_loader(lua_State* L) { + const char* found_path = lua_tostring(L, lua_upvalueindex(1)); + const char* code = lua_tostring(L, lua_upvalueindex(2)); + size_t code_len = lua_objlen(L, lua_upvalueindex(2)); + bool is_fennel = lua_toboolean(L, lua_upvalueindex(3)); + + if (is_fennel) { + lua_getglobal(L, "fennel"); + lua_getfield(L, -1, "eval"); + lua_pushlstring(L, code, code_len); + lua_createtable(L, 0, 1); + lua_pushstring(L, found_path); + lua_setfield(L, -2, "filename"); + + if (lua_pcall(L, 2, 1, 0) != 0) { + lua_remove(L, -2); + return lua_error(L); + } + lua_remove(L, -2); + } else { + if (luaL_loadbuffer(L, code, code_len, found_path) != 0) { + return lua_error(L); + } + if (lua_pcall(L, 0, 1, 0) != 0) { + return lua_error(L); + } + } + + return 1; +} + +static int pxl8_cart_searcher(lua_State* L) { + const char* modname = luaL_checkstring(L, 1); + + pxl8_cart* cart = pxl8_cart_current(); + if (!cart || !pxl8_cart_is_packed(cart)) { + lua_pushstring(L, "\n\tno packed cart mounted"); + return 1; + } + + char path[512]; + size_t len = strlen(modname); + size_t j = 0; + for (size_t i = 0; i < len && j < sizeof(path) - 5; i++) { + if (modname[i] == '.') { + path[j++] = '/'; + } else { + path[j++] = modname[i]; + } + } + path[j] = '\0'; + + const char* extensions[] = {".fnl", ".lua", "/init.fnl", "/init.lua"}; + u8* data = NULL; + u32 size = 0; + char found_path[512]; + bool is_fennel = false; + + for (int i = 0; i < 4; i++) { + snprintf(found_path, sizeof(found_path), "%s%s", path, extensions[i]); + if (pxl8_cart_read_file(cart, found_path, &data, &size) == PXL8_OK) { + is_fennel = (i == 0 || i == 2); + break; + } + } + + if (!data) { + lua_pushfstring(L, "\n\tno file '%s' in cart", path); + return 1; + } + + lua_pushstring(L, found_path); + lua_pushlstring(L, (const char*)data, size); + lua_pushboolean(L, is_fennel); + pxl8_cart_free_file(data); + + lua_pushcclosure(L, pxl8_cart_loader, 3); + return 1; +} + static void pxl8_script_repl_promote_locals(const char* input, char* output, size_t output_size) { size_t i = 0; size_t j = 0; @@ -100,7 +182,13 @@ static const char* pxl8_ffi_cdefs = "void pxl8_sprite(pxl8_gfx* ctx, i32 id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);\n" "void pxl8_text(pxl8_gfx* ctx, const char* str, i32 x, i32 y, u32 color);\n" "void pxl8_gfx_color_ramp(pxl8_gfx* ctx, u8 start, u8 count, u32 from_color, u32 to_color);\n" +"void pxl8_gfx_cycle_palette(pxl8_gfx* ctx, u8 start, u8 count, i32 step);\n" "void pxl8_gfx_fade_palette(pxl8_gfx* ctx, u8 start, u8 count, f32 amount, u32 target_color);\n" +"i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* ctx, u8 start_index, u8 end_index, f32 speed);\n" +"void pxl8_gfx_remove_palette_cycle(pxl8_gfx* ctx, i32 cycle_id);\n" +"void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* ctx, i32 cycle_id, f32 speed);\n" +"void pxl8_gfx_clear_palette_cycles(pxl8_gfx* ctx);\n" +"void pxl8_gfx_update(pxl8_gfx* ctx, f32 dt);\n" "i32 pxl8_gfx_load_palette(pxl8_gfx* ctx, const char* filepath);\n" "i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath, u32* sprite_id);\n" "i32 pxl8_gfx_create_texture(pxl8_gfx* ctx, const u8* pixels, u32 width, u32 height);\n" @@ -323,7 +411,17 @@ static const char* pxl8_ffi_cdefs = "void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title);\n" "void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color);\n" "bool pxl8_gui_is_hovering(const pxl8_gui_state* state);\n" -"void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y);\n"; +"void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y);\n" +"\n" +"typedef struct pxl8_save pxl8_save;\n" +"pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version);\n" +"void pxl8_save_destroy(pxl8_save* save);\n" +"i32 pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size);\n" +"i32 pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out);\n" +"void pxl8_save_free(u8* data);\n" +"bool pxl8_save_exists(pxl8_save* save, u8 slot);\n" +"i32 pxl8_save_delete(pxl8_save* save, u8 slot);\n" +"const char* pxl8_save_get_directory(pxl8_save* save);\n"; void pxl8_lua_info(const char* msg) { pxl8_info("%s", msg); @@ -387,9 +485,10 @@ static void pxl8_install_embed_searcher(lua_State* L) { lua_pop(L, 2); } -pxl8_script* pxl8_script_create(void) { +pxl8_script* pxl8_script_create(bool repl_mode) { pxl8_script* script = (pxl8_script*)calloc(1, sizeof(pxl8_script)); if (!script) return NULL; + script->repl_mode = repl_mode; script->L = luaL_newstate(); if (!script->L) { @@ -427,10 +526,42 @@ pxl8_script* pxl8_script_create(void) { lua_getglobal(script->L, "fennel"); lua_getfield(script->L, -1, "install"); if (lua_isfunction(script->L, -1)) { - if (lua_pcall(script->L, 0, 0, 0) != 0) { + // Pass options table for fennel.install() + // lambdaAsFn: removes arity checking overhead from fn/lambda + // correlate: aligns Lua line numbers with Fennel source + // useMetadata: enables docstrings (only in REPL mode for perf) + lua_newtable(script->L); + lua_pushboolean(script->L, 1); + lua_setfield(script->L, -2, "lambdaAsFn"); + lua_pushboolean(script->L, 1); + lua_setfield(script->L, -2, "correlate"); + if (script->repl_mode) { + lua_pushboolean(script->L, 1); + lua_setfield(script->L, -2, "useMetadata"); + lua_pushboolean(script->L, 1); + lua_setfield(script->L, -2, "assertAsRepl"); + } + if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_warn("Failed to install fennel searcher: %s", lua_tostring(script->L, -1)); lua_pop(script->L, 1); } + + lua_getglobal(script->L, "package"); + lua_getfield(script->L, -1, "loaders"); + if (lua_isnil(script->L, -1)) { + lua_pop(script->L, 1); + lua_getfield(script->L, -1, "searchers"); + } + if (lua_istable(script->L, -1)) { + int n = (int)lua_objlen(script->L, -1); + for (int i = n; i >= 2; i--) { + lua_rawgeti(script->L, -1, i); + lua_rawseti(script->L, -2, i + 1); + } + lua_pushcfunction(script->L, pxl8_cart_searcher); + lua_rawseti(script->L, -2, 2); + } + lua_pop(script->L, 2); } else { lua_pop(script->L, 1); } @@ -574,16 +705,45 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam return PXL8_ERROR_SCRIPT_ERROR; } - lua_getfield(script->L, -1, "dofile"); - lua_pushstring(script->L, basename); - + pxl8_cart* cart = pxl8_cart_current(); pxl8_result result = PXL8_OK; - if (lua_pcall(script->L, 1, 0, 0) != 0) { - pxl8_script_set_error(script, lua_tostring(script->L, -1)); - lua_pop(script->L, 1); - result = PXL8_ERROR_SCRIPT_ERROR; + + if (cart && pxl8_cart_is_packed(cart)) { + u8* data = NULL; + u32 size = 0; + if (pxl8_cart_read_file(cart, basename, &data, &size) != PXL8_OK) { + pxl8_script_set_error(script, "Failed to read script from cart"); + lua_pop(script->L, 1); + return PXL8_ERROR_FILE_NOT_FOUND; + } + + lua_getfield(script->L, -1, "eval"); + lua_pushlstring(script->L, (const char*)data, size); + + lua_createtable(script->L, 0, 1); + lua_pushstring(script->L, basename); + lua_setfield(script->L, -2, "filename"); + + pxl8_cart_free_file(data); + + if (lua_pcall(script->L, 2, 0, 0) != 0) { + pxl8_script_set_error(script, lua_tostring(script->L, -1)); + lua_pop(script->L, 1); + result = PXL8_ERROR_SCRIPT_ERROR; + } else { + script->last_error[0] = '\0'; + } } else { - script->last_error[0] = '\0'; + lua_getfield(script->L, -1, "dofile"); + lua_pushstring(script->L, basename); + + if (lua_pcall(script->L, 1, 0, 0) != 0) { + pxl8_script_set_error(script, lua_tostring(script->L, -1)); + lua_pop(script->L, 1); + result = PXL8_ERROR_SCRIPT_ERROR; + } else { + script->last_error[0] = '\0'; + } } lua_pop(script->L, 1); diff --git a/src/pxl8_script.h b/src/pxl8_script.h index d5cfb12..e492618 100644 --- a/src/pxl8_script.h +++ b/src/pxl8_script.h @@ -11,7 +11,7 @@ typedef struct pxl8_script_repl_command pxl8_script_repl_command; extern "C" { #endif -pxl8_script* pxl8_script_create(void); +pxl8_script* pxl8_script_create(bool repl_mode); void pxl8_script_destroy(pxl8_script* script); const char* pxl8_script_get_last_error(pxl8_script* script); diff --git a/src/pxl8_sdl3.c b/src/pxl8_sdl3.c index 1f8cf2c..fff4e95 100644 --- a/src/pxl8_sdl3.c +++ b/src/pxl8_sdl3.c @@ -23,7 +23,7 @@ typedef struct pxl8_sdl3_context { u32 atlas_height; } pxl8_sdl3_context; -static void* sdl3_create(pxl8_color_mode mode, pxl8_resolution resolution, +static void* sdl3_create(pxl8_pixel_mode mode, pxl8_resolution resolution, const char* title, i32 win_w, i32 win_h) { (void)mode; @@ -120,7 +120,7 @@ static void sdl3_present(void* platform_data) { static void sdl3_upload_framebuffer(void* platform_data, const u8* fb, i32 w, i32 h, const u32* palette, - pxl8_color_mode mode) { + pxl8_pixel_mode mode) { if (!platform_data || !fb) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; @@ -133,16 +133,15 @@ static void sdl3_upload_framebuffer(void* platform_data, const u8* fb, ctx->rgba_buffer_size = needed_size; } - if (mode == PXL8_COLOR_MODE_HICOLOR) { + if (mode == PXL8_PIXEL_HICOLOR) { const u16* fb16 = (const u16*)fb; for (i32 i = 0; i < w * h; i++) { ctx->rgba_buffer[i] = pxl8_rgb565_to_rgba32(fb16[i]); } } else { - u32 palette_size = pxl8_get_palette_size(mode); for (i32 i = 0; i < w * h; i++) { u8 index = fb[i]; - ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0xFF000000; + ctx->rgba_buffer[i] = palette[index]; } } @@ -150,7 +149,7 @@ static void sdl3_upload_framebuffer(void* platform_data, const u8* fb, } static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas, - const u32* palette, pxl8_color_mode mode) { + const u32* palette, pxl8_pixel_mode mode) { if (!platform_data || !atlas) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; @@ -195,17 +194,16 @@ static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas, ctx->rgba_buffer_size = needed_size; } - if (mode == PXL8_COLOR_MODE_HICOLOR) { + if (mode == PXL8_PIXEL_HICOLOR) { const u16* atlas16 = (const u16*)atlas_pixels; for (u32 i = 0; i < atlas_w * atlas_h; i++) { u16 pixel = atlas16[i]; ctx->rgba_buffer[i] = (pixel != 0) ? pxl8_rgb565_to_rgba32(pixel) : 0x00000000; } } else { - u32 palette_size = pxl8_get_palette_size(mode); for (u32 i = 0; i < atlas_w * atlas_h; i++) { u8 index = atlas_pixels[i]; - ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0x00000000; + ctx->rgba_buffer[i] = palette[index]; } } @@ -331,11 +329,11 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { i32 window_width, window_height; SDL_GetWindowSize(window, &window_width, &window_height); - pxl8_resolution resolution = pxl8_get_resolution(sys); - pxl8_size render_size = pxl8_get_resolution_dimensions(resolution); + i32 render_w = pxl8_gfx_get_width(gfx); + i32 render_h = pxl8_gfx_get_height(gfx); pxl8_bounds window_bounds = {0, 0, window_width, window_height}; - pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_size.w, render_size.h); + pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_w, render_h); input->mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale); input->mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale); diff --git a/src/pxl8_sys.h b/src/pxl8_sys.h index 66fc0c8..c534389 100644 --- a/src/pxl8_sys.h +++ b/src/pxl8_sys.h @@ -17,8 +17,6 @@ void pxl8_destroy(pxl8* sys); f32 pxl8_get_fps(const pxl8* sys); pxl8_gfx* pxl8_get_gfx(const pxl8* sys); pxl8_input_state* pxl8_get_input(const pxl8* sys); -u32 pxl8_get_palette_size(pxl8_color_mode mode); -pxl8_resolution pxl8_get_resolution(const pxl8* sys); pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution); bool pxl8_is_running(const pxl8* sys); diff --git a/src/pxl8_tilemap.c b/src/pxl8_tilemap.c index ac56a91..de12d6e 100644 --- a/src/pxl8_tilemap.c +++ b/src/pxl8_tilemap.c @@ -15,7 +15,7 @@ struct pxl8_tilesheet { u32 tiles_per_row; u32 total_tiles; u32 width; - pxl8_color_mode color_mode; + pxl8_pixel_mode pixel_mode; u32 ref_count; pxl8_tile_animation* animations; u32 animation_count; @@ -545,7 +545,7 @@ pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u tilemap->tilesheet->height = tilesheet_height; tilemap->tilesheet->tiles_per_row = tiles_per_row; tilemap->tilesheet->total_tiles = tileset->tile_count; - tilemap->tilesheet->color_mode = PXL8_COLOR_MODE_MEGA; + tilemap->tilesheet->pixel_mode = PXL8_PIXEL_INDEXED; if (tilemap->tilesheet->tile_valid) free(tilemap->tilesheet->tile_valid); tilemap->tilesheet->tile_valid = calloc(tileset->tile_count + 1, sizeof(bool)); diff --git a/src/pxl8_tilesheet.c b/src/pxl8_tilesheet.c index 83b813e..6b7225e 100644 --- a/src/pxl8_tilesheet.c +++ b/src/pxl8_tilesheet.c @@ -18,7 +18,7 @@ struct pxl8_tilesheet { u32 total_tiles; u32 width; - pxl8_color_mode color_mode; + pxl8_pixel_mode pixel_mode; u32 ref_count; pxl8_tile_animation* animations; @@ -104,13 +104,13 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, tilesheet->height = height; tilesheet->tiles_per_row = width / tilesheet->tile_size; tilesheet->total_tiles = (width / tilesheet->tile_size) * (height / tilesheet->tile_size); - tilesheet->color_mode = pxl8_gfx_get_color_mode(gfx); + tilesheet->pixel_mode = pxl8_gfx_get_pixel_mode(gfx); u32 pixel_count = width * height; u16 ase_depth = ase_file.header.color_depth; - bool gfx_hicolor = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR); + bool gfx_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR); - size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->color_mode); + size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->pixel_mode); tilesheet->data = malloc(data_size); if (!tilesheet->data) { pxl8_ase_destroy(&ase_file); @@ -136,7 +136,7 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, } } else if (ase_depth == 8 && gfx_hicolor) { pxl8_warn("Indexed ASE with hicolor gfx - storing as indexed"); - tilesheet->color_mode = PXL8_COLOR_MODE_FAMI; + tilesheet->pixel_mode = PXL8_PIXEL_INDEXED; u8* new_data = realloc(tilesheet->data, pixel_count); if (!new_data) { free(tilesheet->data); @@ -164,7 +164,7 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, } u32 valid_tiles = 0; - bool is_hicolor = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR); + bool is_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR); for (u32 tile_id = 1; tile_id <= tilesheet->total_tiles; tile_id++) { u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size; @@ -310,7 +310,7 @@ pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_i u32 tile_x = (tile_id - 1) % tilesheet->tiles_per_row; u32 tile_y = (tile_id - 1) / tilesheet->tiles_per_row; - u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->color_mode); + u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->pixel_mode); for (u32 py = 0; py < tilesheet->tile_size; py++) { for (u32 px = 0; px < tilesheet->tile_size; px++) { diff --git a/src/pxl8_transition.c b/src/pxl8_transition.c index 8e7eff2..cc056cd 100644 --- a/src/pxl8_transition.c +++ b/src/pxl8_transition.c @@ -168,8 +168,8 @@ void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx) { i32 block_size = (i32)(max_block_size * progress); if (block_size < 1) block_size = 1; - pxl8_color_mode mode = pxl8_gfx_get_color_mode(gfx); - bool has_fb = (mode == PXL8_COLOR_MODE_HICOLOR) + pxl8_pixel_mode mode = pxl8_gfx_get_pixel_mode(gfx); + bool has_fb = (mode == PXL8_PIXEL_HICOLOR) ? (pxl8_gfx_get_framebuffer_hicolor(gfx) != NULL) : (pxl8_gfx_get_framebuffer_indexed(gfx) != NULL); if (!has_fb) break; diff --git a/src/pxl8_types.h b/src/pxl8_types.h index 3f8032c..3a90580 100644 --- a/src/pxl8_types.h +++ b/src/pxl8_types.h @@ -28,13 +28,10 @@ typedef __int128_t i128; typedef __uint128_t u128; #endif -typedef enum pxl8_color_mode { - PXL8_COLOR_MODE_FAMI, - PXL8_COLOR_MODE_GBA, - PXL8_COLOR_MODE_HICOLOR, - PXL8_COLOR_MODE_MEGA, - PXL8_COLOR_MODE_SNES -} pxl8_color_mode; +typedef enum pxl8_pixel_mode { + PXL8_PIXEL_INDEXED, + PXL8_PIXEL_HICOLOR +} pxl8_pixel_mode; typedef enum pxl8_cursor { PXL8_CURSOR_ARROW,