add save and bundle pxl8 with game for standalone game distribution

This commit is contained in:
asrael 2025-11-28 23:42:57 -06:00
parent b1e8525c3e
commit 04d3af11a9
25 changed files with 1173 additions and 346 deletions

View file

@ -173,7 +173,7 @@ timestamp() {
update_fennel() {
print_info "Fetching Fennel"
local version="1.5.3"
local version="1.6.0"
if curl -sL --max-time 5 -o lib/fennel/fennel.lua "https://fennel-lang.org/downloads/fennel-${version}.lua" 2>/dev/null; then
if [[ -f "lib/fennel/fennel.lua" ]] && [[ -s "lib/fennel/fennel.lua" ]]; then
@ -217,11 +217,10 @@ update_luajit() {
print_info "Updated LuaJIT (${version})"
}
update_miniz() {
print_info "Fetching miniz"
local version="3.0.2"
local version="3.1.0"
if curl -sL --max-time 5 -o /tmp/miniz.zip "https://github.com/richgel999/miniz/releases/download/${version}/miniz-${version}.zip" 2>/dev/null; then
unzip -qjo /tmp/miniz.zip miniz.c miniz.h -d lib/miniz/ 2>/dev/null
@ -344,6 +343,7 @@ case "$COMMAND" in
src/pxl8_io.c
src/pxl8_math.c
src/pxl8_rec.c
src/pxl8_save.c
src/pxl8_script.c
src/pxl8_sdl3.c
src/pxl8_tilemap.c

View file

@ -42,6 +42,11 @@ pxl8.create_texture = gfx2d.create_texture
pxl8.upload_atlas = gfx2d.upload_atlas
pxl8.gfx_color_ramp = gfx2d.color_ramp
pxl8.gfx_fade_palette = gfx2d.fade_palette
pxl8.gfx_cycle_palette = gfx2d.cycle_palette
pxl8.add_palette_cycle = gfx2d.add_palette_cycle
pxl8.remove_palette_cycle = gfx2d.remove_palette_cycle
pxl8.set_palette_cycle_speed = gfx2d.set_palette_cycle_speed
pxl8.clear_palette_cycles = gfx2d.clear_palette_cycles
pxl8.key_down = input.key_down
pxl8.key_pressed = input.key_pressed

View file

@ -82,4 +82,24 @@ function graphics.fade_palette(start, count, amount, target_color)
C.pxl8_gfx_fade_palette(core.gfx, start, count, amount, target_color)
end
function graphics.cycle_palette(start, count, step)
C.pxl8_gfx_cycle_palette(core.gfx, start, count, step or 1)
end
function graphics.add_palette_cycle(start_index, end_index, speed)
return C.pxl8_gfx_add_palette_cycle(core.gfx, start_index, end_index, speed or 1.0)
end
function graphics.remove_palette_cycle(cycle_id)
C.pxl8_gfx_remove_palette_cycle(core.gfx, cycle_id)
end
function graphics.set_palette_cycle_speed(cycle_id, speed)
C.pxl8_gfx_set_palette_cycle_speed(core.gfx, cycle_id, speed)
end
function graphics.clear_palette_cycles()
C.pxl8_gfx_clear_palette_cycles(core.gfx)
end
return graphics

View file

@ -73,10 +73,11 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_game* game = sys->game;
game->color_mode = PXL8_COLOR_MODE_MEGA;
game->resolution = PXL8_RESOLUTION_640x360;
pxl8_pixel_mode pixel_mode = PXL8_PIXEL_INDEXED;
pxl8_resolution resolution = PXL8_RESOLUTION_640x360;
const char* script_arg = NULL;
bool bundle_mode = false;
bool pack_mode = false;
const char* pack_input = NULL;
const char* pack_output = NULL;
@ -84,6 +85,15 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
for (i32 i = 1; i < argc; i++) {
if (strcmp(argv[i], "--repl") == 0) {
game->repl_mode = true;
} else if (strcmp(argv[i], "--bundle") == 0) {
bundle_mode = true;
if (i + 2 < argc) {
pack_input = argv[++i];
pack_output = argv[++i];
} else {
pxl8_error("--bundle requires <folder|.pxc> <output>");
return PXL8_ERROR_INVALID_ARGUMENT;
}
} else if (strcmp(argv[i], "--pack") == 0) {
pack_mode = true;
if (i + 2 < argc) {
@ -98,6 +108,18 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
}
}
if (bundle_mode) {
char exe_path[1024];
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (len == -1) {
pxl8_error("Failed to resolve executable path");
return PXL8_ERROR_SYSTEM_FAILURE;
}
exe_path[len] = '\0';
pxl8_result result = pxl8_cart_bundle(pack_input, pack_output, exe_path);
return result;
}
if (pack_mode) {
pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
return result;
@ -109,13 +131,13 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_info("Starting up");
sys->platform_data = sys->hal->create(game->color_mode, game->resolution, "pxl8", 1280, 720);
sys->platform_data = sys->hal->create(pixel_mode, resolution, "pxl8", 1280, 720);
if (!sys->platform_data) {
pxl8_error("Failed to create platform context");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, game->color_mode, game->resolution);
game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, pixel_mode, resolution);
if (!game->gfx) {
pxl8_error("Failed to create graphics context");
return PXL8_ERROR_INITIALIZATION_FAILED;
@ -126,13 +148,32 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
return PXL8_ERROR_INITIALIZATION_FAILED;
}
game->script = pxl8_script_create();
game->script = pxl8_script_create(game->repl_mode);
if (!game->script) {
pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(game->script));
return PXL8_ERROR_INITIALIZATION_FAILED;
}
const char* cart_path = script_arg ? script_arg : "demo";
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
const char* cart_path = script_arg;
if (has_embedded && !script_arg) {
sys->cart = pxl8_cart_create();
if (!sys->cart) {
pxl8_error("Failed to create cart");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
if (pxl8_cart_load_embedded(sys->cart, argv[0]) == PXL8_OK) {
pxl8_cart_mount(sys->cart);
strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
pxl8_info("Running embedded cart");
} else {
pxl8_error("Failed to load embedded cart");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
} else if (cart_path || !has_embedded) {
if (!cart_path) cart_path = "demo";
struct stat st;
bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
@ -150,7 +191,6 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_cart_mount(sys->cart);
strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
pxl8_info("Loaded cart: %s", pxl8_cart_get_name(sys->cart));
} else {
pxl8_error("Failed to load cart: %s", cart_path);
return PXL8_ERROR_INITIALIZATION_FAILED;
@ -160,6 +200,7 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
strncpy(game->script_path, script_arg, sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
}
}
pxl8_script_set_gfx(game->script, game->gfx);
pxl8_script_set_input(game->script, &game->input);
@ -245,6 +286,8 @@ pxl8_result pxl8_update(pxl8* sys) {
}
}
pxl8_gfx_update(game->gfx, dt);
if (game->script_loaded) {
pxl8_script_call_function_f32(game->script, "update", dt);
}
@ -268,19 +311,18 @@ pxl8_result pxl8_frame(pxl8* sys) {
} else {
pxl8_clear(game->gfx, 32);
pxl8_size render_size = pxl8_get_resolution_dimensions(game->resolution);
i32 render_w = pxl8_gfx_get_width(game->gfx);
i32 render_h = pxl8_gfx_get_height(game->gfx);
for (i32 y = 0; y < render_size.h; y += 24) {
for (i32 x = 0; x < render_size.w; x += 32) {
for (i32 y = 0; y < render_h; y += 24) {
for (i32 x = 0; x < render_w; x += 32) {
u32 color = ((x / 32) + (y / 24) + (i32)(game->time * 2)) % 8;
pxl8_rect_fill(game->gfx, x, y, 31, 23, color);
}
}
}
pxl8_size render_size = pxl8_get_resolution_dimensions(game->resolution);
pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, render_size.w, render_size.h));
pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, pxl8_gfx_get_width(game->gfx), pxl8_gfx_get_height(game->gfx)));
pxl8_gfx_upload_framebuffer(game->gfx);
pxl8_gfx_upload_atlas(game->gfx);
pxl8_gfx_present(game->gfx);
@ -343,10 +385,6 @@ pxl8_input_state* pxl8_get_input(const pxl8* sys) {
return (sys && sys->game) ? &sys->game->input : NULL;
}
pxl8_resolution pxl8_get_resolution(const pxl8* sys) {
return (sys && sys->game) ? sys->game->resolution : PXL8_RESOLUTION_640x360;
}
void pxl8_center_cursor(pxl8* sys) {
if (!sys || !sys->hal || !sys->hal->center_cursor) return;
sys->hal->center_cursor(sys->platform_data);
@ -365,17 +403,6 @@ void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled) {
}
}
u32 pxl8_get_palette_size(pxl8_color_mode mode) {
switch (mode) {
case PXL8_COLOR_MODE_HICOLOR: return 0;
case PXL8_COLOR_MODE_FAMI: return 64;
case PXL8_COLOR_MODE_MEGA: return 512;
case PXL8_COLOR_MODE_GBA:
case PXL8_COLOR_MODE_SNES: return 32768;
default: return 256;
}
}
pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution) {
switch (resolution) {
case PXL8_RESOLUTION_240x160: return (pxl8_size){240, 160};

View file

@ -8,6 +8,7 @@
#define MINIZ_NO_ARCHIVE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#include <miniz.h>

View file

@ -141,14 +141,14 @@ static void pxl8_skyline_compact(pxl8_skyline* skyline) {
}
}
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode) {
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode) {
pxl8_atlas* atlas = (pxl8_atlas*)calloc(1, sizeof(pxl8_atlas));
if (!atlas) return NULL;
atlas->height = height;
atlas->width = width;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode);
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
atlas->pixels = (u8*)calloc(width * height, bytes_per_pixel);
if (!atlas->pixels) {
free(atlas);
@ -199,10 +199,10 @@ void pxl8_atlas_destroy(pxl8_atlas* atlas) {
free(atlas);
}
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode) {
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode) {
if (!atlas || atlas->width >= 4096) return false;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode);
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
u32 new_size = atlas->width * 2;
u32 old_width = atlas->width;
@ -278,14 +278,14 @@ u32 pxl8_atlas_add_texture(
const u8* pixels,
u32 w,
u32 h,
pxl8_color_mode color_mode
pxl8_pixel_mode pixel_mode
) {
if (!atlas || !pixels) return UINT32_MAX;
pxl8_skyline_fit fit =
pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) {
if (!pxl8_atlas_expand(atlas, color_mode)) {
if (!pxl8_atlas_expand(atlas, pixel_mode)) {
return UINT32_MAX;
}
@ -319,7 +319,7 @@ u32 pxl8_atlas_add_texture(
entry->w = w;
entry->h = h;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(color_mode);
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
for (u32 y = 0; y < h; y++) {
for (u32 x = 0; x < w; x++) {
u32 src_idx = y * w + x;

View file

@ -14,7 +14,7 @@ typedef struct pxl8_atlas_entry {
extern "C" {
#endif
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode);
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode);
void pxl8_atlas_destroy(pxl8_atlas* atlas);
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id);
@ -26,8 +26,8 @@ bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas);
void pxl8_atlas_mark_clean(pxl8_atlas* atlas);
u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_color_mode color_mode);
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode);
u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_pixel_mode pixel_mode);
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode);
#ifdef __cplusplus
}

View file

@ -1,78 +1,58 @@
#include "pxl8_cart.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <miniz.h>
#include "pxl8_macros.h"
#define PXL8_CART_MAGIC 0x43585850
#define PXL8_CART_VERSION 1
#define PXL8_CART_TRAILER_MAGIC 0x544E4250
typedef struct {
u32 magic;
u16 version;
u16 flags;
u32 file_count;
u32 toc_size;
} pxl8_cart_header;
typedef struct {
u32 offset;
u32 size;
u16 path_len;
} pxl8_cart_entry;
typedef struct {
u32 magic;
u32 cart_offset;
u32 cart_size;
} pxl8_cart_trailer;
typedef struct {
char* path;
u32 offset;
u32 size;
} pxl8_cart_file;
struct pxl8_cart {
void* archive_data;
size_t archive_size;
u8* data;
u32 data_size;
pxl8_cart_file* files;
u32 file_count;
char* base_path;
char* name;
bool is_folder;
bool is_mounted;
char* name;
};
static pxl8_cart* pxl8_current_cart = NULL;
static char* pxl8_original_cwd = NULL;
static void pxl8_add_file_recursive(mz_zip_archive* zip, const char* dir_path, const char* prefix) {
DIR* dir = opendir(dir_path);
if (!dir) return;
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
char full_path[1024];
char zip_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
snprintf(zip_path, sizeof(zip_path), "%s%s", prefix, entry->d_name);
struct stat st;
if (stat(full_path, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
char new_prefix[1025];
snprintf(new_prefix, sizeof(new_prefix), "%s/", zip_path);
pxl8_add_file_recursive(zip, full_path, new_prefix);
} else {
pxl8_info("Adding: %s", zip_path);
if (!mz_zip_writer_add_file(zip, zip_path, full_path, NULL, 0, MZ_BEST_COMPRESSION)) {
pxl8_warn("Failed to add file: %s", zip_path);
}
}
}
}
closedir(dir);
}
static char* get_cart_name(const char* path) {
char* name = strdup(path);
size_t len = strlen(name);
if (len > 4 && strcmp(name + len - 4, ".pxc") == 0) {
name[len - 4] = '\0';
}
char* last_slash = strrchr(name, '/');
if (last_slash) {
char* result = strdup(last_slash + 1);
free(name);
return result;
}
return name;
}
static bool is_directory(const char* path) {
struct stat st;
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
@ -83,9 +63,99 @@ static bool is_pxc_file(const char* path) {
return len > 4 && strcmp(path + len - 4, ".pxc") == 0;
}
static char* get_cart_name(const char* path) {
char* name = strdup(path);
size_t len = strlen(name);
if (len > 4 && strcmp(name + len - 4, ".pxc") == 0) {
name[len - 4] = '\0';
}
char* last_slash = strrchr(name, '/');
if (last_slash) {
char* result = strdup(last_slash + 1);
free(name);
return result;
}
return name;
}
static void collect_files_recursive(const char* dir_path, const char* prefix,
char*** paths, u32* count, u32* capacity) {
DIR* dir = opendir(dir_path);
if (!dir) return;
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
char full_path[1024];
char rel_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
snprintf(rel_path, sizeof(rel_path), "%s%s", prefix, entry->d_name);
struct stat st;
if (stat(full_path, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
char new_prefix[1025];
snprintf(new_prefix, sizeof(new_prefix), "%s/", rel_path);
collect_files_recursive(full_path, new_prefix, paths, count, capacity);
} else {
if (*count >= *capacity) {
*capacity = (*capacity == 0) ? 64 : (*capacity * 2);
*paths = realloc(*paths, *capacity * sizeof(char*));
}
(*paths)[(*count)++] = strdup(rel_path);
}
}
}
closedir(dir);
}
static pxl8_cart_file* find_file(const pxl8_cart* cart, const char* path) {
if (!cart || !cart->files) return NULL;
for (u32 i = 0; i < cart->file_count; i++) {
if (strcmp(cart->files[i].path, path) == 0) {
return &cart->files[i];
}
}
return NULL;
}
static pxl8_result load_packed_cart(pxl8_cart* cart, const u8* data, u32 size) {
if (size < sizeof(pxl8_cart_header)) return PXL8_ERROR_INVALID_FORMAT;
const pxl8_cart_header* header = (const pxl8_cart_header*)data;
if (header->magic != PXL8_CART_MAGIC) return PXL8_ERROR_INVALID_FORMAT;
if (header->version > PXL8_CART_VERSION) return PXL8_ERROR_INVALID_FORMAT;
cart->data = malloc(size);
if (!cart->data) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(cart->data, data, size);
cart->data_size = size;
cart->file_count = header->file_count;
cart->files = calloc(cart->file_count, sizeof(pxl8_cart_file));
if (!cart->files) return PXL8_ERROR_OUT_OF_MEMORY;
const u8* toc = cart->data + sizeof(pxl8_cart_header);
for (u32 i = 0; i < cart->file_count; i++) {
const pxl8_cart_entry* entry = (const pxl8_cart_entry*)toc;
toc += sizeof(pxl8_cart_entry);
cart->files[i].path = malloc(entry->path_len + 1);
memcpy(cart->files[i].path, toc, entry->path_len);
cart->files[i].path[entry->path_len] = '\0';
toc += entry->path_len;
cart->files[i].offset = entry->offset;
cart->files[i].size = entry->size;
}
cart->is_folder = false;
return PXL8_OK;
}
pxl8_cart* pxl8_cart_create(void) {
pxl8_cart* cart = calloc(1, sizeof(pxl8_cart));
return cart;
return calloc(1, sizeof(pxl8_cart));
}
pxl8_cart* pxl8_cart_current(void) {
@ -98,19 +168,9 @@ void pxl8_cart_destroy(pxl8_cart* cart) {
free(cart);
}
const char* pxl8_cart_get_base_path(const pxl8_cart* cart) {
return cart ? cart->base_path : NULL;
}
const char* pxl8_cart_get_name(const pxl8_cart* cart) {
return cart ? cart->name : NULL;
}
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
if (!cart || !path) return PXL8_ERROR_NULL_POINTER;
pxl8_cart_unload(cart);
cart->name = get_cart_name(path);
if (is_directory(path)) {
@ -138,126 +198,109 @@ pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
if (is_pxc_file(path)) {
FILE* file = fopen(path, "rb");
if (!file) {
pxl8_error("Failed to open cart archive: %s", path);
pxl8_error("Failed to open cart: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
fseek(file, 0, SEEK_END);
cart->archive_size = ftell(file);
u32 size = (u32)ftell(file);
fseek(file, 0, SEEK_SET);
cart->archive_data = malloc(cart->archive_size);
if (!cart->archive_data) {
fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (fread(cart->archive_data, 1, cart->archive_size, file) != cart->archive_size) {
free(cart->archive_data);
cart->archive_data = NULL;
u8* data = malloc(size);
if (!data || fread(data, 1, size, file) != size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
char temp_dir[256];
snprintf(temp_dir, sizeof(temp_dir), "/tmp/pxl8_%s_%d", cart->name, getpid());
pxl8_result result = load_packed_cart(cart, data, size);
free(data);
if (mkdir(temp_dir, 0755) != 0) {
pxl8_error("Failed to create temp directory: %s", temp_dir);
return PXL8_ERROR_SYSTEM_FAILURE;
if (result == PXL8_OK) {
pxl8_info("Loaded cart: %s", cart->name);
}
mz_zip_archive zip = {0};
if (!mz_zip_reader_init_mem(&zip, cart->archive_data, cart->archive_size, 0)) {
pxl8_error("Failed to open cart archive as zip");
return PXL8_ERROR_INVALID_FORMAT;
}
i32 num_files = mz_zip_reader_get_num_files(&zip);
for (i32 i = 0; i < num_files; i++) {
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip, i, &file_stat)) continue;
char extract_path[1024];
snprintf(extract_path, sizeof(extract_path), "%s/%s", temp_dir, file_stat.m_filename);
if (file_stat.m_is_directory) {
mkdir(extract_path, 0755);
} else {
char* last_slash = strrchr(extract_path, '/');
if (last_slash) {
*last_slash = '\0';
mkdir(extract_path, 0755);
*last_slash = '/';
}
if (!mz_zip_reader_extract_to_file(&zip, i, extract_path, 0)) {
pxl8_warn("Failed to extract: %s", file_stat.m_filename);
}
}
}
mz_zip_reader_end(&zip);
cart->base_path = strdup(temp_dir);
cart->is_folder = false;
pxl8_info("Loaded cart archive: %s", cart->name);
return PXL8_OK;
return result;
}
pxl8_error("Unknown cart format: %s", path);
return PXL8_ERROR_INVALID_FORMAT;
}
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path) {
if (!cart || !exe_path) return PXL8_ERROR_NULL_POINTER;
FILE* file = fopen(exe_path, "rb");
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
pxl8_cart_trailer trailer;
if (fread(&trailer, sizeof(trailer), 1, file) != 1) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
if (trailer.magic != PXL8_CART_TRAILER_MAGIC) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
fseek(file, trailer.cart_offset, SEEK_SET);
u8* data = malloc(trailer.cart_size);
if (!data || fread(data, 1, trailer.cart_size, file) != trailer.cart_size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
cart->name = get_cart_name(exe_path);
pxl8_result result = load_packed_cart(cart, data, trailer.cart_size);
free(data);
if (result == PXL8_OK) {
pxl8_info("Loaded embedded cart: %s", cart->name);
}
return result;
}
void pxl8_cart_unload(pxl8_cart* cart) {
if (!cart) return;
pxl8_cart_unmount(cart);
if (!cart->is_folder && cart->base_path) {
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", cart->base_path);
system(cmd);
if (cart->files) {
for (u32 i = 0; i < cart->file_count; i++) {
free(cart->files[i].path);
}
char* cart_name = cart->name ? strdup(cart->name) : NULL;
if (cart->base_path) {
free(cart->base_path);
cart->base_path = NULL;
free(cart->files);
cart->files = NULL;
}
cart->file_count = 0;
free(cart->data);
cart->data = NULL;
cart->data_size = 0;
if (cart->name) {
pxl8_info("Unloaded cart: %s", cart->name);
free(cart->name);
cart->name = NULL;
}
if (cart->archive_data) {
free(cart->archive_data);
cart->archive_data = NULL;
}
cart->archive_size = 0;
free(cart->base_path);
cart->base_path = NULL;
cart->is_folder = false;
cart->is_mounted = false;
if (cart_name) {
pxl8_info("Unloaded cart: %s", cart_name);
free(cart_name);
}
}
pxl8_result pxl8_cart_mount(pxl8_cart* cart) {
if (!cart || !cart->base_path) return PXL8_ERROR_NULL_POINTER;
if (!cart) return PXL8_ERROR_NULL_POINTER;
if (cart->is_mounted) return PXL8_OK;
if (pxl8_current_cart) {
pxl8_cart_unmount(pxl8_current_cart);
}
if (cart->is_folder) {
pxl8_original_cwd = getcwd(NULL, 0);
if (chdir(cart->base_path) != 0) {
pxl8_error("Failed to change to cart directory: %s", cart->base_path);
@ -265,10 +308,10 @@ pxl8_result pxl8_cart_mount(pxl8_cart* cart) {
pxl8_original_cwd = NULL;
return PXL8_ERROR_FILE_NOT_FOUND;
}
}
cart->is_mounted = true;
pxl8_current_cart = cart;
pxl8_info("Mounted cart: %s", cart->name);
return PXL8_OK;
}
@ -286,24 +329,97 @@ void pxl8_cart_unmount(pxl8_cart* cart) {
if (pxl8_current_cart == cart) {
pxl8_current_cart = NULL;
}
pxl8_info("Unmounted cart: %s", cart->name);
}
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) {
if (!cart || !cart->base_path || !relative_path || !out_path || out_size == 0) return false;
const char* pxl8_cart_get_base_path(const pxl8_cart* cart) {
return cart ? cart->base_path : NULL;
}
const char* pxl8_cart_get_name(const pxl8_cart* cart) {
return cart ? cart->name : NULL;
}
bool pxl8_cart_is_packed(const pxl8_cart* cart) {
return cart && !cart->is_folder;
}
bool pxl8_cart_has_embedded(const char* exe_path) {
FILE* file = fopen(exe_path, "rb");
if (!file) return false;
fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END);
pxl8_cart_trailer trailer;
bool has = (fread(&trailer, sizeof(trailer), 1, file) == 1 &&
trailer.magic == PXL8_CART_TRAILER_MAGIC);
fclose(file);
return has;
}
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
if (!cart || !path) return false;
if (cart->is_folder) {
char full_path[512];
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
return access(full_path, F_OK) == 0;
}
return find_file(cart, path) != NULL;
}
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) {
if (!cart || !relative_path || !out_path || out_size == 0) return false;
if (cart->is_folder && cart->base_path) {
i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path);
return written >= 0 && (size_t)written < out_size;
}
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
if (!cart || !cart->base_path || !path) return false;
i32 written = snprintf(out_path, out_size, "%s", relative_path);
return written >= 0 && (size_t)written < out_size;
}
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out) {
if (!cart || !path || !data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
if (cart->is_folder) {
char full_path[512];
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
return access(full_path, F_OK) == 0;
FILE* file = fopen(full_path, "rb");
if (!file) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(file, 0, SEEK_END);
*size_out = (u32)ftell(file);
fseek(file, 0, SEEK_SET);
*data_out = malloc(*size_out);
if (!*data_out || fread(*data_out, 1, *size_out, file) != *size_out) {
free(*data_out);
*data_out = NULL;
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
return PXL8_OK;
}
pxl8_cart_file* cf = find_file(cart, path);
if (!cf) return PXL8_ERROR_FILE_NOT_FOUND;
*size_out = cf->size;
*data_out = malloc(cf->size);
if (!*data_out) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(*data_out, cart->data + cf->offset, cf->size);
return PXL8_OK;
}
void pxl8_cart_free_file(u8* data) {
free(data);
}
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
@ -324,22 +440,169 @@ pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
}
}
mz_zip_archive zip = {0};
if (!mz_zip_writer_init_file(&zip, output_path, 0)) {
pxl8_error("Failed to create archive: %s", output_path);
return PXL8_ERROR_SYSTEM_FAILURE;
char** paths = NULL;
u32 count = 0, capacity = 0;
collect_files_recursive(folder_path, "", &paths, &count, &capacity);
if (count == 0) {
pxl8_error("No files found in cart folder");
return PXL8_ERROR_FILE_NOT_FOUND;
}
u32 toc_size = 0;
for (u32 i = 0; i < count; i++) {
toc_size += sizeof(pxl8_cart_entry) + strlen(paths[i]);
}
u32 data_offset = sizeof(pxl8_cart_header) + toc_size;
u32 total_size = data_offset;
u32* file_sizes = malloc(count * sizeof(u32));
for (u32 i = 0; i < count; i++) {
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
struct stat st;
if (stat(full_path, &st) == 0) {
file_sizes[i] = (u32)st.st_size;
total_size += file_sizes[i];
} else {
file_sizes[i] = 0;
}
}
u8* buffer = calloc(1, total_size);
if (!buffer) {
free(file_sizes);
for (u32 i = 0; i < count; i++) free(paths[i]);
free(paths);
return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_cart_header* header = (pxl8_cart_header*)buffer;
header->magic = PXL8_CART_MAGIC;
header->version = PXL8_CART_VERSION;
header->flags = 0;
header->file_count = count;
header->toc_size = toc_size;
u8* toc = buffer + sizeof(pxl8_cart_header);
u32 file_offset = data_offset;
pxl8_info("Packing cart: %s -> %s", folder_path, output_path);
pxl8_add_file_recursive(&zip, folder_path, "");
if (!mz_zip_writer_finalize_archive(&zip)) {
pxl8_error("Failed to finalize archive");
mz_zip_writer_end(&zip);
for (u32 i = 0; i < count; i++) {
pxl8_cart_entry* entry = (pxl8_cart_entry*)toc;
entry->offset = file_offset;
entry->size = file_sizes[i];
entry->path_len = (u16)strlen(paths[i]);
toc += sizeof(pxl8_cart_entry);
memcpy(toc, paths[i], entry->path_len);
toc += entry->path_len;
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]);
FILE* file = fopen(full_path, "rb");
if (file) {
fread(buffer + file_offset, 1, file_sizes[i], file);
fclose(file);
pxl8_info(" %s (%u bytes)", paths[i], file_sizes[i]);
}
file_offset += file_sizes[i];
free(paths[i]);
}
free(paths);
free(file_sizes);
FILE* out = fopen(output_path, "wb");
if (!out) {
free(buffer);
return PXL8_ERROR_SYSTEM_FAILURE;
}
mz_zip_writer_end(&zip);
pxl8_info("Cart packed successfully!");
fwrite(buffer, 1, total_size, out);
fclose(out);
free(buffer);
pxl8_info("Cart packed: %u files, %u bytes", count, total_size);
return PXL8_OK;
}
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path) {
if (!input_path || !output_path || !exe_path) return PXL8_ERROR_NULL_POINTER;
u8* cart_data = NULL;
u32 cart_size = 0;
bool free_cart = false;
if (is_directory(input_path)) {
char temp_pxc[256];
snprintf(temp_pxc, sizeof(temp_pxc), "/tmp/pxl8_bundle_%d.pxc", getpid());
pxl8_result result = pxl8_cart_pack(input_path, temp_pxc);
if (result != PXL8_OK) return result;
FILE* f = fopen(temp_pxc, "rb");
if (!f) return PXL8_ERROR_SYSTEM_FAILURE;
fseek(f, 0, SEEK_END);
cart_size = (u32)ftell(f);
fseek(f, 0, SEEK_SET);
cart_data = malloc(cart_size);
fread(cart_data, 1, cart_size, f);
fclose(f);
unlink(temp_pxc);
free_cart = true;
} else if (is_pxc_file(input_path)) {
FILE* f = fopen(input_path, "rb");
if (!f) return PXL8_ERROR_FILE_NOT_FOUND;
fseek(f, 0, SEEK_END);
cart_size = (u32)ftell(f);
fseek(f, 0, SEEK_SET);
cart_data = malloc(cart_size);
fread(cart_data, 1, cart_size, f);
fclose(f);
free_cart = true;
} else {
return PXL8_ERROR_INVALID_FORMAT;
}
FILE* exe = fopen(exe_path, "rb");
if (!exe) {
if (free_cart) free(cart_data);
return PXL8_ERROR_FILE_NOT_FOUND;
}
fseek(exe, 0, SEEK_END);
u32 exe_size = (u32)ftell(exe);
fseek(exe, 0, SEEK_SET);
u8* exe_data = malloc(exe_size);
fread(exe_data, 1, exe_size, exe);
fclose(exe);
FILE* out = fopen(output_path, "wb");
if (!out) {
free(exe_data);
if (free_cart) free(cart_data);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fwrite(exe_data, 1, exe_size, out);
free(exe_data);
u32 cart_offset = exe_size;
fwrite(cart_data, 1, cart_size, out);
if (free_cart) free(cart_data);
pxl8_cart_trailer trailer = {
.magic = PXL8_CART_TRAILER_MAGIC,
.cart_offset = cart_offset,
.cart_size = cart_size
};
fwrite(&trailer, sizeof(trailer), 1, out);
fclose(out);
chmod(output_path, 0755);
pxl8_info("Bundle created: %s", output_path);
return PXL8_OK;
}

View file

@ -11,15 +11,25 @@ extern "C" {
pxl8_cart* pxl8_cart_create(void);
pxl8_cart* pxl8_cart_current(void);
void pxl8_cart_destroy(pxl8_cart* cart);
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path);
pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path);
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path);
void pxl8_cart_unload(pxl8_cart* cart);
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
void pxl8_cart_unmount(pxl8_cart* cart);
const char* pxl8_cart_get_base_path(const pxl8_cart* cart);
const char* pxl8_cart_get_name(const pxl8_cart* cart);
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
bool pxl8_cart_is_packed(const pxl8_cart* cart);
bool pxl8_cart_has_embedded(const char* exe_path);
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path);
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size);
void pxl8_cart_unload(pxl8_cart* cart);
void pxl8_cart_unmount(pxl8_cart* cart);
pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out);
void pxl8_cart_free_file(u8* data);
#ifdef __cplusplus
}

View file

@ -2,8 +2,8 @@
#include "pxl8_types.h"
static inline i32 pxl8_bytes_per_pixel(pxl8_color_mode mode) {
return (mode == PXL8_COLOR_MODE_HICOLOR) ? 2 : 1;
static inline i32 pxl8_bytes_per_pixel(pxl8_pixel_mode mode) {
return (mode == PXL8_PIXEL_HICOLOR) ? 2 : 1;
}
static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) {

View file

@ -5,9 +5,7 @@
#include "pxl8_types.h"
typedef struct pxl8_game {
pxl8_color_mode color_mode;
pxl8_gfx* gfx;
pxl8_resolution resolution;
pxl8_script* script;
i32 frame_count;

View file

@ -14,6 +14,19 @@
#include "pxl8_sys.h"
#include "pxl8_types.h"
typedef struct pxl8_palette_cycle {
bool active;
u8 end_index;
f32 speed;
u8 start_index;
f32 timer;
} pxl8_palette_cycle;
typedef struct pxl8_effects {
pxl8_palette_cycle palette_cycles[8];
f32 time;
} pxl8_effects;
typedef struct pxl8_sprite_cache_entry {
char path[256];
u32 sprite_id;
@ -29,7 +42,7 @@ struct pxl8_gfx {
u32 sprite_cache_capacity;
u32 sprite_cache_count;
pxl8_color_mode color_mode;
pxl8_pixel_mode pixel_mode;
u8* framebuffer;
i32 framebuffer_height;
i32 framebuffer_width;
@ -40,6 +53,8 @@ struct pxl8_gfx {
pxl8_viewport viewport;
pxl8_effects effects;
bool backface_culling;
pxl8_mat4 model;
pxl8_mat4 projection;
@ -66,17 +81,17 @@ pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) {
return bounds;
}
pxl8_color_mode pxl8_gfx_get_color_mode(pxl8_gfx* gfx) {
return gfx ? gfx->color_mode : PXL8_COLOR_MODE_FAMI;
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx) {
return gfx ? gfx->pixel_mode : PXL8_PIXEL_INDEXED;
}
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx) {
if (!gfx || gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) return NULL;
if (!gfx || gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return NULL;
return gfx->framebuffer;
}
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx) {
if (!gfx || gfx->color_mode != PXL8_COLOR_MODE_HICOLOR) return NULL;
if (!gfx || gfx->pixel_mode != PXL8_PIXEL_HICOLOR) return NULL;
return (u16*)gfx->framebuffer;
}
@ -95,7 +110,7 @@ u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx) {
pxl8_gfx* pxl8_gfx_create(
const pxl8_hal* hal,
void* platform_data,
pxl8_color_mode mode,
pxl8_pixel_mode mode,
pxl8_resolution resolution
) {
pxl8_gfx* gfx = (pxl8_gfx*)calloc(1, sizeof(pxl8_gfx));
@ -107,7 +122,7 @@ pxl8_gfx* pxl8_gfx_create(
gfx->hal = hal;
gfx->platform_data = platform_data;
gfx->color_mode = mode;
gfx->pixel_mode = mode;
pxl8_size size = pxl8_get_resolution_dimensions(resolution);
gfx->framebuffer_width = size.w;
gfx->framebuffer_height = size.h;
@ -118,7 +133,7 @@ pxl8_gfx* pxl8_gfx_create(
return NULL;
}
i32 bytes_per_pixel = pxl8_bytes_per_pixel(gfx->color_mode);
i32 bytes_per_pixel = pxl8_bytes_per_pixel(gfx->pixel_mode);
i32 fb_size = gfx->framebuffer_width * gfx->framebuffer_height * bytes_per_pixel;
gfx->framebuffer = (u8*)calloc(1, fb_size);
if (!gfx->framebuffer) {
@ -127,7 +142,7 @@ pxl8_gfx* pxl8_gfx_create(
return NULL;
}
gfx->palette_size = pxl8_get_palette_size(mode);
gfx->palette_size = (mode == PXL8_PIXEL_HICOLOR) ? 0 : PXL8_MAX_PALETTE_SIZE;
if (gfx->palette_size > 0) {
gfx->palette = (u32*)calloc(gfx->palette_size, sizeof(u32));
if (!gfx->palette) {
@ -136,8 +151,8 @@ pxl8_gfx* pxl8_gfx_create(
return NULL;
}
for (u32 i = 0; i < 256 && i < gfx->palette_size; i++) {
u8 gray = (u8)(i * 255 / 255);
for (u32 i = 0; i < gfx->palette_size; i++) {
u8 gray = (u8)i;
gfx->palette[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray;
}
}
@ -177,7 +192,7 @@ void pxl8_gfx_destroy(pxl8_gfx* gfx) {
static pxl8_result pxl8_gfx_ensure_atlas(pxl8_gfx* gfx) {
if (gfx->atlas) return PXL8_OK;
gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->color_mode);
gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->pixel_mode);
return gfx->atlas ? PXL8_OK : PXL8_ERROR_OUT_OF_MEMORY;
}
@ -187,7 +202,7 @@ pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width,
pxl8_result result = pxl8_gfx_ensure_atlas(gfx);
if (result != PXL8_OK) return result;
u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->color_mode);
u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->pixel_mode);
if (texture_id == UINT32_MAX) {
pxl8_error("Texture doesn't fit in atlas");
return PXL8_ERROR_INVALID_SIZE;
@ -234,7 +249,7 @@ pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
ase_file.frames[0].pixels,
ase_file.header.width,
ase_file.header.height,
gfx->color_mode
gfx->pixel_mode
);
pxl8_ase_destroy(&ase_file);
@ -273,7 +288,7 @@ pxl8_atlas* pxl8_gfx_get_atlas(pxl8_gfx* gfx) {
pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path) {
if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) return PXL8_OK;
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return PXL8_OK;
pxl8_debug("Loading palette from: %s", path);
@ -316,7 +331,7 @@ void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->atlas) return;
if (gfx->hal && gfx->hal->upload_atlas) {
gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->color_mode);
gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->pixel_mode);
pxl8_atlas_mark_clean(gfx->atlas);
}
}
@ -330,7 +345,7 @@ void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) {
gfx->framebuffer_width,
gfx->framebuffer_height,
gfx->palette,
gfx->color_mode
gfx->pixel_mode
);
}
@ -364,7 +379,7 @@ void pxl8_clear(pxl8_gfx* gfx, u32 color) {
i32 size = gfx->framebuffer_width * gfx->framebuffer_height;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
u16* fb16 = (u16*)gfx->framebuffer;
u16 color16 = pxl8_rgba32_to_rgb565(color);
for (i32 i = 0; i < size; i++) {
@ -380,7 +395,7 @@ void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return;
i32 idx = y * gfx->framebuffer_width + x;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color);
} else {
gfx->framebuffer[idx] = color & 0xFF;
@ -392,7 +407,7 @@ u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0;
i32 idx = y * gfx->framebuffer_width + x;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
return pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx]);
} else {
return gfx->framebuffer[idx];
@ -436,7 +451,7 @@ void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
i32 idx = y * gfx->framebuffer_width + x;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color);
} else {
gfx->framebuffer[idx] = color & 0xFF;
@ -534,7 +549,7 @@ void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) {
if (pixel_bit) {
i32 fb_idx = py * gfx->framebuffer_width + px;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
((u16*)gfx->framebuffer)[fb_idx] = pxl8_rgba32_to_rgb565(color);
} else {
gfx->framebuffer[fb_idx] = (u8)color;
@ -574,7 +589,7 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas);
if (is_1to1_scale && is_unclipped) {
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
const u16* sprite_data = (const u16*)atlas_pixels + entry->y * atlas_width + entry->x;
pxl8_blit_hicolor(
(u16*)gfx->framebuffer,
@ -601,7 +616,7 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
i32 src_idx = src_y * atlas_width + src_x;
i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px);
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
u16 pixel = ((const u16*)atlas_pixels)[src_idx];
if (pixel != 0) {
((u16*)gfx->framebuffer)[dest_idx] = pixel;
@ -709,7 +724,7 @@ void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32
}
}
void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) {
static void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) {
if (!gfx || !effects) return;
effects->time += dt;
@ -727,6 +742,45 @@ void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt) {
}
}
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt) {
if (!gfx) return;
pxl8_gfx_process_effects(gfx, &gfx->effects, dt);
}
i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed) {
if (!gfx) return -1;
if (start_index >= end_index) return -1;
for (i32 i = 0; i < 8; i++) {
if (!gfx->effects.palette_cycles[i].active) {
gfx->effects.palette_cycles[i].active = true;
gfx->effects.palette_cycles[i].start_index = start_index;
gfx->effects.palette_cycles[i].end_index = end_index;
gfx->effects.palette_cycles[i].speed = speed;
gfx->effects.palette_cycles[i].timer = 0.0f;
return i;
}
}
return -1;
}
void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id) {
if (!gfx || cycle_id < 0 || cycle_id >= 8) return;
gfx->effects.palette_cycles[cycle_id].active = false;
}
void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed) {
if (!gfx || cycle_id < 0 || cycle_id >= 8) return;
gfx->effects.palette_cycles[cycle_id].speed = speed;
}
void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx) {
if (!gfx) return;
for (i32 i = 0; i < 8; i++) {
gfx->effects.palette_cycles[i].active = false;
}
}
static bool pxl8_3d_init_zbuffer(pxl8_gfx* gfx) {
if (gfx->zbuffer) return true;
@ -863,7 +917,7 @@ static inline void pxl8_fill_scanline(pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32
i32 zbuf_offset = y * gfx->zbuffer_width;
i32 fb_offset = y * gfx->framebuffer_width;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) {
u16* fb = (u16*)gfx->framebuffer;
u16 color16 = pxl8_rgba32_to_rgb565(color);
if (width == 0) {
@ -939,7 +993,7 @@ static inline void pxl8_fill_scanline_textured(
i32 tex_mask = tex_w - 1;
i32 atlas_x_base = entry->x;
i32 atlas_y_base = entry->y;
bool is_hicolor = gfx->color_mode == PXL8_COLOR_MODE_HICOLOR;
bool is_hicolor = gfx->pixel_mode == PXL8_PIXEL_HICOLOR;
bool affine = gfx->affine_textures;
i32 span = xe - xs;

View file

@ -7,26 +7,6 @@
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_gfx pxl8_gfx;
typedef enum pxl8_blend_mode {
PXL8_BLEND_ADD,
PXL8_BLEND_ALPHA,
PXL8_BLEND_MULTIPLY,
PXL8_BLEND_NONE
} pxl8_blend_mode;
typedef struct pxl8_palette_cycle {
bool active;
u8 end_index;
f32 speed;
u8 start_index;
f32 timer;
} pxl8_palette_cycle;
typedef struct pxl8_scanline_effect {
bool active;
void (*process)(pxl8_gfx* gfx, i32 line, f32 time);
} pxl8_scanline_effect;
typedef struct pxl8_vertex {
u32 color;
pxl8_vec3 normal;
@ -34,12 +14,6 @@ typedef struct pxl8_vertex {
f32 u, v;
} pxl8_vertex;
typedef struct pxl8_effects {
pxl8_palette_cycle palette_cycles[8];
pxl8_scanline_effect scanline_effects[4];
f32 time;
} pxl8_effects;
typedef struct pxl8_triangle {
pxl8_vertex v[3];
u32 texture_id;
@ -49,11 +23,11 @@ typedef struct pxl8_triangle {
extern "C" {
#endif
pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_color_mode mode, pxl8_resolution resolution);
pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution);
void pxl8_gfx_destroy(pxl8_gfx* gfx);
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx);
pxl8_color_mode pxl8_gfx_get_color_mode(pxl8_gfx* gfx);
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx);
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx);
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx);
@ -67,7 +41,6 @@ pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx);
pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path);
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path);
void pxl8_gfx_present(pxl8_gfx* gfx);
void pxl8_gfx_process_effects(pxl8_gfx* gfx, pxl8_effects* effects, f32 dt);
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom);
void pxl8_gfx_upload_atlas(pxl8_gfx* gfx);
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx);
@ -79,6 +52,12 @@ void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 ta
void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t);
void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors);
i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed);
void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id);
void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed);
void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx);
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt);
void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_clear(pxl8_gfx* gfx, u32 color);

View file

@ -6,7 +6,7 @@ typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_game pxl8_game;
typedef struct pxl8_hal {
void* (*create)(pxl8_color_mode mode, pxl8_resolution res,
void* (*create)(pxl8_pixel_mode mode, pxl8_resolution res,
const char* title, i32 win_w, i32 win_h);
void (*destroy)(void* platform_data);
@ -17,8 +17,8 @@ typedef struct pxl8_hal {
void (*set_cursor)(void* platform_data, pxl8_cursor cursor);
void (*set_relative_mouse_mode)(void* platform_data, bool enabled);
void (*upload_atlas)(void* platform_data, const pxl8_atlas* atlas,
const u32* palette, pxl8_color_mode mode);
const u32* palette, pxl8_pixel_mode mode);
void (*upload_framebuffer)(void* platform_data, const u8* fb,
i32 w, i32 h, const u32* palette,
pxl8_color_mode mode);
pxl8_pixel_mode mode);
} pxl8_hal;

View file

@ -3,6 +3,7 @@
#include <stdlib.h>
#include <string.h>
#include "pxl8_cart.h"
#include "pxl8_types.h"
static inline char pxl8_to_lower(char c) {
@ -12,6 +13,23 @@ static inline char pxl8_to_lower(char c) {
pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) {
if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER;
pxl8_cart* cart = pxl8_cart_current();
if (cart && pxl8_cart_is_packed(cart)) {
u8* data = NULL;
u32 cart_size = 0;
pxl8_result result = pxl8_cart_read_file(cart, path, &data, &cart_size);
if (result == PXL8_OK) {
*content = realloc(data, cart_size + 1);
if (!*content) {
pxl8_cart_free_file(data);
return PXL8_ERROR_OUT_OF_MEMORY;
}
(*content)[cart_size] = '\0';
*size = cart_size;
return PXL8_OK;
}
}
FILE* file = fopen(path, "rb");
if (!file) {
return PXL8_ERROR_FILE_NOT_FOUND;

272
src/pxl8_save.c Normal file
View file

@ -0,0 +1,272 @@
#include "pxl8_save.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#ifdef _WIN32
#include <direct.h>
#include <shlobj.h>
#define PATH_SEP '\\'
#else
#include <unistd.h>
#include <pwd.h>
#define PATH_SEP '/'
#endif
#include "pxl8_macros.h"
typedef struct {
u32 magic;
u32 version;
u32 size;
u32 checksum;
} pxl8_save_header;
struct pxl8_save {
char directory[PXL8_SAVE_MAX_PATH];
u32 magic;
u32 version;
};
static u32 pxl8_save_checksum(const u8* data, u32 size) {
u32 hash = 2166136261u;
for (u32 i = 0; i < size; i++) {
hash ^= data[i];
hash *= 16777619u;
}
return hash;
}
static void pxl8_save_get_slot_path(pxl8_save* save, u8 slot, char* path, size_t path_size) {
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
snprintf(path, path_size, "%s%chotreload.sav", save->directory, PATH_SEP);
} else {
snprintf(path, path_size, "%s%csave%d.sav", save->directory, PATH_SEP, slot);
}
}
static pxl8_result pxl8_save_ensure_directory(const char* path) {
struct stat st;
if (stat(path, &st) == 0) {
return S_ISDIR(st.st_mode) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE;
}
#ifdef _WIN32
if (_mkdir(path) != 0 && errno != EEXIST) {
#else
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
#endif
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version) {
if (!game_name) return NULL;
pxl8_save* save = (pxl8_save*)calloc(1, sizeof(pxl8_save));
if (!save) return NULL;
save->magic = magic;
save->version = version;
char base_dir[PXL8_SAVE_MAX_PATH];
#ifdef _WIN32
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, base_dir))) {
snprintf(save->directory, sizeof(save->directory),
"%s%cpxl8%c%s", base_dir, PATH_SEP, PATH_SEP, game_name);
} else {
free(save);
return NULL;
}
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) {
free(save);
return NULL;
}
snprintf(base_dir, sizeof(base_dir), "%s/.local/share", home);
pxl8_save_ensure_directory(base_dir);
snprintf(base_dir, sizeof(base_dir), "%s/.local/share/pxl8", home);
pxl8_save_ensure_directory(base_dir);
snprintf(save->directory, sizeof(save->directory),
"%s/.local/share/pxl8/%s", home, game_name);
#endif
if (pxl8_save_ensure_directory(save->directory) != PXL8_OK) {
free(save);
return NULL;
}
pxl8_info("Save system initialized: %s", save->directory);
return save;
}
void pxl8_save_destroy(pxl8_save* save) {
if (save) {
free(save);
}
}
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data || size == 0) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "wb");
if (!file) {
pxl8_error("Failed to open save file for writing: %s", path);
return PXL8_ERROR_SYSTEM_FAILURE;
}
pxl8_save_header header = {
.magic = save->magic,
.version = save->version,
.size = size,
.checksum = pxl8_save_checksum(data, size)
};
bool success = true;
if (fwrite(&header, sizeof(header), 1, file) != 1) success = false;
if (success && fwrite(data, 1, size, file) != size) success = false;
fclose(file);
if (!success) {
pxl8_error("Failed to write save data");
return PXL8_ERROR_SYSTEM_FAILURE;
}
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state saved (%u bytes)", size);
} else {
pxl8_info("Game saved to slot %d (%u bytes)", slot, size);
}
return PXL8_OK;
}
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (!data_out || !size_out) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
*data_out = NULL;
*size_out = 0;
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
FILE* file = fopen(path, "rb");
if (!file) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
pxl8_save_header header;
if (fread(&header, sizeof(header), 1, file) != 1) {
fclose(file);
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.magic != save->magic) {
fclose(file);
pxl8_error("Invalid save file magic");
return PXL8_ERROR_INVALID_FORMAT;
}
if (header.version > save->version) {
fclose(file);
pxl8_error("Save file version too new: %u", header.version);
return PXL8_ERROR_INVALID_FORMAT;
}
u8* data = (u8*)malloc(header.size);
if (!data) {
fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (fread(data, 1, header.size, file) != header.size) {
free(data);
fclose(file);
return PXL8_ERROR_SYSTEM_FAILURE;
}
fclose(file);
u32 checksum = pxl8_save_checksum(data, header.size);
if (checksum != header.checksum) {
free(data);
pxl8_error("Save file checksum mismatch");
return PXL8_ERROR_INVALID_FORMAT;
}
*data_out = data;
*size_out = header.size;
if (slot == PXL8_SAVE_HOTRELOAD_SLOT) {
pxl8_debug("Hot reload state loaded (%u bytes)", header.size);
} else {
pxl8_info("Game loaded from slot %d (%u bytes)", slot, header.size);
}
return PXL8_OK;
}
void pxl8_save_free(u8* data) {
if (data) {
free(data);
}
}
bool pxl8_save_exists(pxl8_save* save, u8 slot) {
if (!save) return false;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return false;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
struct stat st;
return stat(path, &st) == 0;
}
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot) {
if (!save) return PXL8_ERROR_NULL_POINTER;
if (slot >= PXL8_SAVE_MAX_SLOTS && slot != PXL8_SAVE_HOTRELOAD_SLOT) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
char path[PXL8_SAVE_MAX_PATH];
pxl8_save_get_slot_path(save, slot, path, sizeof(path));
if (remove(path) != 0 && errno != ENOENT) {
return PXL8_ERROR_SYSTEM_FAILURE;
}
return PXL8_OK;
}
const char* pxl8_save_get_directory(pxl8_save* save) {
return save ? save->directory : NULL;
}

27
src/pxl8_save.h Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_SAVE_MAX_SLOTS 10
#define PXL8_SAVE_HOTRELOAD_SLOT 255
#define PXL8_SAVE_MAX_PATH 512
typedef struct pxl8_save pxl8_save;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version);
void pxl8_save_destroy(pxl8_save* save);
pxl8_result pxl8_save_delete(pxl8_save* save, u8 slot);
bool pxl8_save_exists(pxl8_save* save, u8 slot);
void pxl8_save_free(u8* data);
const char* pxl8_save_get_directory(pxl8_save* save);
pxl8_result pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out);
pxl8_result pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size);
#ifdef __cplusplus
}
#endif

View file

@ -12,6 +12,7 @@
#include <lua.h>
#include <lualib.h>
#include "pxl8_cart.h"
#include "pxl8_embed.h"
#include "pxl8_macros.h"
#include "pxl8_gui.h"
@ -24,10 +25,91 @@ struct pxl8_script {
char main_path[PXL8_MAX_PATH];
char watch_dir[PXL8_MAX_PATH];
time_t latest_mod_time;
bool repl_mode;
};
#define PXL8_MAX_REPL_COMMAND_SIZE 4096
static int pxl8_cart_loader(lua_State* L) {
const char* found_path = lua_tostring(L, lua_upvalueindex(1));
const char* code = lua_tostring(L, lua_upvalueindex(2));
size_t code_len = lua_objlen(L, lua_upvalueindex(2));
bool is_fennel = lua_toboolean(L, lua_upvalueindex(3));
if (is_fennel) {
lua_getglobal(L, "fennel");
lua_getfield(L, -1, "eval");
lua_pushlstring(L, code, code_len);
lua_createtable(L, 0, 1);
lua_pushstring(L, found_path);
lua_setfield(L, -2, "filename");
if (lua_pcall(L, 2, 1, 0) != 0) {
lua_remove(L, -2);
return lua_error(L);
}
lua_remove(L, -2);
} else {
if (luaL_loadbuffer(L, code, code_len, found_path) != 0) {
return lua_error(L);
}
if (lua_pcall(L, 0, 1, 0) != 0) {
return lua_error(L);
}
}
return 1;
}
static int pxl8_cart_searcher(lua_State* L) {
const char* modname = luaL_checkstring(L, 1);
pxl8_cart* cart = pxl8_cart_current();
if (!cart || !pxl8_cart_is_packed(cart)) {
lua_pushstring(L, "\n\tno packed cart mounted");
return 1;
}
char path[512];
size_t len = strlen(modname);
size_t j = 0;
for (size_t i = 0; i < len && j < sizeof(path) - 5; i++) {
if (modname[i] == '.') {
path[j++] = '/';
} else {
path[j++] = modname[i];
}
}
path[j] = '\0';
const char* extensions[] = {".fnl", ".lua", "/init.fnl", "/init.lua"};
u8* data = NULL;
u32 size = 0;
char found_path[512];
bool is_fennel = false;
for (int i = 0; i < 4; i++) {
snprintf(found_path, sizeof(found_path), "%s%s", path, extensions[i]);
if (pxl8_cart_read_file(cart, found_path, &data, &size) == PXL8_OK) {
is_fennel = (i == 0 || i == 2);
break;
}
}
if (!data) {
lua_pushfstring(L, "\n\tno file '%s' in cart", path);
return 1;
}
lua_pushstring(L, found_path);
lua_pushlstring(L, (const char*)data, size);
lua_pushboolean(L, is_fennel);
pxl8_cart_free_file(data);
lua_pushcclosure(L, pxl8_cart_loader, 3);
return 1;
}
static void pxl8_script_repl_promote_locals(const char* input, char* output, size_t output_size) {
size_t i = 0;
size_t j = 0;
@ -100,7 +182,13 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_sprite(pxl8_gfx* ctx, i32 id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);\n"
"void pxl8_text(pxl8_gfx* ctx, const char* str, i32 x, i32 y, u32 color);\n"
"void pxl8_gfx_color_ramp(pxl8_gfx* ctx, u8 start, u8 count, u32 from_color, u32 to_color);\n"
"void pxl8_gfx_cycle_palette(pxl8_gfx* ctx, u8 start, u8 count, i32 step);\n"
"void pxl8_gfx_fade_palette(pxl8_gfx* ctx, u8 start, u8 count, f32 amount, u32 target_color);\n"
"i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* ctx, u8 start_index, u8 end_index, f32 speed);\n"
"void pxl8_gfx_remove_palette_cycle(pxl8_gfx* ctx, i32 cycle_id);\n"
"void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* ctx, i32 cycle_id, f32 speed);\n"
"void pxl8_gfx_clear_palette_cycles(pxl8_gfx* ctx);\n"
"void pxl8_gfx_update(pxl8_gfx* ctx, f32 dt);\n"
"i32 pxl8_gfx_load_palette(pxl8_gfx* ctx, const char* filepath);\n"
"i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath, u32* sprite_id);\n"
"i32 pxl8_gfx_create_texture(pxl8_gfx* ctx, const u8* pixels, u32 width, u32 height);\n"
@ -323,7 +411,17 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_gui_window(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, const char* title);\n"
"void pxl8_gui_label(pxl8_gfx* gfx, i32 x, i32 y, const char* text, u8 color);\n"
"bool pxl8_gui_is_hovering(const pxl8_gui_state* state);\n"
"void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y);\n";
"void pxl8_gui_get_cursor_pos(const pxl8_gui_state* state, i32* x, i32* y);\n"
"\n"
"typedef struct pxl8_save pxl8_save;\n"
"pxl8_save* pxl8_save_create(const char* game_name, u32 magic, u32 version);\n"
"void pxl8_save_destroy(pxl8_save* save);\n"
"i32 pxl8_save_write(pxl8_save* save, u8 slot, const u8* data, u32 size);\n"
"i32 pxl8_save_read(pxl8_save* save, u8 slot, u8** data_out, u32* size_out);\n"
"void pxl8_save_free(u8* data);\n"
"bool pxl8_save_exists(pxl8_save* save, u8 slot);\n"
"i32 pxl8_save_delete(pxl8_save* save, u8 slot);\n"
"const char* pxl8_save_get_directory(pxl8_save* save);\n";
void pxl8_lua_info(const char* msg) {
pxl8_info("%s", msg);
@ -387,9 +485,10 @@ static void pxl8_install_embed_searcher(lua_State* L) {
lua_pop(L, 2);
}
pxl8_script* pxl8_script_create(void) {
pxl8_script* pxl8_script_create(bool repl_mode) {
pxl8_script* script = (pxl8_script*)calloc(1, sizeof(pxl8_script));
if (!script) return NULL;
script->repl_mode = repl_mode;
script->L = luaL_newstate();
if (!script->L) {
@ -427,10 +526,42 @@ pxl8_script* pxl8_script_create(void) {
lua_getglobal(script->L, "fennel");
lua_getfield(script->L, -1, "install");
if (lua_isfunction(script->L, -1)) {
if (lua_pcall(script->L, 0, 0, 0) != 0) {
// Pass options table for fennel.install()
// lambdaAsFn: removes arity checking overhead from fn/lambda
// correlate: aligns Lua line numbers with Fennel source
// useMetadata: enables docstrings (only in REPL mode for perf)
lua_newtable(script->L);
lua_pushboolean(script->L, 1);
lua_setfield(script->L, -2, "lambdaAsFn");
lua_pushboolean(script->L, 1);
lua_setfield(script->L, -2, "correlate");
if (script->repl_mode) {
lua_pushboolean(script->L, 1);
lua_setfield(script->L, -2, "useMetadata");
lua_pushboolean(script->L, 1);
lua_setfield(script->L, -2, "assertAsRepl");
}
if (lua_pcall(script->L, 1, 0, 0) != 0) {
pxl8_warn("Failed to install fennel searcher: %s", lua_tostring(script->L, -1));
lua_pop(script->L, 1);
}
lua_getglobal(script->L, "package");
lua_getfield(script->L, -1, "loaders");
if (lua_isnil(script->L, -1)) {
lua_pop(script->L, 1);
lua_getfield(script->L, -1, "searchers");
}
if (lua_istable(script->L, -1)) {
int n = (int)lua_objlen(script->L, -1);
for (int i = n; i >= 2; i--) {
lua_rawgeti(script->L, -1, i);
lua_rawseti(script->L, -2, i + 1);
}
lua_pushcfunction(script->L, pxl8_cart_searcher);
lua_rawseti(script->L, -2, 2);
}
lua_pop(script->L, 2);
} else {
lua_pop(script->L, 1);
}
@ -574,10 +705,38 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam
return PXL8_ERROR_SCRIPT_ERROR;
}
pxl8_cart* cart = pxl8_cart_current();
pxl8_result result = PXL8_OK;
if (cart && pxl8_cart_is_packed(cart)) {
u8* data = NULL;
u32 size = 0;
if (pxl8_cart_read_file(cart, basename, &data, &size) != PXL8_OK) {
pxl8_script_set_error(script, "Failed to read script from cart");
lua_pop(script->L, 1);
return PXL8_ERROR_FILE_NOT_FOUND;
}
lua_getfield(script->L, -1, "eval");
lua_pushlstring(script->L, (const char*)data, size);
lua_createtable(script->L, 0, 1);
lua_pushstring(script->L, basename);
lua_setfield(script->L, -2, "filename");
pxl8_cart_free_file(data);
if (lua_pcall(script->L, 2, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_pop(script->L, 1);
result = PXL8_ERROR_SCRIPT_ERROR;
} else {
script->last_error[0] = '\0';
}
} else {
lua_getfield(script->L, -1, "dofile");
lua_pushstring(script->L, basename);
pxl8_result result = PXL8_OK;
if (lua_pcall(script->L, 1, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_pop(script->L, 1);
@ -585,6 +744,7 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam
} else {
script->last_error[0] = '\0';
}
}
lua_pop(script->L, 1);

View file

@ -11,7 +11,7 @@ typedef struct pxl8_script_repl_command pxl8_script_repl_command;
extern "C" {
#endif
pxl8_script* pxl8_script_create(void);
pxl8_script* pxl8_script_create(bool repl_mode);
void pxl8_script_destroy(pxl8_script* script);
const char* pxl8_script_get_last_error(pxl8_script* script);

View file

@ -23,7 +23,7 @@ typedef struct pxl8_sdl3_context {
u32 atlas_height;
} pxl8_sdl3_context;
static void* sdl3_create(pxl8_color_mode mode, pxl8_resolution resolution,
static void* sdl3_create(pxl8_pixel_mode mode, pxl8_resolution resolution,
const char* title, i32 win_w, i32 win_h) {
(void)mode;
@ -120,7 +120,7 @@ static void sdl3_present(void* platform_data) {
static void sdl3_upload_framebuffer(void* platform_data, const u8* fb,
i32 w, i32 h, const u32* palette,
pxl8_color_mode mode) {
pxl8_pixel_mode mode) {
if (!platform_data || !fb) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
@ -133,16 +133,15 @@ static void sdl3_upload_framebuffer(void* platform_data, const u8* fb,
ctx->rgba_buffer_size = needed_size;
}
if (mode == PXL8_COLOR_MODE_HICOLOR) {
if (mode == PXL8_PIXEL_HICOLOR) {
const u16* fb16 = (const u16*)fb;
for (i32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = pxl8_rgb565_to_rgba32(fb16[i]);
}
} else {
u32 palette_size = pxl8_get_palette_size(mode);
for (i32 i = 0; i < w * h; i++) {
u8 index = fb[i];
ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0xFF000000;
ctx->rgba_buffer[i] = palette[index];
}
}
@ -150,7 +149,7 @@ static void sdl3_upload_framebuffer(void* platform_data, const u8* fb,
}
static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas,
const u32* palette, pxl8_color_mode mode) {
const u32* palette, pxl8_pixel_mode mode) {
if (!platform_data || !atlas) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
@ -195,17 +194,16 @@ static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas,
ctx->rgba_buffer_size = needed_size;
}
if (mode == PXL8_COLOR_MODE_HICOLOR) {
if (mode == PXL8_PIXEL_HICOLOR) {
const u16* atlas16 = (const u16*)atlas_pixels;
for (u32 i = 0; i < atlas_w * atlas_h; i++) {
u16 pixel = atlas16[i];
ctx->rgba_buffer[i] = (pixel != 0) ? pxl8_rgb565_to_rgba32(pixel) : 0x00000000;
}
} else {
u32 palette_size = pxl8_get_palette_size(mode);
for (u32 i = 0; i < atlas_w * atlas_h; i++) {
u8 index = atlas_pixels[i];
ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0x00000000;
ctx->rgba_buffer[i] = palette[index];
}
}
@ -331,11 +329,11 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
i32 window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
pxl8_resolution resolution = pxl8_get_resolution(sys);
pxl8_size render_size = pxl8_get_resolution_dimensions(resolution);
i32 render_w = pxl8_gfx_get_width(gfx);
i32 render_h = pxl8_gfx_get_height(gfx);
pxl8_bounds window_bounds = {0, 0, window_width, window_height};
pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_size.w, render_size.h);
pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_w, render_h);
input->mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale);
input->mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale);

View file

@ -17,8 +17,6 @@ void pxl8_destroy(pxl8* sys);
f32 pxl8_get_fps(const pxl8* sys);
pxl8_gfx* pxl8_get_gfx(const pxl8* sys);
pxl8_input_state* pxl8_get_input(const pxl8* sys);
u32 pxl8_get_palette_size(pxl8_color_mode mode);
pxl8_resolution pxl8_get_resolution(const pxl8* sys);
pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution);
bool pxl8_is_running(const pxl8* sys);

View file

@ -15,7 +15,7 @@ struct pxl8_tilesheet {
u32 tiles_per_row;
u32 total_tiles;
u32 width;
pxl8_color_mode color_mode;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
u32 animation_count;
@ -545,7 +545,7 @@ pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u
tilemap->tilesheet->height = tilesheet_height;
tilemap->tilesheet->tiles_per_row = tiles_per_row;
tilemap->tilesheet->total_tiles = tileset->tile_count;
tilemap->tilesheet->color_mode = PXL8_COLOR_MODE_MEGA;
tilemap->tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
if (tilemap->tilesheet->tile_valid) free(tilemap->tilesheet->tile_valid);
tilemap->tilesheet->tile_valid = calloc(tileset->tile_count + 1, sizeof(bool));

View file

@ -18,7 +18,7 @@ struct pxl8_tilesheet {
u32 total_tiles;
u32 width;
pxl8_color_mode color_mode;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
@ -104,13 +104,13 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath,
tilesheet->height = height;
tilesheet->tiles_per_row = width / tilesheet->tile_size;
tilesheet->total_tiles = (width / tilesheet->tile_size) * (height / tilesheet->tile_size);
tilesheet->color_mode = pxl8_gfx_get_color_mode(gfx);
tilesheet->pixel_mode = pxl8_gfx_get_pixel_mode(gfx);
u32 pixel_count = width * height;
u16 ase_depth = ase_file.header.color_depth;
bool gfx_hicolor = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR);
bool gfx_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->color_mode);
size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->pixel_mode);
tilesheet->data = malloc(data_size);
if (!tilesheet->data) {
pxl8_ase_destroy(&ase_file);
@ -136,7 +136,7 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath,
}
} else if (ase_depth == 8 && gfx_hicolor) {
pxl8_warn("Indexed ASE with hicolor gfx - storing as indexed");
tilesheet->color_mode = PXL8_COLOR_MODE_FAMI;
tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
u8* new_data = realloc(tilesheet->data, pixel_count);
if (!new_data) {
free(tilesheet->data);
@ -164,7 +164,7 @@ pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath,
}
u32 valid_tiles = 0;
bool is_hicolor = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR);
bool is_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
for (u32 tile_id = 1; tile_id <= tilesheet->total_tiles; tile_id++) {
u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size;
@ -310,7 +310,7 @@ pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_i
u32 tile_x = (tile_id - 1) % tilesheet->tiles_per_row;
u32 tile_y = (tile_id - 1) / tilesheet->tiles_per_row;
u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->color_mode);
u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->pixel_mode);
for (u32 py = 0; py < tilesheet->tile_size; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {

View file

@ -168,8 +168,8 @@ void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx) {
i32 block_size = (i32)(max_block_size * progress);
if (block_size < 1) block_size = 1;
pxl8_color_mode mode = pxl8_gfx_get_color_mode(gfx);
bool has_fb = (mode == PXL8_COLOR_MODE_HICOLOR)
pxl8_pixel_mode mode = pxl8_gfx_get_pixel_mode(gfx);
bool has_fb = (mode == PXL8_PIXEL_HICOLOR)
? (pxl8_gfx_get_framebuffer_hicolor(gfx) != NULL)
: (pxl8_gfx_get_framebuffer_indexed(gfx) != NULL);
if (!has_fb) break;

View file

@ -28,13 +28,10 @@ typedef __int128_t i128;
typedef __uint128_t u128;
#endif
typedef enum pxl8_color_mode {
PXL8_COLOR_MODE_FAMI,
PXL8_COLOR_MODE_GBA,
PXL8_COLOR_MODE_HICOLOR,
PXL8_COLOR_MODE_MEGA,
PXL8_COLOR_MODE_SNES
} pxl8_color_mode;
typedef enum pxl8_pixel_mode {
PXL8_PIXEL_INDEXED,
PXL8_PIXEL_HICOLOR
} pxl8_pixel_mode;
typedef enum pxl8_cursor {
PXL8_CURSOR_ARROW,