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

@ -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,16 +705,45 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam
return PXL8_ERROR_SCRIPT_ERROR;
}
lua_getfield(script->L, -1, "dofile");
lua_pushstring(script->L, basename);
pxl8_cart* cart = pxl8_cart_current();
pxl8_result result = PXL8_OK;
if (lua_pcall(script->L, 1, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_pop(script->L, 1);
result = PXL8_ERROR_SCRIPT_ERROR;
if (cart && pxl8_cart_is_packed(cart)) {
u8* data = NULL;
u32 size = 0;
if (pxl8_cart_read_file(cart, basename, &data, &size) != PXL8_OK) {
pxl8_script_set_error(script, "Failed to read script from cart");
lua_pop(script->L, 1);
return PXL8_ERROR_FILE_NOT_FOUND;
}
lua_getfield(script->L, -1, "eval");
lua_pushlstring(script->L, (const char*)data, size);
lua_createtable(script->L, 0, 1);
lua_pushstring(script->L, basename);
lua_setfield(script->L, -2, "filename");
pxl8_cart_free_file(data);
if (lua_pcall(script->L, 2, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_pop(script->L, 1);
result = PXL8_ERROR_SCRIPT_ERROR;
} else {
script->last_error[0] = '\0';
}
} else {
script->last_error[0] = '\0';
lua_getfield(script->L, -1, "dofile");
lua_pushstring(script->L, basename);
if (lua_pcall(script->L, 1, 0, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_pop(script->L, 1);
result = PXL8_ERROR_SCRIPT_ERROR;
} else {
script->last_error[0] = '\0';
}
}
lua_pop(script->L, 1);