#include "pxl8_cart.h" #include #include #include #include #include #include #include "pxl8_log.h" #define PXL8_CART_MAGIC 0x43585850 #define PXL8_CART_VERSION 1 #define PXL8_CART_TRAILER_MAGIC 0x544E4250 typedef struct { u32 magic; u16 version; u16 flags; u32 file_count; u32 toc_size; } pxl8_cart_header; typedef struct { u32 offset; u32 size; u16 path_len; } pxl8_cart_entry; typedef struct { u32 magic; u32 cart_offset; u32 cart_size; } pxl8_cart_trailer; typedef struct { char* path; u32 offset; u32 size; } pxl8_cart_file; struct pxl8_cart { u8* data; u32 data_size; pxl8_cart_file* files; u32 file_count; char* base_path; char* title; pxl8_resolution resolution; pxl8_size window_size; pxl8_pixel_mode pixel_mode; bool is_folder; bool is_mounted; }; static pxl8_cart* pxl8_current_cart = NULL; static char* pxl8_original_cwd = NULL; static bool is_directory(const char* path) { struct stat st; return stat(path, &st) == 0 && S_ISDIR(st.st_mode); } static bool is_pxc_file(const char* path) { size_t len = strlen(path); return len > 4 && strcmp(path + len - 4, ".pxc") == 0; } static bool has_main_script(const char* base_path) { char path[512]; snprintf(path, sizeof(path), "%s/main.fnl", base_path); if (access(path, F_OK) == 0) return true; snprintf(path, sizeof(path), "%s/main.lua", base_path); return access(path, F_OK) == 0; } static void collect_files_recursive(const char* dir_path, const char* prefix, char*** paths, u32* count, u32* capacity) { DIR* dir = opendir(dir_path); if (!dir) return; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char full_path[1024]; char rel_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); snprintf(rel_path, sizeof(rel_path), "%s%s", prefix, entry->d_name); struct stat st; if (stat(full_path, &st) == 0) { if (S_ISDIR(st.st_mode)) { char new_prefix[1025]; snprintf(new_prefix, sizeof(new_prefix), "%s/", rel_path); collect_files_recursive(full_path, new_prefix, paths, count, capacity); } else { if (*count >= *capacity) { *capacity = (*capacity == 0) ? 64 : (*capacity * 2); *paths = realloc(*paths, *capacity * sizeof(char*)); } (*paths)[(*count)++] = strdup(rel_path); } } } closedir(dir); } static pxl8_cart_file* find_file(const pxl8_cart* cart, const char* path) { if (!cart || !cart->files) return NULL; for (u32 i = 0; i < cart->file_count; i++) { if (strcmp(cart->files[i].path, path) == 0) { return &cart->files[i]; } } return NULL; } static pxl8_result load_packed_cart(pxl8_cart* cart, const u8* data, u32 size) { if (size < sizeof(pxl8_cart_header)) return PXL8_ERROR_INVALID_FORMAT; const pxl8_cart_header* header = (const pxl8_cart_header*)data; if (header->magic != PXL8_CART_MAGIC) return PXL8_ERROR_INVALID_FORMAT; if (header->version > PXL8_CART_VERSION) return PXL8_ERROR_INVALID_FORMAT; cart->data = malloc(size); if (!cart->data) return PXL8_ERROR_OUT_OF_MEMORY; memcpy(cart->data, data, size); cart->data_size = size; cart->file_count = header->file_count; cart->files = calloc(cart->file_count, sizeof(pxl8_cart_file)); if (!cart->files) return PXL8_ERROR_OUT_OF_MEMORY; const u8* toc = cart->data + sizeof(pxl8_cart_header); for (u32 i = 0; i < cart->file_count; i++) { const pxl8_cart_entry* entry = (const pxl8_cart_entry*)toc; toc += sizeof(pxl8_cart_entry); cart->files[i].path = malloc(entry->path_len + 1); memcpy(cart->files[i].path, toc, entry->path_len); cart->files[i].path[entry->path_len] = '\0'; toc += entry->path_len; cart->files[i].offset = entry->offset; cart->files[i].size = entry->size; } cart->is_folder = false; return PXL8_OK; } pxl8_cart* pxl8_cart_create(void) { pxl8_cart* cart = calloc(1, sizeof(pxl8_cart)); if (cart) { cart->resolution = PXL8_RESOLUTION_640x360; cart->window_size = (pxl8_size){1280, 720}; cart->pixel_mode = PXL8_PIXEL_INDEXED; } return cart; } pxl8_cart* pxl8_get_cart(void) { return pxl8_current_cart; } void pxl8_cart_destroy(pxl8_cart* cart) { if (!cart) return; pxl8_cart_unload(cart); free(cart); } pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) { if (!cart || !path) return PXL8_ERROR_NULL_POINTER; pxl8_cart_unload(cart); if (is_directory(path)) { cart->base_path = realpath(path, NULL); if (!cart->base_path) { pxl8_error("Failed to resolve cart path: %s", path); return PXL8_ERROR_FILE_NOT_FOUND; } cart->is_folder = true; if (!has_main_script(cart->base_path)) { pxl8_error("No main.fnl or main.lua found in cart: %s", path); return PXL8_ERROR_FILE_NOT_FOUND; } pxl8_info("Loaded cart"); return PXL8_OK; } if (is_pxc_file(path)) { FILE* file = fopen(path, "rb"); if (!file) { pxl8_error("Failed to open cart: %s", path); return PXL8_ERROR_FILE_NOT_FOUND; } fseek(file, 0, SEEK_END); u32 size = (u32)ftell(file); fseek(file, 0, SEEK_SET); u8* data = malloc(size); if (!data || fread(data, 1, size, file) != size) { free(data); fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } fclose(file); pxl8_result result = load_packed_cart(cart, data, size); free(data); if (result == PXL8_OK) { pxl8_info("Loaded cart"); } return result; } pxl8_error("Unknown cart format: %s", path); return PXL8_ERROR_INVALID_FORMAT; } pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path) { if (!cart || !exe_path) return PXL8_ERROR_NULL_POINTER; FILE* file = fopen(exe_path, "rb"); if (!file) return PXL8_ERROR_FILE_NOT_FOUND; fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END); pxl8_cart_trailer trailer; if (fread(&trailer, sizeof(trailer), 1, file) != 1) { fclose(file); return PXL8_ERROR_INVALID_FORMAT; } if (trailer.magic != PXL8_CART_TRAILER_MAGIC) { fclose(file); return PXL8_ERROR_INVALID_FORMAT; } fseek(file, trailer.cart_offset, SEEK_SET); u8* data = malloc(trailer.cart_size); if (!data || fread(data, 1, trailer.cart_size, file) != trailer.cart_size) { free(data); fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } fclose(file); pxl8_result result = load_packed_cart(cart, data, trailer.cart_size); free(data); if (result == PXL8_OK) { pxl8_info("Loaded embedded cart"); } return result; } void pxl8_cart_unload(pxl8_cart* cart) { if (!cart) return; if (cart->title) { pxl8_info("Unloaded cart: %s", cart->title); pxl8_cart_unmount(cart); free(cart->title); cart->title = NULL; } else if (cart->base_path || cart->data) { pxl8_info("Unloaded cart"); pxl8_cart_unmount(cart); } if (cart->files) { for (u32 i = 0; i < cart->file_count; i++) { free(cart->files[i].path); } free(cart->files); cart->files = NULL; } cart->file_count = 0; free(cart->data); cart->data = NULL; cart->data_size = 0; free(cart->base_path); cart->base_path = NULL; cart->is_folder = false; } pxl8_result pxl8_cart_mount(pxl8_cart* cart) { if (!cart) return PXL8_ERROR_NULL_POINTER; if (cart->is_mounted) return PXL8_OK; if (pxl8_current_cart) { pxl8_cart_unmount(pxl8_current_cart); } if (cart->is_folder) { pxl8_original_cwd = getcwd(NULL, 0); if (chdir(cart->base_path) != 0) { pxl8_error("Failed to change to cart directory: %s", cart->base_path); free(pxl8_original_cwd); pxl8_original_cwd = NULL; return PXL8_ERROR_FILE_NOT_FOUND; } } cart->is_mounted = true; pxl8_current_cart = cart; if (cart->title) { pxl8_info("Mounted cart: %s", cart->title); } else { pxl8_info("Mounted cart"); } return PXL8_OK; } void pxl8_cart_unmount(pxl8_cart* cart) { if (!cart || !cart->is_mounted) return; if (pxl8_original_cwd) { chdir(pxl8_original_cwd); free(pxl8_original_cwd); pxl8_original_cwd = NULL; } cart->is_mounted = false; if (pxl8_current_cart == cart) { pxl8_current_cart = NULL; } } const char* pxl8_cart_get_base_path(const pxl8_cart* cart) { return cart ? cart->base_path : NULL; } const char* pxl8_cart_get_title(const pxl8_cart* cart) { return cart ? cart->title : NULL; } void pxl8_cart_set_title(pxl8_cart* cart, const char* title) { if (!cart || !title) return; free(cart->title); cart->title = strdup(title); } pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart) { return cart ? cart->resolution : PXL8_RESOLUTION_640x360; } void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution) { if (cart) cart->resolution = resolution; } pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart) { return cart ? cart->window_size : (pxl8_size){1280, 720}; } void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size) { if (cart) cart->window_size = size; } pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart) { return cart ? cart->pixel_mode : PXL8_PIXEL_INDEXED; } void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode) { if (cart) cart->pixel_mode = mode; } bool pxl8_cart_is_packed(const pxl8_cart* cart) { return cart && !cart->is_folder; } bool pxl8_cart_has_embedded(const char* exe_path) { FILE* file = fopen(exe_path, "rb"); if (!file) return false; fseek(file, -(i32)sizeof(pxl8_cart_trailer), SEEK_END); pxl8_cart_trailer trailer; bool has = (fread(&trailer, sizeof(trailer), 1, file) == 1 && trailer.magic == PXL8_CART_TRAILER_MAGIC); fclose(file); return has; } bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) { if (!cart || !path) return false; if (cart->is_folder) { char full_path[512]; if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false; return access(full_path, F_OK) == 0; } return find_file(cart, path) != NULL; } bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) { if (!cart || !relative_path || !out_path || out_size == 0) return false; if (cart->is_folder && cart->base_path) { i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path); return written >= 0 && (size_t)written < out_size; } i32 written = snprintf(out_path, out_size, "%s", relative_path); return written >= 0 && (size_t)written < out_size; } pxl8_result pxl8_cart_read_file(const pxl8_cart* cart, const char* path, u8** data_out, u32* size_out) { if (!cart || !path || !data_out || !size_out) return PXL8_ERROR_NULL_POINTER; if (cart->is_folder) { char full_path[512]; if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) { return PXL8_ERROR_FILE_NOT_FOUND; } FILE* file = fopen(full_path, "rb"); if (!file) return PXL8_ERROR_FILE_NOT_FOUND; fseek(file, 0, SEEK_END); *size_out = (u32)ftell(file); fseek(file, 0, SEEK_SET); *data_out = malloc(*size_out); if (!*data_out || fread(*data_out, 1, *size_out, file) != *size_out) { free(*data_out); *data_out = NULL; fclose(file); return PXL8_ERROR_SYSTEM_FAILURE; } fclose(file); return PXL8_OK; } pxl8_cart_file* cf = find_file(cart, path); if (!cf) return PXL8_ERROR_FILE_NOT_FOUND; *size_out = cf->size; *data_out = malloc(cf->size); if (!*data_out) return PXL8_ERROR_OUT_OF_MEMORY; memcpy(*data_out, cart->data + cf->offset, cf->size); return PXL8_OK; } void pxl8_cart_free_file(u8* data) { free(data); } pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) { if (!folder_path || !output_path) return PXL8_ERROR_NULL_POINTER; if (!is_directory(folder_path)) { pxl8_error("Cart folder not found: %s", folder_path); return PXL8_ERROR_FILE_NOT_FOUND; } if (!has_main_script(folder_path)) { pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path); return PXL8_ERROR_FILE_NOT_FOUND; } char** paths = NULL; u32 count = 0, capacity = 0; collect_files_recursive(folder_path, "", &paths, &count, &capacity); if (count == 0) { pxl8_error("No files found in cart folder"); return PXL8_ERROR_FILE_NOT_FOUND; } u32 toc_size = 0; for (u32 i = 0; i < count; i++) { toc_size += sizeof(pxl8_cart_entry) + strlen(paths[i]); } u32 data_offset = sizeof(pxl8_cart_header) + toc_size; u32 total_size = data_offset; u32* file_sizes = malloc(count * sizeof(u32)); for (u32 i = 0; i < count; i++) { char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]); struct stat st; if (stat(full_path, &st) == 0) { file_sizes[i] = (u32)st.st_size; total_size += file_sizes[i]; } else { file_sizes[i] = 0; } } u8* buffer = calloc(1, total_size); if (!buffer) { free(file_sizes); for (u32 i = 0; i < count; i++) free(paths[i]); free(paths); return PXL8_ERROR_OUT_OF_MEMORY; } pxl8_cart_header* header = (pxl8_cart_header*)buffer; header->magic = PXL8_CART_MAGIC; header->version = PXL8_CART_VERSION; header->flags = 0; header->file_count = count; header->toc_size = toc_size; u8* toc = buffer + sizeof(pxl8_cart_header); u32 file_offset = data_offset; pxl8_info("Packing cart: %s -> %s", folder_path, output_path); for (u32 i = 0; i < count; i++) { pxl8_cart_entry* entry = (pxl8_cart_entry*)toc; entry->offset = file_offset; entry->size = file_sizes[i]; entry->path_len = (u16)strlen(paths[i]); toc += sizeof(pxl8_cart_entry); memcpy(toc, paths[i], entry->path_len); toc += entry->path_len; char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", folder_path, paths[i]); FILE* file = fopen(full_path, "rb"); if (file) { fread(buffer + file_offset, 1, file_sizes[i], file); fclose(file); pxl8_info(" %s (%u bytes)", paths[i], file_sizes[i]); } file_offset += file_sizes[i]; free(paths[i]); } free(paths); free(file_sizes); FILE* out = fopen(output_path, "wb"); if (!out) { free(buffer); return PXL8_ERROR_SYSTEM_FAILURE; } fwrite(buffer, 1, total_size, out); fclose(out); free(buffer); pxl8_info("Cart packed: %u files, %u bytes", count, total_size); return PXL8_OK; } pxl8_result pxl8_cart_bundle(const char* input_path, const char* output_path, const char* exe_path) { if (!input_path || !output_path || !exe_path) return PXL8_ERROR_NULL_POINTER; u8* cart_data = NULL; u32 cart_size = 0; bool free_cart = false; if (is_directory(input_path)) { char temp_pxc[256]; snprintf(temp_pxc, sizeof(temp_pxc), "/tmp/pxl8_bundle_%d.pxc", getpid()); pxl8_result result = pxl8_cart_pack(input_path, temp_pxc); if (result != PXL8_OK) return result; FILE* f = fopen(temp_pxc, "rb"); if (!f) return PXL8_ERROR_SYSTEM_FAILURE; fseek(f, 0, SEEK_END); cart_size = (u32)ftell(f); fseek(f, 0, SEEK_SET); cart_data = malloc(cart_size); fread(cart_data, 1, cart_size, f); fclose(f); unlink(temp_pxc); free_cart = true; } else if (is_pxc_file(input_path)) { FILE* f = fopen(input_path, "rb"); if (!f) return PXL8_ERROR_FILE_NOT_FOUND; fseek(f, 0, SEEK_END); cart_size = (u32)ftell(f); fseek(f, 0, SEEK_SET); cart_data = malloc(cart_size); fread(cart_data, 1, cart_size, f); fclose(f); free_cart = true; } else { return PXL8_ERROR_INVALID_FORMAT; } FILE* exe = fopen(exe_path, "rb"); if (!exe) { if (free_cart) free(cart_data); return PXL8_ERROR_FILE_NOT_FOUND; } fseek(exe, 0, SEEK_END); u32 exe_size = (u32)ftell(exe); fseek(exe, 0, SEEK_SET); u8* exe_data = malloc(exe_size); fread(exe_data, 1, exe_size, exe); fclose(exe); FILE* out = fopen(output_path, "wb"); if (!out) { free(exe_data); if (free_cart) free(cart_data); return PXL8_ERROR_SYSTEM_FAILURE; } fwrite(exe_data, 1, exe_size, out); free(exe_data); u32 cart_offset = exe_size; fwrite(cart_data, 1, cart_size, out); if (free_cart) free(cart_data); pxl8_cart_trailer trailer = { .magic = PXL8_CART_TRAILER_MAGIC, .cart_offset = cart_offset, .cart_size = cart_size }; fwrite(&trailer, sizeof(trailer), 1, out); fclose(out); chmod(output_path, 0755); pxl8_info("Bundle created: %s", output_path); return PXL8_OK; }