add cartridge system

This commit is contained in:
asrael 2025-09-27 11:03:36 -05:00
parent ff698730f1
commit 98ca54e920
25 changed files with 968 additions and 315 deletions

View file

@ -9,7 +9,7 @@
(var snow-init false) (var snow-init false)
(global init (fn [] (global init (fn []
(pxl8.load_palette "res/palettes/gruvbox.ase") (pxl8.load_palette "palettes/gruvbox.ase")
(set particles (pxl8.particles_new 1000)))) (set particles (pxl8.particles_new 1000))))
(global update (fn [dt] (global update (fn [dt]

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

@ -345,6 +345,7 @@ case "$COMMAND" in
src/pxl8.c src/pxl8.c
src/pxl8_ase.c src/pxl8_ase.c
src/pxl8_blit.c src/pxl8_blit.c
src/pxl8_cart.c
src/pxl8_font.c src/pxl8_font.c
src/pxl8_gfx.c src/pxl8_gfx.c
src/pxl8_io.c src/pxl8_io.c

View file

@ -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))))

View file

@ -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)))

View file

@ -12,6 +12,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_main.h> #include <SDL3/SDL_main.h>
#include "pxl8_cart.h"
#include "pxl8_lua.h" #include "pxl8_lua.h"
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include "pxl8_types.h" #include "pxl8_types.h"
@ -37,12 +38,13 @@ typedef struct pxl8_app_state {
lua_State* lua; lua_State* lua;
pxl8_repl_state repl; pxl8_repl_state repl;
pxl8_resolution resolution; pxl8_resolution resolution;
pxl8_cart* cart;
f32 fps_timer; f32 fps_timer;
i32 frame_count; i32 frame_count;
u64 last_time; u64 last_time;
f32 time; f32 time;
bool repl_mode; bool repl_mode;
bool running; bool running;
bool script_loaded; 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.color_mode = PXL8_COLOR_MODE_MEGA;
app.resolution = PXL8_RESOLUTION_640x360; app.resolution = PXL8_RESOLUTION_640x360;
const char* script_arg = NULL; 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++) { for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--repl") == 0) { if (strcmp(argv[i], "--repl") == 0) {
app.repl_mode = true; 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 <folder> <output.pxc>");
return SDL_APP_FAILURE;
}
} else if (!script_arg) { } else if (!script_arg) {
script_arg = argv[i]; script_arg = argv[i];
} }
} }
if (script_arg) { if (pack_mode) {
strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1); pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
app.script_path[sizeof(app.script_path) - 1] = '\0'; return (result == PXL8_OK) ? SDL_APP_SUCCESS : SDL_APP_FAILURE;
} else {
strcpy(app.script_path, "src/fnl/demo.fnl");
} }
@ -285,6 +298,32 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
pxl8_gfx_shutdown(&app.gfx); pxl8_gfx_shutdown(&app.gfx);
return SDL_APP_FAILURE; 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); 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) { if (app->repl_mode) {
pxl8_repl_shutdown(&app->repl); pxl8_repl_shutdown(&app->repl);
} }
if (app->cart) {
pxl8_cart_destroy(app->cart);
app->cart = NULL;
}
pxl8_lua_shutdown(app->lua); pxl8_lua_shutdown(app->lua);
pxl8_gfx_shutdown(&app->gfx); pxl8_gfx_shutdown(&app->gfx);
} }

View file

@ -397,4 +397,4 @@ void pxl8_ase_free(pxl8_ase_file* ase_file) {
} }
memset(ase_file, 0, sizeof(pxl8_ase_file)); memset(ase_file, 0, sizeof(pxl8_ase_file));
} }

326
src/pxl8_cart.c Normal file
View file

@ -0,0 +1,326 @@
#include "pxl8_cart.h"
#include "pxl8_macros.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
}

36
src/pxl8_cart.h Normal file
View file

@ -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

View file

@ -57,4 +57,4 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32*
} }
return PXL8_OK; return PXL8_OK;
} }

View file

@ -127,4 +127,4 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32*
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -1,6 +1,7 @@
#include "pxl8_lua.h" #include "pxl8_lua.h"
#include "pxl8_vfx.h" #include "pxl8_vfx.h"
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include <unistd.h>
static const char* pxl8_ffi_cdefs = static const char* pxl8_ffi_cdefs =
"typedef uint8_t u8;\n" "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; 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) { pxl8_result pxl8_lua_run_fennel_file(lua_State* lua_state, const char* filename) {
if (!lua_state || !filename) return PXL8_ERROR_NULL_POINTER; if (!lua_state || !filename) return PXL8_ERROR_NULL_POINTER;
lua_getglobal(lua_state, "fennel"); lua_getglobal(lua_state, "fennel");
if (lua_isnil(lua_state, -1)) { if (lua_isnil(lua_state, -1)) {
lua_pop(lua_state, 1); lua_pop(lua_state, 1);
return PXL8_ERROR_SCRIPT_ERROR; return PXL8_ERROR_SCRIPT_ERROR;
} }
lua_getfield(lua_state, -1, "dofile"); lua_getfield(lua_state, -1, "dofile");
lua_pushstring(lua_state, filename); lua_pushstring(lua_state, filename);
if (lua_pcall(lua_state, 1, 0, 0) != 0) { if (lua_pcall(lua_state, 1, 0, 0) != 0) {
printf("Fennel error: %s\n", lua_tostring(lua_state, -1)); printf("Fennel error: %s\n", lua_tostring(lua_state, -1));
lua_pop(lua_state, 1); lua_pop(lua_state, 1);
return PXL8_ERROR_SCRIPT_ERROR; return PXL8_ERROR_SCRIPT_ERROR;
} }
lua_pop(lua_state, 1); lua_pop(lua_state, 1);
return PXL8_OK; return PXL8_OK;
} }

View file

@ -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_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_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); 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 #ifdef __cplusplus
} }

View file

@ -4,6 +4,30 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
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) { pxl8_result pxl8_tilemap_init(pxl8_tilemap* tilemap, u32 width, u32 height, u32 tile_size) {
if (!tilemap) return PXL8_ERROR_NULL_POINTER; if (!tilemap) return PXL8_ERROR_NULL_POINTER;
if (width > PXL8_MAX_TILEMAP_WIDTH || height > PXL8_MAX_TILEMAP_HEIGHT) { 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->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
tilemap->active_layers = 1; tilemap->active_layers = 1;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
tilemap->layers[i].width = width; u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
tilemap->layers[i].height = height;
tilemap->layers[i].visible = (i == 0);
tilemap->layers[i].opacity = 255;
size_t tiles_size = width * height * sizeof(pxl8_tile); for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
tilemap->layers[i].tiles = calloc(1, tiles_size); pxl8_tilemap_layer* layer = &tilemap->layers[i];
if (!tilemap->layers[i].tiles) { 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++) { for (u32 j = 0; j < i; j++) {
free(tilemap->layers[j].tiles); free(tilemap->layers[j].chunks);
} }
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
} }
@ -39,17 +67,33 @@ void pxl8_tilemap_free(pxl8_tilemap* tilemap) {
if (!tilemap) return; if (!tilemap) return;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) { for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
if (tilemap->layers[i].tiles) { pxl8_tilemap_layer* layer = &tilemap->layers[i];
free(tilemap->layers[i].tiles); if (layer->chunks) {
tilemap->layers[i].tiles = NULL; 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) { pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) {
if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER; if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER;
if (tilemap->tilesheet) {
pxl8_tilesheet_unref(tilemap->tilesheet);
}
tilemap->tilesheet = tilesheet; tilemap->tilesheet = tilesheet;
pxl8_tilesheet_ref(tilesheet);
if (tilesheet->tile_size != tilemap->tile_size) { if (tilesheet->tile_size != tilemap->tile_size) {
pxl8_warn("Tilesheet tile size (%d) differs from tilemap tile size (%d)", 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; if (x >= tilemap->width || y >= tilemap->height) return PXL8_ERROR_INVALID_COORDINATE;
pxl8_tilemap_layer* l = &tilemap->layers[layer]; pxl8_tilemap_layer* l = &tilemap->layers[layer];
u32 idx = y * tilemap->width + x; pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, x, y);
l->tiles[idx].id = tile_id; if (!chunk) return PXL8_ERROR_OUT_OF_MEMORY;
l->tiles[idx].flags = flags;
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) { if (layer >= tilemap->active_layers) {
tilemap->active_layers = layer + 1; 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 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; const pxl8_tilemap_layer* l = &tilemap->layers[layer];
if (x >= tilemap->width || y >= tilemap->height) return empty_tile; 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) { 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]; const pxl8_tilemap_layer* l = &tilemap->layers[layer];
if (!l->visible) return; if (!l->visible) return;
pxl8_tilemap_view view; i32 view_left = tilemap->camera_x / tilemap->tile_size;
pxl8_tilemap_get_view(tilemap, gfx, &view); 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++) { u32 chunk_left = pxl8_max(0, view_left >> 4);
for (i32 tx = view.tile_start_x; tx < view.tile_end_x; tx++) { u32 chunk_top = pxl8_max(0, view_top >> 4);
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); u32 chunk_right = pxl8_min((tilemap->width + 15) >> 4, (view_right >> 4) + 1);
if (tile.id == 0) continue; u32 chunk_bottom = pxl8_min((tilemap->height + 15) >> 4, (view_bottom >> 4) + 1);
i32 screen_x = tx * tilemap->tile_size - tilemap->camera_x; for (u32 cy = chunk_top; cy < chunk_bottom; cy++) {
i32 screen_y = ty * tilemap->tile_size - tilemap->camera_y; 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++) { for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tile_x, tile_y); 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; 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++) { for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty); 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) { u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) {
if (!tilemap) return 0; if (!tilemap) return 0;
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y); pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y);
return tile.id; 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;
}
}
}
}

View file

@ -8,24 +8,78 @@
#define PXL8_MAX_TILEMAP_WIDTH 256 #define PXL8_MAX_TILEMAP_WIDTH 256
#define PXL8_MAX_TILEMAP_HEIGHT 256 #define PXL8_MAX_TILEMAP_HEIGHT 256
#define PXL8_MAX_TILE_LAYERS 4 #define PXL8_MAX_TILE_LAYERS 4
#define PXL8_CHUNK_SIZE 16
#define PXL8_CHUNK_MASK 15
typedef enum pxl8_tile_flags { typedef enum pxl8_tile_flags {
PXL8_TILE_FLIP_X = 1 << 0, PXL8_TILE_FLIP_X = 1 << 0,
PXL8_TILE_FLIP_Y = 1 << 1, PXL8_TILE_FLIP_Y = 1 << 1,
PXL8_TILE_SOLID = 1 << 2, PXL8_TILE_SOLID = 1 << 2,
PXL8_TILE_TRIGGER = 1 << 3, PXL8_TILE_TRIGGER = 1 << 3,
PXL8_TILE_ANIMATED = 1 << 4,
PXL8_TILE_AUTOTILE = 1 << 5,
} pxl8_tile_flags; } pxl8_tile_flags;
typedef struct pxl8_tile { #define PXL8_TILE_ID_MASK 0x0000FFFF
u16 id; #define PXL8_TILE_FLAGS_MASK 0x00FF0000
u8 flags; #define PXL8_TILE_PAL_MASK 0xFF000000
u8 palette_offset; #define PXL8_TILE_ID_SHIFT 0
} pxl8_tile; #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 { typedef struct pxl8_tilemap_layer {
pxl8_tile* tiles; pxl8_tile_chunk** chunks;
u32 width; u32 chunks_wide;
u32 height; u32 chunks_high;
u32 chunk_count;
u32 allocated_chunks;
bool visible; bool visible;
u8 opacity; u8 opacity;
} pxl8_tilemap_layer; } 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); void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);
u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y); 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 #ifdef __cplusplus
} }
#endif #endif

View file

@ -10,6 +10,7 @@ pxl8_result pxl8_tilesheet_init(pxl8_tilesheet* tilesheet, u32 tile_size) {
memset(tilesheet, 0, sizeof(pxl8_tilesheet)); memset(tilesheet, 0, sizeof(pxl8_tilesheet));
tilesheet->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE; tilesheet->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
tilesheet->ref_count = 1;
return PXL8_OK; return PXL8_OK;
} }
@ -26,6 +27,33 @@ void pxl8_tilesheet_free(pxl8_tilesheet* tilesheet) {
free(tilesheet->tile_valid); free(tilesheet->tile_valid);
tilesheet->tile_valid = NULL; 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) { 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) { void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return; if (!tilesheet) return;
pxl8_tilesheet_free(tilesheet); pxl8_tilesheet_unref(tilesheet);
free(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;
}

View file

@ -3,6 +3,10 @@
#include "pxl8_types.h" #include "pxl8_types.h"
#include "pxl8_gfx.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 { typedef struct pxl8_tilesheet {
u8* data; u8* data;
bool* tile_valid; bool* tile_valid;
@ -12,6 +16,15 @@ typedef struct pxl8_tilesheet {
u32 tiles_per_row; u32 tiles_per_row;
u32 total_tiles; u32 total_tiles;
pxl8_color_mode color_mode; 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; } pxl8_tilesheet;
#ifdef __cplusplus #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); pxl8_tilesheet* pxl8_tilesheet_new(u32 tile_size);
void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet); 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 #ifdef __cplusplus
} }
#endif #endif

View file

@ -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) { static void rain_spawn(pxl8_particle* p, void* userdata) {
(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->max_life = 2.0f;
p->vy = 200.0f + ((f32)rand() / RAND_MAX) * 100.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) { static void snow_spawn(pxl8_particle* p, void* userdata) {
(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->max_life = 4.0f;
p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 20.0f; p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 20.0f;
p->vy = 30.0f + ((f32)rand() / RAND_MAX) * 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->color = 8 + (rand() % 8);
p->flags = 1; p->flags = 1;
} }
} }

View file

@ -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_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_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_sparks(pxl8_particle_system* sys, i32 x, i32 y, u32 color);
void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread); void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread);