diff --git a/src/fnl/demo-effects.fnl b/demo/main.fnl similarity index 97% rename from src/fnl/demo-effects.fnl rename to demo/main.fnl index c13f092..51b77fe 100644 --- a/src/fnl/demo-effects.fnl +++ b/demo/main.fnl @@ -9,7 +9,7 @@ (var snow-init false) (global init (fn [] - (pxl8.load_palette "res/palettes/gruvbox.ase") + (pxl8.load_palette "palettes/gruvbox.ase") (set particles (pxl8.particles_new 1000)))) (global update (fn [dt] diff --git a/res/palettes/gruvbox.ase b/demo/palettes/gruvbox.ase similarity index 100% rename from res/palettes/gruvbox.ase rename to demo/palettes/gruvbox.ase diff --git a/res/palettes/sweaft-64.ase b/demo/palettes/sweaft-64.ase similarity index 100% rename from res/palettes/sweaft-64.ase rename to demo/palettes/sweaft-64.ase diff --git a/res/sprites/pxl8.ase b/demo/sprites/pxl8.ase similarity index 100% rename from res/sprites/pxl8.ase rename to demo/sprites/pxl8.ase diff --git a/res/sprites/pxl8.png b/demo/sprites/pxl8.png similarity index 100% rename from res/sprites/pxl8.png rename to demo/sprites/pxl8.png diff --git a/res/sprites/trees.ase b/demo/sprites/trees.ase similarity index 100% rename from res/sprites/trees.ase rename to demo/sprites/trees.ase diff --git a/res/tiles/tilesheet-dungeon.ase b/demo/tiles/tilesheet-dungeon.ase similarity index 100% rename from res/tiles/tilesheet-dungeon.ase rename to demo/tiles/tilesheet-dungeon.ase diff --git a/res/tiles/tilesheet-world.ase b/demo/tiles/tilesheet-world.ase similarity index 100% rename from res/tiles/tilesheet-world.ase rename to demo/tiles/tilesheet-world.ase diff --git a/pxl8.sh b/pxl8.sh index 00afe9e..90f2523 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -345,6 +345,7 @@ case "$COMMAND" in src/pxl8.c src/pxl8_ase.c src/pxl8_blit.c + src/pxl8_cart.c src/pxl8_font.c src/pxl8_gfx.c src/pxl8_io.c diff --git a/src/fnl/demo.fnl b/src/fnl/demo.fnl deleted file mode 100644 index 8e97781..0000000 --- a/src/fnl/demo.fnl +++ /dev/null @@ -1,24 +0,0 @@ -(local pxl8 (require :pxl8)) - -(var pxl8-sprite-id nil) - -(global init (fn [] - (pxl8.load_palette "./res/palettes/gruvbox.ase") - (set pxl8-sprite-id (pxl8.load_sprite "./res/sprites/pxl8.ase")) - (when (not pxl8-sprite-id) - (pxl8.error "Failed to load pxl8 sprite")))) - -(global update (fn [_dt])) - -(global draw (fn [] - (pxl8.clr 1) - (local cols 5) - (local sprite-w 128) - (local sprite-h 64) - - (for [i 0 8192] - (local col (% i cols)) - (local row (math.floor (/ i cols))) - (local x (* col (+ sprite-w 4))) - (local y (* row (+ sprite-h 2))) - (pxl8.sprite pxl8-sprite-id x y sprite-w sprite-h)))) diff --git a/src/fnl/prydain.fnl b/src/fnl/prydain.fnl deleted file mode 100644 index 2d27204..0000000 --- a/src/fnl/prydain.fnl +++ /dev/null @@ -1,226 +0,0 @@ -(local pxl8 (require :pxl8)) - -(local TILE_SIZE 16) -(local SCREEN_WIDTH 320) -(local SCREEN_HEIGHT 240) - -(var game-state { - :tilesheet nil - :tilemap nil - :camera {:x 0 :y 0} - :player {:x 5 :y 5 - :screen-x 80 :screen-y 80 - :facing :down - :moving false - :move-timer 0} -}) - -(local tile-types { - :empty 0 - :floor 1 - :wall-top 2 - :wall-front 3 - :wall-left 4 - :wall-right 5 - :wall-corner-tl 6 - :wall-corner-tr 7 - :wall-corner-bl 8 - :wall-corner-br 9 - :door 10 - :chest 11 - :stairs-up 12 - :stairs-down 13 -}) - -(fn generate-room [map x y width height] - (for [ry y (+ y height -1)] - (for [rx x (+ x width -1)] - (when (and (< rx 40) (< ry 30)) - (let [is-wall (or (= rx x) - (= rx (+ x width -1)) - (= ry y) - (= ry (+ y height -1)))] - (if is-wall - (tset map ry rx :wall) - (tset map ry rx :floor))))))) - -(fn generate-dungeon [] - (local map {}) - - (for [y 0 29] - (tset map y {}) - (for [x 0 39] - (tset map y x :empty))) - - (local rooms []) - (local room-count (+ 5 (math.random 5))) - - (for [i 1 room-count] - (local room-w (+ 4 (math.random 6))) - (local room-h (+ 4 (math.random 6))) - (local room-x (math.random 0 (- 39 room-w))) - (local room-y (math.random 0 (- 29 room-h))) - - (generate-room map room-x room-y room-w room-h) - (table.insert rooms {:x (+ room-x (math.floor (/ room-w 2))) - :y (+ room-y (math.floor (/ room-h 2))) - :w room-w - :h room-h})) - - (for [i 1 (- (length rooms) 1)] - (let [room1 (. rooms i) - room2 (. rooms (+ i 1)) - start-x room1.x - start-y room1.y - end-x room2.x - end-y room2.y] - - (var cx start-x) - (var cy start-y) - - (while (not= cx end-x) - (set cx (+ cx (if (< cx end-x) 1 -1))) - (when (= (. map cy cx) :empty) - (tset map cy cx :floor))) - - (while (not= cy end-y) - (set cy (+ cy (if (< cy end-y) 1 -1))) - (when (= (. map cy cx) :empty) - (tset map cy cx :floor))))) - - (let [first-room (. rooms 1)] - (set game-state.player.x first-room.x) - (set game-state.player.y first-room.y) - (set game-state.player.screen-x (* first-room.x TILE_SIZE)) - (set game-state.player.screen-y (* first-room.y TILE_SIZE))) - - map) - -(fn map-to-tilemap [map tilemap] - (for [y 0 29] - (for [x 0 39] - (let [tile (. map y x) - above (and (> y 0) (. map (- y 1) x)) - below (and (< y 29) (. map (+ y 1) x)) - left (and (> x 0) (. map y (- x 1))) - right (and (< x 39) (. map y (+ x 1)))] - - (if (= tile :floor) - (pxl8.tilemap_set_tile tilemap 0 x y tile-types.floor 0) - - (= tile :wall) - (let [is-top-wall (and below (= below :floor)) - is-corner-tl (and (= above :empty) (= left :empty)) - is-corner-tr (and (= above :empty) (= right :empty)) - is-corner-bl (and (= below :floor) (= left :empty)) - is-corner-br (and (= below :floor) (= right :empty))] - - (if is-corner-tl (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-tl pxl8.TILE_SOLID) - is-corner-tr (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-tr pxl8.TILE_SOLID) - is-corner-bl (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-bl pxl8.TILE_SOLID) - is-corner-br (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-corner-br pxl8.TILE_SOLID) - is-top-wall (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-top pxl8.TILE_SOLID) - (pxl8.tilemap_set_tile tilemap 0 x y tile-types.wall-front pxl8.TILE_SOLID))) - - :else - (pxl8.tilemap_set_tile tilemap 0 x y 0 0)))))) - -(fn move-player [dx dy] - (let [p game-state.player - new-x (+ p.x dx) - new-y (+ p.y dy)] - - (when (and (>= new-x 0) (< new-x 40) - (>= new-y 0) (< new-y 30) - (not (pxl8.tilemap_is_solid game-state.tilemap - (* new-x TILE_SIZE) - (* new-y TILE_SIZE)))) - (set p.x new-x) - (set p.y new-y) - (set p.screen-x (* new-x TILE_SIZE)) - (set p.screen-y (* new-y TILE_SIZE)) - (set p.moving true) - (set p.move-timer 0.2)))) - -(global init (fn [] - (pxl8.info "Prydain RPG initializing...") - - (pxl8.load_palette "./res/palettes/gruvbox.ase") - - (set game-state.tilesheet (pxl8.tilesheet_new TILE_SIZE)) - (set game-state.tilemap (pxl8.tilemap_new 40 30 TILE_SIZE)) - - (if (and game-state.tilesheet game-state.tilemap) - (do - (let [load-result (pxl8.tilesheet_load game-state.tilesheet "./res/tiles/tilesheet-dungeon.ase")] - (if (= load-result 0) - (pxl8.info "Loaded dungeon tilesheet") - (do - (pxl8.warn "Failed to load dungeon tilesheet, using placeholder tiles") - (pxl8.info (.. "Error code: " load-result))))) - - (pxl8.tilemap_set_tilesheet game-state.tilemap game-state.tilesheet) - (pxl8.info "Created tilemap 40x30 and tilesheet") - - (local dungeon-map (generate-dungeon)) - (map-to-tilemap dungeon-map game-state.tilemap) - - (pxl8.info "Random dungeon generated")) - (pxl8.error "Failed to create tilemap or tilesheet")))) - -(global update (fn [dt] - (let [p game-state.player] - - (when (> p.move-timer 0) - (set p.move-timer (- p.move-timer dt)) - (when (<= p.move-timer 0) - (set p.moving false))) - - (when (not p.moving) - (when (pxl8.key_pressed "w") - (set p.facing :up) - (move-player 0 -1)) - (when (pxl8.key_pressed "s") - (set p.facing :down) - (move-player 0 1)) - (when (pxl8.key_pressed "a") - (set p.facing :left) - (move-player -1 0)) - (when (pxl8.key_pressed "d") - (set p.facing :right) - (move-player 1 0)) - (when (pxl8.key_pressed "r") - (pxl8.info "Regenerating dungeon...") - (local new-map (generate-dungeon)) - (map-to-tilemap new-map game-state.tilemap))) - - (let [target-cam-x (- p.screen-x (/ SCREEN_WIDTH 2)) - target-cam-y (- p.screen-y (/ SCREEN_HEIGHT 2))] - (set game-state.camera.x (math.max 0 (math.min target-cam-x (* (- 40 20) TILE_SIZE)))) - (set game-state.camera.y (math.max 0 (math.min target-cam-y (* (- 30 15) TILE_SIZE))))) - - (when game-state.tilemap - (pxl8.tilemap_set_camera game-state.tilemap - game-state.camera.x - game-state.camera.y))))) - -(global draw (fn [] - (pxl8.clr 0) - - (when game-state.tilemap - (pxl8.tilemap_render game-state.tilemap)) - - (let [p game-state.player - px (- p.screen-x game-state.camera.x) - py (- p.screen-y game-state.camera.y)] - - (pxl8.rect_fill px py TILE_SIZE TILE_SIZE 14) - - (case p.facing - :up (pxl8.rect_fill (+ px 6) py 4 4 15) - :down (pxl8.rect_fill (+ px 6) (+ py 12) 4 4 15) - :left (pxl8.rect_fill px (+ py 6) 4 4 15) - :right (pxl8.rect_fill (+ px 12) (+ py 6) 4 4 15))) - - (pxl8.text "WASD: Move | R: New Dungeon" 5 5 15) - (pxl8.text (.. "Pos: " game-state.player.x "," game-state.player.y) 5 15 15))) diff --git a/src/pxl8.c b/src/pxl8.c index d4c6855..83d76f2 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -12,6 +12,7 @@ #include #include +#include "pxl8_cart.h" #include "pxl8_lua.h" #include "pxl8_macros.h" #include "pxl8_types.h" @@ -37,12 +38,13 @@ typedef struct pxl8_app_state { lua_State* lua; pxl8_repl_state repl; pxl8_resolution resolution; + pxl8_cart* cart; f32 fps_timer; i32 frame_count; u64 last_time; f32 time; - + bool repl_mode; bool running; bool script_loaded; @@ -240,21 +242,32 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { app.color_mode = PXL8_COLOR_MODE_MEGA; app.resolution = PXL8_RESOLUTION_640x360; - + const char* script_arg = NULL; + bool pack_mode = false; + const char* pack_input = NULL; + const char* pack_output = NULL; + for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--repl") == 0) { app.repl_mode = true; + } else if (strcmp(argv[i], "--pack") == 0) { + pack_mode = true; + if (i + 2 < argc) { + pack_input = argv[++i]; + pack_output = argv[++i]; + } else { + pxl8_error("--pack requires "); + return SDL_APP_FAILURE; + } } else if (!script_arg) { script_arg = argv[i]; } } - - if (script_arg) { - strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1); - app.script_path[sizeof(app.script_path) - 1] = '\0'; - } else { - strcpy(app.script_path, "src/fnl/demo.fnl"); + + if (pack_mode) { + pxl8_result result = pxl8_cart_pack(pack_input, pack_output); + return (result == PXL8_OK) ? SDL_APP_SUCCESS : SDL_APP_FAILURE; } @@ -285,6 +298,32 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { pxl8_gfx_shutdown(&app.gfx); return SDL_APP_FAILURE; } + + if (script_arg) { + struct stat st; + bool is_cart = (stat(script_arg, &st) == 0 && S_ISDIR(st.st_mode)) || + strstr(script_arg, ".pxc"); + + if (is_cart) { + char* original_cwd = getcwd(NULL, 0); + app.cart = pxl8_cart_new(); + if (pxl8_cart_load(app.cart, script_arg) == PXL8_OK) { + pxl8_lua_setup_cart_path(app.lua, app.cart->base_path, original_cwd); + pxl8_cart_mount(app.cart); + strcpy(app.script_path, "main.fnl"); + pxl8_info("Loaded cart: %s", app.cart->name); + } else { + pxl8_error("Failed to load cart: %s", script_arg); + return SDL_APP_FAILURE; + } + free(original_cwd); + } else { + strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1); + app.script_path[sizeof(app.script_path) - 1] = '\0'; + } + } else { + strcpy(app.script_path, "src/fnl/demo.fnl"); + } pxl8_lua_setup_contexts(app.lua, &app.gfx, &app.input); @@ -471,6 +510,10 @@ void SDL_AppQuit(void* appstate, SDL_AppResult result) { if (app->repl_mode) { pxl8_repl_shutdown(&app->repl); } + if (app->cart) { + pxl8_cart_destroy(app->cart); + app->cart = NULL; + } pxl8_lua_shutdown(app->lua); pxl8_gfx_shutdown(&app->gfx); } diff --git a/src/pxl8_ase.c b/src/pxl8_ase.c index 82b21bf..186230f 100644 --- a/src/pxl8_ase.c +++ b/src/pxl8_ase.c @@ -397,4 +397,4 @@ void pxl8_ase_free(pxl8_ase_file* ase_file) { } memset(ase_file, 0, sizeof(pxl8_ase_file)); -} \ No newline at end of file +} diff --git a/src/pxl8_cart.c b/src/pxl8_cart.c new file mode 100644 index 0000000..9501e2f --- /dev/null +++ b/src/pxl8_cart.c @@ -0,0 +1,326 @@ +#include "pxl8_cart.h" +#include "pxl8_macros.h" +#include +#include +#include +#include +#include +#include +#include "../lib/miniz/miniz.h" + +static pxl8_cart* s_current_cart = NULL; +static char* s_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 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; +} + +pxl8_cart* pxl8_cart_new(void) { + pxl8_cart* cart = calloc(1, sizeof(pxl8_cart)); + if (!cart) { + pxl8_error("Failed to allocate cart"); + return NULL; + } + return 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); + + cart->name = get_cart_name(path); + + 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; + + char main_path[512]; + snprintf(main_path, sizeof(main_path), "%s/main.fnl", cart->base_path); + if (access(main_path, F_OK) != 0) { + snprintf(main_path, sizeof(main_path), "%s/main.lua", cart->base_path); + if (access(main_path, F_OK) != 0) { + pxl8_error("No main.fnl or main.lua found in cart: %s", path); + return PXL8_ERROR_FILE_NOT_FOUND; + } + } + + pxl8_info("Loaded cart folder: %s", cart->name); + return PXL8_OK; + } + + if (is_pxc_file(path)) { + FILE* file = fopen(path, "rb"); + if (!file) { + pxl8_error("Failed to open cart archive: %s", path); + return PXL8_ERROR_FILE_NOT_FOUND; + } + + fseek(file, 0, SEEK_END); + cart->archive_size = 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; + 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()); + + if (mkdir(temp_dir, 0755) != 0) { + pxl8_error("Failed to create temp directory: %s", temp_dir); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + 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; + } + + int num_files = mz_zip_reader_get_num_files(&zip); + for (int 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[512]; + 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; + } + + pxl8_error("Unknown cart format: %s", path); + return PXL8_ERROR_INVALID_FORMAT; +} + +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->base_path) { + free(cart->base_path); + cart->base_path = NULL; + } + + if (cart->name) { + free(cart->name); + cart->name = NULL; + } + + if (cart->archive_data) { + free(cart->archive_data); + cart->archive_data = NULL; + } + + cart->archive_size = 0; + cart->is_folder = false; + cart->is_mounted = false; +} + +pxl8_result pxl8_cart_mount(pxl8_cart* cart) { + if (!cart || !cart->base_path) return PXL8_ERROR_NULL_POINTER; + if (cart->is_mounted) return PXL8_OK; + + if (s_current_cart) { + pxl8_cart_unmount(s_current_cart); + } + + s_original_cwd = getcwd(NULL, 0); + if (chdir(cart->base_path) != 0) { + pxl8_error("Failed to change to cart directory: %s", cart->base_path); + free(s_original_cwd); + s_original_cwd = NULL; + return PXL8_ERROR_FILE_NOT_FOUND; + } + + cart->is_mounted = true; + s_current_cart = cart; + + pxl8_info("Mounted cart: %s", cart->name); + return PXL8_OK; +} + +void pxl8_cart_unmount(pxl8_cart* cart) { + if (!cart || !cart->is_mounted) return; + + if (s_original_cwd) { + chdir(s_original_cwd); + free(s_original_cwd); + s_original_cwd = NULL; + } + + cart->is_mounted = false; + if (s_current_cart == cart) { + s_current_cart = NULL; + } + + pxl8_info("Unmounted cart: %s", cart->name); +} + +char* pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path) { + if (!cart || !cart->base_path || !relative_path) return NULL; + + char* full_path = malloc(512); + if (!full_path) return NULL; + + snprintf(full_path, 512, "%s/%s", cart->base_path, relative_path); + return full_path; +} + +bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) { + if (!cart || !cart->base_path || !path) return false; + + char* full_path = pxl8_cart_resolve_path(cart, path); + if (!full_path) return false; + + bool exists = access(full_path, F_OK) == 0; + free(full_path); + return exists; +} + +static void pxl8_add_files_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[512]; + char zip_path[512]; + 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[512]; + snprintf(new_prefix, sizeof(new_prefix), "%s/", zip_path); + pxl8_add_files_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); +} + +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; + } + + char main_path[512]; + snprintf(main_path, sizeof(main_path), "%s/main.fnl", folder_path); + if (access(main_path, F_OK) != 0) { + snprintf(main_path, sizeof(main_path), "%s/main.lua", folder_path); + if (access(main_path, F_OK) != 0) { + pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path); + return PXL8_ERROR_FILE_NOT_FOUND; + } + } + + 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; + } + + pxl8_info("Packing cart: %s -> %s", folder_path, output_path); + pxl8_add_files_recursive(&zip, folder_path, ""); + + if (!mz_zip_writer_finalize_archive(&zip)) { + pxl8_error("Failed to finalize archive"); + mz_zip_writer_end(&zip); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + mz_zip_writer_end(&zip); + pxl8_info("Cart packed successfully!"); + return PXL8_OK; +} + +pxl8_cart* pxl8_cart_current(void) { + return s_current_cart; +} diff --git a/src/pxl8_cart.h b/src/pxl8_cart.h new file mode 100644 index 0000000..25e2880 --- /dev/null +++ b/src/pxl8_cart.h @@ -0,0 +1,36 @@ +#pragma once + +#include "pxl8_types.h" + +typedef struct pxl8_cart { + char* base_path; + char* name; + void* archive_data; + size_t archive_size; + bool is_folder; + bool is_mounted; +} pxl8_cart; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path); +void pxl8_cart_unload(pxl8_cart* cart); + +pxl8_result pxl8_cart_mount(pxl8_cart* cart); +void pxl8_cart_unmount(pxl8_cart* cart); + +char* pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path); +bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path); + +pxl8_cart* pxl8_cart_new(void); +void pxl8_cart_destroy(pxl8_cart* cart); + +pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path); + +pxl8_cart* pxl8_cart_current(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_font.c b/src/pxl8_font.c index 3e21327..d3b7ab2 100644 --- a/src/pxl8_font.c +++ b/src/pxl8_font.c @@ -57,4 +57,4 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* } return PXL8_OK; -} \ No newline at end of file +} diff --git a/src/pxl8_font.h b/src/pxl8_font.h index 590f2f8..dec8429 100644 --- a/src/pxl8_font.h +++ b/src/pxl8_font.h @@ -127,4 +127,4 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/pxl8_lua.c b/src/pxl8_lua.c index 1c9a3fd..fe87043 100644 --- a/src/pxl8_lua.c +++ b/src/pxl8_lua.c @@ -1,6 +1,7 @@ #include "pxl8_lua.h" #include "pxl8_vfx.h" #include "pxl8_macros.h" +#include static const char* pxl8_ffi_cdefs = "typedef uint8_t u8;\n" @@ -250,24 +251,58 @@ pxl8_result pxl8_lua_run_string(lua_State* lua_state, const char* code) { return PXL8_OK; } +void pxl8_lua_setup_cart_path(lua_State* lua_state, const char* cart_path, const char* original_cwd) { + if (!lua_state || !cart_path || !original_cwd) return; + + lua_getglobal(lua_state, "package"); + lua_getfield(lua_state, -1, "path"); + const char* current_path = lua_tostring(lua_state, -1); + + char new_path[2048]; + snprintf(new_path, sizeof(new_path), + "%s/?.lua;%s/?/init.lua;%s/src/?.lua;%s/src/?/init.lua;%s/src/lua/?.lua;%s", + cart_path, cart_path, cart_path, cart_path, original_cwd, + current_path ? current_path : ""); + + lua_pushstring(lua_state, new_path); + lua_setfield(lua_state, -3, "path"); + lua_pop(lua_state, 2); + + lua_getglobal(lua_state, "fennel"); + if (!lua_isnil(lua_state, -1)) { + lua_getfield(lua_state, -1, "path"); + const char* fennel_path = lua_tostring(lua_state, -1); + + snprintf(new_path, sizeof(new_path), + "%s/?.fnl;%s/?/init.fnl;%s/src/?.fnl;%s/src/?/init.fnl;%s", + cart_path, cart_path, cart_path, cart_path, + fennel_path ? fennel_path : ""); + + lua_pushstring(lua_state, new_path); + lua_setfield(lua_state, -3, "path"); + lua_pop(lua_state, 1); + } + lua_pop(lua_state, 1); +} + pxl8_result pxl8_lua_run_fennel_file(lua_State* lua_state, const char* filename) { if (!lua_state || !filename) return PXL8_ERROR_NULL_POINTER; - + lua_getglobal(lua_state, "fennel"); if (lua_isnil(lua_state, -1)) { lua_pop(lua_state, 1); return PXL8_ERROR_SCRIPT_ERROR; } - + lua_getfield(lua_state, -1, "dofile"); lua_pushstring(lua_state, filename); - + if (lua_pcall(lua_state, 1, 0, 0) != 0) { printf("Fennel error: %s\n", lua_tostring(lua_state, -1)); lua_pop(lua_state, 1); return PXL8_ERROR_SCRIPT_ERROR; } - + lua_pop(lua_state, 1); return PXL8_OK; } diff --git a/src/pxl8_lua.h b/src/pxl8_lua.h index 01d3ce2..d7d872a 100644 --- a/src/pxl8_lua.h +++ b/src/pxl8_lua.h @@ -18,6 +18,7 @@ pxl8_result pxl8_lua_run_fennel_file(lua_State* lua_state, const char* filename) pxl8_result pxl8_lua_run_file(lua_State* lua_state, const char* filename); pxl8_result pxl8_lua_run_string(lua_State* lua_state, const char* code); pxl8_result pxl8_lua_setup_contexts(lua_State* lua_state, pxl8_gfx_ctx* gfx_ctx, pxl8_input_state* input); +void pxl8_lua_setup_cart_path(lua_State* lua_state, const char* cart_path, const char* original_cwd); #ifdef __cplusplus } diff --git a/src/pxl8_tilemap.c b/src/pxl8_tilemap.c index 2db24a3..f3ecf1e 100644 --- a/src/pxl8_tilemap.c +++ b/src/pxl8_tilemap.c @@ -4,6 +4,30 @@ #include #include +static inline u32 pxl8_chunk_index(u32 x, u32 y, u32 chunks_wide) { + return (y >> 4) * chunks_wide + (x >> 4); +} + +static pxl8_tile_chunk* pxl8_get_or_create_chunk(pxl8_tilemap_layer* layer, u32 x, u32 y) { + u32 chunk_x = x >> 4; + u32 chunk_y = y >> 4; + u32 idx = chunk_y * layer->chunks_wide + chunk_x; + + if (idx >= layer->chunks_wide * layer->chunks_high) return NULL; + + if (!layer->chunks[idx]) { + layer->chunks[idx] = calloc(1, sizeof(pxl8_tile_chunk)); + if (!layer->chunks[idx]) return NULL; + + layer->chunks[idx]->chunk_x = chunk_x; + layer->chunks[idx]->chunk_y = chunk_y; + layer->chunks[idx]->empty = true; + layer->allocated_chunks++; + } + + return layer->chunks[idx]; +} + pxl8_result pxl8_tilemap_init(pxl8_tilemap* tilemap, u32 width, u32 height, u32 tile_size) { if (!tilemap) return PXL8_ERROR_NULL_POINTER; if (width > PXL8_MAX_TILEMAP_WIDTH || height > PXL8_MAX_TILEMAP_HEIGHT) { @@ -16,17 +40,21 @@ pxl8_result pxl8_tilemap_init(pxl8_tilemap* tilemap, u32 width, u32 height, u32 tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE; tilemap->active_layers = 1; - for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { - tilemap->layers[i].width = width; - tilemap->layers[i].height = height; - tilemap->layers[i].visible = (i == 0); - tilemap->layers[i].opacity = 255; + u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE; + u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE; - size_t tiles_size = width * height * sizeof(pxl8_tile); - tilemap->layers[i].tiles = calloc(1, tiles_size); - if (!tilemap->layers[i].tiles) { + for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { + pxl8_tilemap_layer* layer = &tilemap->layers[i]; + layer->chunks_wide = chunks_wide; + layer->chunks_high = chunks_high; + layer->chunk_count = chunks_wide * chunks_high; + layer->visible = (i == 0); + layer->opacity = 255; + + layer->chunks = calloc(layer->chunk_count, sizeof(pxl8_tile_chunk*)); + if (!layer->chunks) { for (u32 j = 0; j < i; j++) { - free(tilemap->layers[j].tiles); + free(tilemap->layers[j].chunks); } return PXL8_ERROR_OUT_OF_MEMORY; } @@ -39,17 +67,33 @@ void pxl8_tilemap_free(pxl8_tilemap* tilemap) { if (!tilemap) return; for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { - if (tilemap->layers[i].tiles) { - free(tilemap->layers[i].tiles); - tilemap->layers[i].tiles = NULL; + pxl8_tilemap_layer* layer = &tilemap->layers[i]; + if (layer->chunks) { + for (u32 j = 0; j < layer->chunk_count; j++) { + if (layer->chunks[j]) { + free(layer->chunks[j]); + } + } + free(layer->chunks); + layer->chunks = NULL; } } + + if (tilemap->tilesheet) { + pxl8_tilesheet_unref(tilemap->tilesheet); + tilemap->tilesheet = NULL; + } } pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) { if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER; + if (tilemap->tilesheet) { + pxl8_tilesheet_unref(tilemap->tilesheet); + } + tilemap->tilesheet = tilesheet; + pxl8_tilesheet_ref(tilesheet); if (tilesheet->tile_size != tilemap->tile_size) { pxl8_warn("Tilesheet tile size (%d) differs from tilemap tile size (%d)", @@ -66,9 +110,18 @@ pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y if (x >= tilemap->width || y >= tilemap->height) return PXL8_ERROR_INVALID_COORDINATE; pxl8_tilemap_layer* l = &tilemap->layers[layer]; - u32 idx = y * tilemap->width + x; - l->tiles[idx].id = tile_id; - l->tiles[idx].flags = flags; + pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, x, y); + if (!chunk) return PXL8_ERROR_OUT_OF_MEMORY; + + u32 local_x = x & PXL8_CHUNK_MASK; + u32 local_y = y & PXL8_CHUNK_MASK; + u32 idx = local_y * PXL8_CHUNK_SIZE + local_x; + + chunk->tiles[idx] = pxl8_tile_pack(tile_id, flags, 0); + + if (tile_id != 0) { + chunk->empty = false; + } if (layer >= tilemap->active_layers) { tilemap->active_layers = layer + 1; @@ -79,12 +132,20 @@ pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y } pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) { - pxl8_tile empty_tile = {0, 0, 0}; + if (!tilemap || layer >= PXL8_MAX_TILE_LAYERS) return 0; + if (x >= tilemap->width || y >= tilemap->height) return 0; - if (!tilemap || layer >= PXL8_MAX_TILE_LAYERS) return empty_tile; - if (x >= tilemap->width || y >= tilemap->height) return empty_tile; + const pxl8_tilemap_layer* l = &tilemap->layers[layer]; + u32 chunk_idx = pxl8_chunk_index(x, y, l->chunks_wide); - return tilemap->layers[layer].tiles[y * tilemap->width + x]; + if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) return 0; + + const pxl8_tile_chunk* chunk = l->chunks[chunk_idx]; + u32 local_x = x & PXL8_CHUNK_MASK; + u32 local_y = y & PXL8_CHUNK_MASK; + u32 idx = local_y * PXL8_CHUNK_SIZE + local_x; + + return chunk->tiles[idx]; } void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y) { @@ -124,18 +185,49 @@ void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx_ctx* gfx, u const pxl8_tilemap_layer* l = &tilemap->layers[layer]; if (!l->visible) return; - pxl8_tilemap_view view; - pxl8_tilemap_get_view(tilemap, gfx, &view); + i32 view_left = tilemap->camera_x / tilemap->tile_size; + i32 view_top = tilemap->camera_y / tilemap->tile_size; + i32 view_right = (tilemap->camera_x + gfx->framebuffer_width) / tilemap->tile_size + 1; + i32 view_bottom = (tilemap->camera_y + gfx->framebuffer_height) / tilemap->tile_size + 1; - for (i32 ty = view.tile_start_y; ty < view.tile_end_y; ty++) { - for (i32 tx = view.tile_start_x; tx < view.tile_end_x; tx++) { - pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); - if (tile.id == 0) continue; + u32 chunk_left = pxl8_max(0, view_left >> 4); + u32 chunk_top = pxl8_max(0, view_top >> 4); + u32 chunk_right = pxl8_min((tilemap->width + 15) >> 4, (view_right >> 4) + 1); + u32 chunk_bottom = pxl8_min((tilemap->height + 15) >> 4, (view_bottom >> 4) + 1); - i32 screen_x = tx * tilemap->tile_size - tilemap->camera_x; - i32 screen_y = ty * tilemap->tile_size - tilemap->camera_y; + for (u32 cy = chunk_top; cy < chunk_bottom; cy++) { + for (u32 cx = chunk_left; cx < chunk_right; cx++) { + u32 chunk_idx = cy * l->chunks_wide + cx; + if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) continue; - pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile.id, screen_x, screen_y, tile.flags); + const pxl8_tile_chunk* chunk = l->chunks[chunk_idx]; + if (chunk->empty) continue; + + u32 tile_start_x = pxl8_max(0, view_left - (i32)(cx << 4)); + u32 tile_end_x = pxl8_min(PXL8_CHUNK_SIZE, view_right - (i32)(cx << 4)); + u32 tile_start_y = pxl8_max(0, view_top - (i32)(cy << 4)); + u32 tile_end_y = pxl8_min(PXL8_CHUNK_SIZE, view_bottom - (i32)(cy << 4)); + + for (u32 ty = tile_start_y; ty < tile_end_y; ty++) { + for (u32 tx = tile_start_x; tx < tile_end_x; tx++) { + u32 idx = ty * PXL8_CHUNK_SIZE + tx; + pxl8_tile tile = chunk->tiles[idx]; + u16 tile_id = pxl8_tile_get_id(tile); + if (tile_id == 0) continue; + + i32 world_x = (cx << 4) + tx; + i32 world_y = (cy << 4) + ty; + i32 screen_x = world_x * tilemap->tile_size - tilemap->camera_x; + i32 screen_y = world_y * tilemap->tile_size - tilemap->camera_y; + + u8 flags = pxl8_tile_get_flags(tile); + if (flags & PXL8_TILE_ANIMATED && tilemap->tilesheet) { + tile_id = pxl8_tilesheet_get_animated_frame(tilemap->tilesheet, tile_id); + } + + pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, screen_x, screen_y, flags); + } + } } } } @@ -156,7 +248,7 @@ bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y) { for (u32 layer = 0; layer < tilemap->active_layers; layer++) { pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tile_x, tile_y); - if (tile.flags & PXL8_TILE_SOLID) return true; + if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true; } return false; @@ -177,7 +269,7 @@ bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 for (u32 layer = 0; layer < tilemap->active_layers; layer++) { pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); - if (tile.flags & PXL8_TILE_SOLID) return true; + if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true; } } } @@ -211,5 +303,120 @@ void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) { u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) { if (!tilemap) return 0; pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y); - return tile.id; -} \ No newline at end of file + return pxl8_tile_get_id(tile); +} + +void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time) { + if (!tilemap || !tilemap->tilesheet) return; + pxl8_tilesheet_update_animations(tilemap->tilesheet, delta_time); +} + +static u8 pxl8_tilemap_get_neighbors(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 match_id) { + u8 neighbors = 0; + + if (y > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y - 1) == match_id) + neighbors |= 1 << 0; + if (x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y) == match_id) + neighbors |= 1 << 1; + if (y < tilemap->height - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y + 1) == match_id) + neighbors |= 1 << 2; + if (x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y) == match_id) + neighbors |= 1 << 3; + + if (y > 0 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y - 1) == match_id) + neighbors |= 1 << 4; + if (y < tilemap->height - 1 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y + 1) == match_id) + neighbors |= 1 << 5; + if (y < tilemap->height - 1 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y + 1) == match_id) + neighbors |= 1 << 6; + if (y > 0 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y - 1) == match_id) + neighbors |= 1 << 7; + + return neighbors; +} + +pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, + u16 base_tile_id, u8 flags) { + if (!tilemap || !tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER; + + pxl8_result result = pxl8_tilemap_set_tile(tilemap, layer, x, y, base_tile_id, flags | PXL8_TILE_AUTOTILE); + if (result != PXL8_OK) return result; + + pxl8_tilemap_update_autotiles(tilemap, layer, + x > 0 ? x - 1 : x, y > 0 ? y - 1 : y, + x < tilemap->width - 1 ? 3 : 2, + y < tilemap->height - 1 ? 3 : 2); + + return PXL8_OK; +} + +void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h) { + if (!tilemap || !tilemap->tilesheet) return; + + for (u32 ty = y; ty < y + h && ty < tilemap->height; ty++) { + for (u32 tx = x; tx < x + w && tx < tilemap->width; tx++) { + pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); + u8 flags = pxl8_tile_get_flags(tile); + + if (flags & PXL8_TILE_AUTOTILE) { + u16 base_id = pxl8_tile_get_id(tile); + u8 neighbors = pxl8_tilemap_get_neighbors(tilemap, layer, tx, ty, base_id); + u16 new_id = pxl8_tilesheet_apply_autotile(tilemap->tilesheet, base_id, neighbors); + + if (new_id != base_id) { + pxl8_tilemap_layer* l = &tilemap->layers[layer]; + pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, tx, ty); + if (chunk) { + u32 local_x = tx & PXL8_CHUNK_MASK; + u32 local_y = ty & PXL8_CHUNK_MASK; + u32 idx = local_y * PXL8_CHUNK_SIZE + local_x; + chunk->tiles[idx] = pxl8_tile_pack(new_id, flags, pxl8_tile_get_palette(tile)); + } + } + } + } + } +} + +u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap) { + if (!tilemap) return 0; + + u32 total = sizeof(pxl8_tilemap); + + for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { + const pxl8_tilemap_layer* layer = &tilemap->layers[i]; + total += layer->chunk_count * sizeof(pxl8_tile_chunk*); + total += layer->allocated_chunks * sizeof(pxl8_tile_chunk); + } + + return total; +} + +void pxl8_tilemap_compress(pxl8_tilemap* tilemap) { + if (!tilemap) return; + + for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { + pxl8_tilemap_layer* layer = &tilemap->layers[i]; + + for (u32 j = 0; j < layer->chunk_count; j++) { + pxl8_tile_chunk* chunk = layer->chunks[j]; + if (!chunk) continue; + + bool has_tiles = false; + for (u32 k = 0; k < PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE; k++) { + if (pxl8_tile_get_id(chunk->tiles[k]) != 0) { + has_tiles = true; + break; + } + } + + if (!has_tiles) { + free(chunk); + layer->chunks[j] = NULL; + layer->allocated_chunks--; + } else { + chunk->empty = false; + } + } + } +} diff --git a/src/pxl8_tilemap.h b/src/pxl8_tilemap.h index 764a62a..d612cc5 100644 --- a/src/pxl8_tilemap.h +++ b/src/pxl8_tilemap.h @@ -8,24 +8,78 @@ #define PXL8_MAX_TILEMAP_WIDTH 256 #define PXL8_MAX_TILEMAP_HEIGHT 256 #define PXL8_MAX_TILE_LAYERS 4 +#define PXL8_CHUNK_SIZE 16 +#define PXL8_CHUNK_MASK 15 typedef enum pxl8_tile_flags { PXL8_TILE_FLIP_X = 1 << 0, PXL8_TILE_FLIP_Y = 1 << 1, PXL8_TILE_SOLID = 1 << 2, PXL8_TILE_TRIGGER = 1 << 3, + PXL8_TILE_ANIMATED = 1 << 4, + PXL8_TILE_AUTOTILE = 1 << 5, } pxl8_tile_flags; -typedef struct pxl8_tile { - u16 id; - u8 flags; - u8 palette_offset; -} pxl8_tile; +#define PXL8_TILE_ID_MASK 0x0000FFFF +#define PXL8_TILE_FLAGS_MASK 0x00FF0000 +#define PXL8_TILE_PAL_MASK 0xFF000000 +#define PXL8_TILE_ID_SHIFT 0 +#define PXL8_TILE_FLAGS_SHIFT 16 +#define PXL8_TILE_PAL_SHIFT 24 + +typedef u32 pxl8_tile; + +static inline pxl8_tile pxl8_tile_pack(u16 id, u8 flags, u8 palette_offset) { + return (u32)id | ((u32)flags << 16) | ((u32)palette_offset << 24); +} + +static inline u16 pxl8_tile_get_id(pxl8_tile tile) { + return tile & PXL8_TILE_ID_MASK; +} + +static inline u8 pxl8_tile_get_flags(pxl8_tile tile) { + return (tile & PXL8_TILE_FLAGS_MASK) >> PXL8_TILE_FLAGS_SHIFT; +} + +static inline u8 pxl8_tile_get_palette(pxl8_tile tile) { + return (tile & PXL8_TILE_PAL_MASK) >> PXL8_TILE_PAL_SHIFT; +} + +typedef struct pxl8_tile_animation { + u16* frames; + u16 frame_count; + u16 current_frame; + f32 frame_duration; + f32 time_accumulator; +} pxl8_tile_animation; + +typedef struct pxl8_tile_properties { + void* user_data; + u32 property_flags; + i16 collision_offset_x; + i16 collision_offset_y; + u16 collision_width; + u16 collision_height; +} pxl8_tile_properties; + +typedef struct pxl8_autotile_rule { + u8 neighbor_mask; + u16 tile_id; +} pxl8_autotile_rule; + +typedef struct pxl8_tile_chunk { + pxl8_tile tiles[PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE]; + u32 chunk_x; + u32 chunk_y; + bool empty; +} pxl8_tile_chunk; typedef struct pxl8_tilemap_layer { - pxl8_tile* tiles; - u32 width; - u32 height; + pxl8_tile_chunk** chunks; + u32 chunks_wide; + u32 chunks_high; + u32 chunk_count; + u32 allocated_chunks; bool visible; u8 opacity; } pxl8_tilemap_layer; @@ -75,6 +129,15 @@ pxl8_tilemap* pxl8_tilemap_new(u32 width, u32 height, u32 tile_size); void pxl8_tilemap_destroy(pxl8_tilemap* tilemap); u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y); +void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time); + +pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, + u16 base_tile_id, u8 flags); +void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h); + +u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap); +void pxl8_tilemap_compress(pxl8_tilemap* tilemap); + #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/pxl8_tilesheet.c b/src/pxl8_tilesheet.c index f7865a4..a9ecb46 100644 --- a/src/pxl8_tilesheet.c +++ b/src/pxl8_tilesheet.c @@ -10,6 +10,7 @@ pxl8_result pxl8_tilesheet_init(pxl8_tilesheet* tilesheet, u32 tile_size) { memset(tilesheet, 0, sizeof(pxl8_tilesheet)); tilesheet->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE; + tilesheet->ref_count = 1; return PXL8_OK; } @@ -26,6 +27,33 @@ void pxl8_tilesheet_free(pxl8_tilesheet* tilesheet) { free(tilesheet->tile_valid); tilesheet->tile_valid = NULL; } + + if (tilesheet->animations) { + for (u32 i = 0; i < tilesheet->animation_count; i++) { + if (tilesheet->animations[i].frames) { + free(tilesheet->animations[i].frames); + } + } + free(tilesheet->animations); + tilesheet->animations = NULL; + } + + if (tilesheet->properties) { + free(tilesheet->properties); + tilesheet->properties = NULL; + } + + if (tilesheet->autotile_rules) { + for (u32 i = 0; i <= tilesheet->total_tiles; i++) { + if (tilesheet->autotile_rules[i]) { + free(tilesheet->autotile_rules[i]); + } + } + free(tilesheet->autotile_rules); + free(tilesheet->autotile_rule_counts); + tilesheet->autotile_rules = NULL; + tilesheet->autotile_rule_counts = NULL; + } } pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx_ctx* gfx) { @@ -177,6 +205,136 @@ pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size) { void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet) { if (!tilesheet) return; - pxl8_tilesheet_free(tilesheet); - free(tilesheet); -} \ No newline at end of file + pxl8_tilesheet_unref(tilesheet); +} + +void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet) { + if (!tilesheet) return; + tilesheet->ref_count++; +} + +void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet) { + if (!tilesheet) return; + + if (--tilesheet->ref_count == 0) { + pxl8_tilesheet_free(tilesheet); + free(tilesheet); + } +} + +pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id, + const u16* frames, u16 frame_count, f32 frame_duration) { + if (!tilesheet || !frames || frame_count == 0) return PXL8_ERROR_INVALID_ARGUMENT; + if (base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT; + + if (!tilesheet->animations) { + tilesheet->animations = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_animation)); + if (!tilesheet->animations) return PXL8_ERROR_OUT_OF_MEMORY; + } + + pxl8_tile_animation* anim = &tilesheet->animations[base_tile_id]; + if (anim->frames) free(anim->frames); + + anim->frames = malloc(frame_count * sizeof(u16)); + if (!anim->frames) return PXL8_ERROR_OUT_OF_MEMORY; + + memcpy(anim->frames, frames, frame_count * sizeof(u16)); + anim->frame_count = frame_count; + anim->current_frame = 0; + anim->frame_duration = frame_duration; + anim->time_accumulator = 0; + + tilesheet->animation_count++; + return PXL8_OK; +} + +void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time) { + if (!tilesheet || !tilesheet->animations) return; + + for (u32 i = 1; i <= tilesheet->total_tiles; i++) { + pxl8_tile_animation* anim = &tilesheet->animations[i]; + if (!anim->frames || anim->frame_count == 0) continue; + + anim->time_accumulator += delta_time; + while (anim->time_accumulator >= anim->frame_duration) { + anim->time_accumulator -= anim->frame_duration; + anim->current_frame = (anim->current_frame + 1) % anim->frame_count; + } + } +} + +u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id) { + if (!tilesheet || !tilesheet->animations || tile_id == 0 || tile_id > tilesheet->total_tiles) { + return tile_id; + } + + const pxl8_tile_animation* anim = &tilesheet->animations[tile_id]; + if (!anim->frames || anim->frame_count == 0) return tile_id; + + return anim->frames[anim->current_frame]; +} + +void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id, + const pxl8_tile_properties* props) { + if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return; + + if (!tilesheet->properties) { + tilesheet->properties = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_properties)); + if (!tilesheet->properties) return; + } + + tilesheet->properties[tile_id] = *props; +} + +const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id) { + if (!tilesheet || !tilesheet->properties || tile_id == 0 || tile_id > tilesheet->total_tiles) { + return NULL; + } + return &tilesheet->properties[tile_id]; +} + +pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id, + u8 neighbor_mask, u16 result_tile_id) { + if (!tilesheet || base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) { + return PXL8_ERROR_INVALID_ARGUMENT; + } + + if (!tilesheet->autotile_rules) { + tilesheet->autotile_rules = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_autotile_rule*)); + tilesheet->autotile_rule_counts = calloc(tilesheet->total_tiles + 1, sizeof(u32)); + if (!tilesheet->autotile_rules || !tilesheet->autotile_rule_counts) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + } + + u32 count = tilesheet->autotile_rule_counts[base_tile_id]; + pxl8_autotile_rule* new_rules = realloc(tilesheet->autotile_rules[base_tile_id], + (count + 1) * sizeof(pxl8_autotile_rule)); + if (!new_rules) return PXL8_ERROR_OUT_OF_MEMORY; + + new_rules[count].neighbor_mask = neighbor_mask; + new_rules[count].tile_id = result_tile_id; + + tilesheet->autotile_rules[base_tile_id] = new_rules; + tilesheet->autotile_rule_counts[base_tile_id]++; + + return PXL8_OK; +} + +u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors) { + if (!tilesheet || !tilesheet->autotile_rules || base_tile_id == 0 || + base_tile_id > tilesheet->total_tiles) { + return base_tile_id; + } + + pxl8_autotile_rule* rules = tilesheet->autotile_rules[base_tile_id]; + u32 rule_count = tilesheet->autotile_rule_counts[base_tile_id]; + + for (u32 i = 0; i < rule_count; i++) { + if (rules[i].neighbor_mask == neighbors) { + return rules[i].tile_id; + } + } + + return base_tile_id; +} diff --git a/src/pxl8_tilesheet.h b/src/pxl8_tilesheet.h index e40fdbf..b7432df 100644 --- a/src/pxl8_tilesheet.h +++ b/src/pxl8_tilesheet.h @@ -3,6 +3,10 @@ #include "pxl8_types.h" #include "pxl8_gfx.h" +typedef struct pxl8_tile_animation pxl8_tile_animation; +typedef struct pxl8_tile_properties pxl8_tile_properties; +typedef struct pxl8_autotile_rule pxl8_autotile_rule; + typedef struct pxl8_tilesheet { u8* data; bool* tile_valid; @@ -12,6 +16,15 @@ typedef struct pxl8_tilesheet { u32 tiles_per_row; u32 total_tiles; pxl8_color_mode color_mode; + u32 ref_count; + + pxl8_tile_animation* animations; + u32 animation_count; + + pxl8_tile_properties* properties; + + pxl8_autotile_rule** autotile_rules; + u32* autotile_rule_counts; } pxl8_tilesheet; #ifdef __cplusplus @@ -30,6 +43,22 @@ bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id); pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size); void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet); +void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet); +void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet); + +pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id, + const u16* frames, u16 frame_count, f32 frame_duration); +void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time); +u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id); + +void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id, + const pxl8_tile_properties* props); +const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id); + +pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id, + u8 neighbor_mask, u16 result_tile_id); +u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors); + #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/pxl8_vfx.c b/src/pxl8_vfx.c index 2451469..119bf06 100644 --- a/src/pxl8_vfx.c +++ b/src/pxl8_vfx.c @@ -344,7 +344,9 @@ void pxl8_vfx_fire(pxl8_particle_system* sys, i32 x, i32 y, i32 width, u8 palett static void rain_spawn(pxl8_particle* p, void* userdata) { (void)userdata; - p->color = 27 + (rand() % 3); + p->start_color = 27 + (rand() % 3); + p->end_color = 29; + p->color = p->start_color; p->max_life = 2.0f; p->vy = 200.0f + ((f32)rand() / RAND_MAX) * 100.0f; } @@ -391,7 +393,9 @@ void pxl8_vfx_smoke(pxl8_particle_system* sys, i32 x, i32 y, u8 color) { static void snow_spawn(pxl8_particle* p, void* userdata) { (void)userdata; - p->color = 10 + (rand() % 2); + p->start_color = 15 + (rand() % 2); + p->end_color = 10; + p->color = p->start_color; p->max_life = 4.0f; p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 20.0f; p->vy = 30.0f + ((f32)rand() / RAND_MAX) * 20.0f; @@ -458,4 +462,4 @@ void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread) { p->color = 8 + (rand() % 8); p->flags = 1; } -} \ No newline at end of file +} diff --git a/src/pxl8_vfx.h b/src/pxl8_vfx.h index 8beb96c..05aaa39 100644 --- a/src/pxl8_vfx.h +++ b/src/pxl8_vfx.h @@ -65,4 +65,4 @@ void pxl8_vfx_rain(pxl8_particle_system* sys, i32 width, f32 wind); void pxl8_vfx_smoke(pxl8_particle_system* sys, i32 x, i32 y, u8 color); void pxl8_vfx_snow(pxl8_particle_system* sys, i32 width, f32 wind); void pxl8_vfx_sparks(pxl8_particle_system* sys, i32 x, i32 y, u32 color); -void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread); \ No newline at end of file +void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread);