#include "pxl8_script.h" #include #include #include #include #include #include #include #include #include #include "pxl8_cart.h" #include "pxl8_embed.h" #include "pxl8_gui.h" #include "pxl8_log.h" #include "pxl8_macros.h" #include "pxl8_mem.h" #include "pxl8_script_ffi.h" struct pxl8_script { lua_State* L; pxl8_gfx* gfx; pxl8_input_state* input; pxl8_sfx_mixer* mixer; char last_error[PXL8_MAX_ERROR_SIZE]; char main_path[PXL8_MAX_PATH]; char watch_dir[PXL8_MAX_PATH]; time_t latest_mod_time; int repl_env_ref; 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)); usize 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_get_cart(); if (!cart || !pxl8_cart_is_packed(cart)) { lua_pushstring(L, "\n\tno packed cart mounted"); return 1; } char path[512]; usize len = strlen(modname); usize j = 0; for (usize 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, usize output_size) { usize i = 0; usize j = 0; bool in_string = false; bool in_comment = false; while (input[i] && j < output_size - 1) { if (in_comment) { output[j++] = input[i]; if (input[i] == '\n') { in_comment = false; } i++; continue; } if (input[i] == ';' && !in_string) { in_comment = true; output[j++] = input[i++]; continue; } if (input[i] == '"' && (i == 0 || input[i-1] != '\\')) { in_string = !in_string; output[j++] = input[i++]; continue; } if (!in_string && input[i] == '(' && strncmp(&input[i], "(local ", 7) == 0) { memcpy(&output[j], "(global ", 8); j += 8; i += 7; continue; } output[j++] = input[i++]; } output[j] = '\0'; } void pxl8_lua_log(int level, const char* file, int line, const char* msg) { if (file && (file[0] == '?' || file[0] == '\0')) file = NULL; switch (level) { case PXL8_LOG_LEVEL_TRACE: pxl8_log_write_trace(file, line, "%s", msg); break; case PXL8_LOG_LEVEL_DEBUG: pxl8_log_write_debug(file, line, "%s", msg); break; case PXL8_LOG_LEVEL_INFO: pxl8_log_write_info("%s", msg); break; case PXL8_LOG_LEVEL_WARN: pxl8_log_write_warn(file, line, "%s", msg); break; case PXL8_LOG_LEVEL_ERROR: pxl8_log_write_error(file, line, "%s", msg); break; } } static void pxl8_script_set_error(pxl8_script* script, const char* error) { if (!script) return; if (!error) { snprintf(script->last_error, sizeof(script->last_error), "Unknown error"); return; } const char* src = error; char* dst = script->last_error; char* end = dst + sizeof(script->last_error) - 1; while (*src && dst < end) { if (strncmp(src, "[string \"", 9) == 0) { src += 9; const char* mod_start = src; while (*src && !(*src == '"' && *(src+1) == ']')) src++; usize mod_len = src - mod_start; if (mod_len > 4 && strncmp(mod_start, "pxl8", 4) == 0) { const char* prefix = "src/lua/"; while (*prefix && dst < end) *dst++ = *prefix++; for (usize i = 0; i < mod_len && dst < end; i++) { *dst++ = (mod_start[i] == '.') ? '/' : mod_start[i]; } const char* suffix = ".lua"; while (*suffix && dst < end) *dst++ = *suffix++; } else { for (usize i = 0; i < mod_len && dst < end; i++) { *dst++ = mod_start[i]; } } if (*src == '"' && *(src+1) == ']') src += 2; } else { *dst++ = *src++; } } *dst = '\0'; } const char* pxl8_script_get_last_error(pxl8_script* script) { return script ? script->last_error : "Invalid script context"; } static int pxl8_embed_searcher(lua_State* L) { const char* name = luaL_checkstring(L, 1); const pxl8_embed* e = pxl8_embed_find(name); if (!e) { lua_pushfstring(L, "\n\tno embedded module '%s'", name); return 1; } if (luaL_loadbuffer(L, e->data, e->size, name) != 0) { return lua_error(L); } lua_pushstring(L, name); return 2; } static void pxl8_install_embed_searcher(lua_State* L) { lua_getglobal(L, "package"); lua_getfield(L, -1, "searchers"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_getfield(L, -1, "loaders"); } int len = (int)lua_objlen(L, -1); for (int i = len; i >= 2; i--) { lua_rawgeti(L, -1, i); lua_rawseti(L, -2, i + 1); } lua_pushcfunction(L, pxl8_embed_searcher); lua_rawseti(L, -2, 2); lua_pop(L, 2); } pxl8_script* pxl8_script_create(bool repl_mode) { pxl8_script* script = (pxl8_script*)pxl8_calloc(1, sizeof(pxl8_script)); if (!script) return NULL; script->repl_mode = repl_mode; script->L = luaL_newstate(); if (!script->L) { pxl8_free(script); return NULL; } luaL_openlibs(script->L); lua_getglobal(script->L, "require"); lua_pushstring(script->L, "ffi"); if (lua_pcall(script->L, 1, 1, 0) != 0) { pxl8_error("FFI require failed: %s", lua_tostring(script->L, -1)); pxl8_script_destroy(script); return NULL; } lua_getfield(script->L, -1, "cdef"); lua_pushstring(script->L, pxl8_ffi_cdefs); if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_error("FFI cdef failed: %s", lua_tostring(script->L, -1)); pxl8_script_destroy(script); return NULL; } lua_pop(script->L, 1); pxl8_install_embed_searcher(script->L); const pxl8_embed* fennel = pxl8_embed_find("fennel"); if (fennel && luaL_loadbuffer(script->L, fennel->data, fennel->size, "fennel") == 0) { if (lua_pcall(script->L, 0, 1, 0) == 0) { lua_setglobal(script->L, "fennel"); lua_getglobal(script->L, "fennel"); lua_getfield(script->L, -1, "install"); if (lua_isfunction(script->L, -1)) { lua_newtable(script->L); lua_pushboolean(script->L, 1); lua_setfield(script->L, -2, "correlate"); lua_pushboolean(script->L, 1); lua_setfield(script->L, -2, "lambdaAsFn"); 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); } lua_pop(script->L, 1); } else { pxl8_warn("Failed to execute fennel: %s", lua_tostring(script->L, -1)); lua_pop(script->L, 1); } } script->last_error[0] = '\0'; script->repl_env_ref = LUA_NOREF; if (script->repl_mode) { lua_newtable(script->L); lua_newtable(script->L); lua_getglobal(script->L, "_G"); lua_setfield(script->L, -2, "__index"); lua_setmetatable(script->L, -2); script->repl_env_ref = luaL_ref(script->L, LUA_REGISTRYINDEX); } return script; } void pxl8_script_destroy(pxl8_script* script) { if (!script) return; if (script->L) { if (script->repl_env_ref != LUA_NOREF) { luaL_unref(script->L, LUA_REGISTRYINDEX, script->repl_env_ref); } lua_close(script->L); } pxl8_free(script); } void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx) { if (!script) return; script->gfx = gfx; if (script->L && gfx) { lua_pushlightuserdata(script->L, gfx); lua_setglobal(script->L, "pxl8_gfx"); } } void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) { if (!script) return; script->input = input; if (script->L && input) { lua_pushlightuserdata(script->L, input); lua_setglobal(script->L, "pxl8_input"); } } void pxl8_script_set_rng(pxl8_script* script, void* rng) { if (!script) return; if (script->L && rng) { lua_pushlightuserdata(script->L, rng); lua_setglobal(script->L, "pxl8_rng"); } } void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) { if (!script) return; script->mixer = mixer; if (script->L && mixer) { lua_pushlightuserdata(script->L, mixer); lua_setglobal(script->L, "pxl8_sfx"); } } void pxl8_script_set_sys(pxl8_script* script, void* sys) { if (!script) return; if (script->L && sys) { lua_pushlightuserdata(script->L, sys); lua_setglobal(script->L, "pxl8_sys"); } } static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, usize basename_size) { char filename_copy[PATH_MAX]; pxl8_strncpy(filename_copy, filename, sizeof(filename_copy)); char* last_slash = strrchr(filename_copy, '/'); if (last_slash) { *last_slash = '\0'; char script_dir[PATH_MAX]; char original_cwd[PATH_MAX]; if (realpath(filename_copy, script_dir) && getcwd(original_cwd, sizeof(original_cwd))) { chdir(script_dir); pxl8_script_set_cart_path(script, script_dir, original_cwd); strncpy(out_basename, last_slash + 1, basename_size - 1); out_basename[basename_size - 1] = '\0'; } else { strncpy(out_basename, filename, basename_size - 1); out_basename[basename_size - 1] = '\0'; } } else { strncpy(out_basename, filename, basename_size - 1); out_basename[basename_size - 1] = '\0'; } return PXL8_OK; } pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; char basename[PATH_MAX]; pxl8_script_prepare_path(script, filename, basename, sizeof(basename)); pxl8_result result = PXL8_OK; if (luaL_dofile(script->L, basename) != 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'; } return result; } static time_t get_file_mod_time(const char* path) { struct stat file_stat; if (stat(path, &file_stat) == 0) { return file_stat.st_mtime; } return 0; } static time_t get_latest_script_mod_time(const char* dir_path) { DIR* dir = opendir(dir_path); if (!dir) return 0; time_t latest = 0; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; char full_path[512]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); struct stat st; if (stat(full_path, &st) == 0) { if (S_ISDIR(st.st_mode)) { time_t subdir_time = get_latest_script_mod_time(full_path); if (subdir_time > latest) { latest = subdir_time; } } else { usize len = strlen(entry->d_name); bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) || (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0); if (is_script) { time_t mod_time = get_file_mod_time(full_path); if (mod_time > latest) { latest = mod_time; } } } } } closedir(dir); return latest; } void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) { if (!script || !script->L || !cart_path || !original_cwd) return; lua_getglobal(script->L, "package"); lua_getfield(script->L, -1, "path"); const char* current_path = lua_tostring(script->L, -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(script->L, new_path); lua_setfield(script->L, -3, "path"); lua_pop(script->L, 2); lua_getglobal(script->L, "fennel"); if (!lua_isnil(script->L, -1)) { lua_getfield(script->L, -1, "path"); const char* fennel_path = lua_tostring(script->L, -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(script->L, new_path); lua_setfield(script->L, -3, "path"); lua_pop(script->L, 1); } lua_pop(script->L, 1); pxl8_strncpy(script->watch_dir, cart_path, sizeof(script->watch_dir)); script->latest_mod_time = get_latest_script_mod_time(script->watch_dir); } pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) { if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; char basename[PATH_MAX]; pxl8_script_prepare_path(script, filename, basename, sizeof(basename)); lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { pxl8_script_set_error(script, "Fennel not loaded"); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } pxl8_cart* cart = pxl8_get_cart(); 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); 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); return result; } static pxl8_result pxl8_script_eval_internal(pxl8_script* script, const char* code, bool repl_mode) { if (!script || !script->L || !code) return PXL8_ERROR_NULL_POINTER; char transformed_code[PXL8_MAX_REPL_COMMAND_SIZE]; if (repl_mode) { pxl8_script_repl_promote_locals(code, transformed_code, sizeof(transformed_code)); code = transformed_code; } lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { pxl8_script_set_error(script, "Fennel not loaded"); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } lua_getfield(script->L, -1, "eval"); lua_pushstring(script->L, code); lua_newtable(script->L); lua_pushboolean(script->L, true); lua_setfield(script->L, -2, "useMetadata"); if (repl_mode && script->repl_env_ref != LUA_NOREF) { lua_rawgeti(script->L, LUA_REGISTRYINDEX, script->repl_env_ref); lua_setfield(script->L, -2, "env"); lua_pushboolean(script->L, false); lua_setfield(script->L, -2, "allowedGlobals"); } if (lua_pcall(script->L, 2, 1, 0) != 0) { const char* error = lua_tostring(script->L, -1); if (error) { char cleaned_error[2048]; usize j = 0; for (usize i = 0; error[i] && j < sizeof(cleaned_error) - 1; i++) { if (error[i] == '\t') { cleaned_error[j++] = ' '; } else { cleaned_error[j++] = error[i]; } } cleaned_error[j] = '\0'; pxl8_script_set_error(script, cleaned_error); } else { pxl8_script_set_error(script, "Unknown error"); } lua_pop(script->L, 2); return PXL8_ERROR_SCRIPT_ERROR; } if (!lua_isnil(script->L, -1)) { lua_pushvalue(script->L, -1); lua_setglobal(script->L, "_"); lua_getglobal(script->L, "tostring"); lua_pushvalue(script->L, -2); if (lua_pcall(script->L, 1, 1, 0) == 0) { const char* result = lua_tostring(script->L, -1); if (result && strlen(result) > 0) { printf("=> %s\n", result); fflush(stdout); } lua_pop(script->L, 1); } } lua_pop(script->L, 2); script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) { return pxl8_script_eval_internal(script, code, false); } pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code) { return pxl8_script_eval_internal(script, code, true); } pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name) { if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, "require"); lua_pushstring(script->L, module_name); if (lua_pcall(script->L, 1, 1, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } lua_setglobal(script->L, module_name); script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name) { if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, name); if (!lua_isfunction(script->L, -1)) { lua_pop(script->L, 1); return PXL8_OK; } if (lua_pcall(script->L, 0, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg) { if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, name); if (!lua_isfunction(script->L, -1)) { lua_pop(script->L, 1); return PXL8_OK; } lua_pushnumber(script->L, arg); if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } script->last_error[0] = '\0'; return PXL8_OK; } static bool pxl8_script_is_builtin_global(const char* name) { static const char* builtins[] = { "_G", "_VERSION", "arg", "assert", "bit", "collectgarbage", "coroutine", "debug", "dofile", "error", "fennel", "ffi", "gcinfo", "getfenv", "getmetatable", "init", "io", "ipairs", "jit", "load", "loadfile", "loadstring", "math", "module", "newproxy", "next", "os", "package", "pairs", "pcall", "print", "pxl8", "rawequal", "rawget", "rawset", "require", "select", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "update", "frame", "xpcall", "pxl8_gfx", "pxl8_input", "pxl8_rng", "pxl8_sfx", "pxl8_sys", NULL }; for (int i = 0; builtins[i]; i++) { if (strcmp(name, builtins[i]) == 0) return true; } return name[0] == '_' && name[1] == '_'; } static void pxl8_script_cleanup(pxl8_script* script) { if (script->gfx) { pxl8_gfx_clear_textures(script->gfx); } if (script->mixer) { pxl8_sfx_mixer_clear(script->mixer); } } static void pxl8_script_save_upvalues(lua_State* L, const char* func_name, int saved_table) { lua_getglobal(L, func_name); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return; } int func_idx = lua_gettop(L); for (int i = 1; ; i++) { const char* name = lua_getupvalue(L, func_idx, i); if (!name) break; int vtype = lua_type(L, -1); if (name[0] != '(' && vtype != LUA_TFUNCTION && vtype != LUA_TUSERDATA && vtype != LUA_TLIGHTUSERDATA && vtype != LUA_TTHREAD && vtype != LUA_TTABLE) { lua_pushstring(L, name); lua_pushvalue(L, -2); lua_settable(L, saved_table); } lua_pop(L, 1); } lua_pop(L, 1); } static void pxl8_script_restore_upvalues(lua_State* L, const char* func_name, int saved_table) { lua_getglobal(L, func_name); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return; } int func_idx = lua_gettop(L); for (int i = 1; ; i++) { const char* name = lua_getupvalue(L, func_idx, i); if (!name) break; lua_pop(L, 1); lua_getfield(L, saved_table, name); if (!lua_isnil(L, -1)) { lua_setupvalue(L, func_idx, i); } else { lua_pop(L, 1); } } lua_pop(L, 1); } static void pxl8_script_save_globals(pxl8_script* script) { lua_State* L = script->L; lua_newtable(L); int saved_table = lua_gettop(L); lua_pushvalue(L, LUA_GLOBALSINDEX); lua_pushnil(L); while (lua_next(L, -2) != 0) { if (lua_type(L, -2) == LUA_TSTRING) { const char* name = lua_tostring(L, -2); int vtype = lua_type(L, -1); if (!pxl8_script_is_builtin_global(name) && vtype != LUA_TFUNCTION && vtype != LUA_TUSERDATA && vtype != LUA_TLIGHTUSERDATA && vtype != LUA_TTHREAD && vtype != 10) { lua_pushvalue(L, -2); lua_pushvalue(L, -2); lua_settable(L, saved_table); } } lua_pop(L, 1); } lua_pop(L, 1); pxl8_script_save_upvalues(L, "update", saved_table); pxl8_script_save_upvalues(L, "frame", saved_table); lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); pxl8_debug("Hot reload state saved"); } static void pxl8_script_restore_globals(pxl8_script* script) { lua_State* L = script->L; lua_getfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); if (lua_isnil(L, -1)) { lua_pop(L, 1); return; } int saved_table = lua_gettop(L); lua_pushnil(L); while (lua_next(L, saved_table) != 0) { lua_pushvalue(L, -2); lua_pushvalue(L, -2); lua_settable(L, LUA_GLOBALSINDEX); lua_pop(L, 1); } pxl8_script_restore_upvalues(L, "update", saved_table); pxl8_script_restore_upvalues(L, "frame", saved_table); lua_pop(L, 1); lua_pushnil(L); lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); pxl8_debug("Hot reload state restored"); } #define SER_NIL 0 #define SER_BOOL 1 #define SER_NUMBER 2 #define SER_STRING 3 #define SER_TABLE 4 typedef struct { u8* data; u32 size; u32 capacity; } ser_buffer; static void ser_buffer_init(ser_buffer* buf) { buf->capacity = 1024; buf->data = pxl8_malloc(buf->capacity); buf->size = 0; } static void ser_buffer_grow(ser_buffer* buf, u32 needed) { if (buf->size + needed > buf->capacity) { while (buf->size + needed > buf->capacity) { buf->capacity *= 2; } buf->data = pxl8_realloc(buf->data, buf->capacity); } } static void ser_write_u8(ser_buffer* buf, u8 v) { ser_buffer_grow(buf, 1); buf->data[buf->size++] = v; } static void ser_write_u32(ser_buffer* buf, u32 v) { ser_buffer_grow(buf, 4); buf->data[buf->size++] = v & 0xFF; buf->data[buf->size++] = (v >> 8) & 0xFF; buf->data[buf->size++] = (v >> 16) & 0xFF; buf->data[buf->size++] = (v >> 24) & 0xFF; } static void ser_write_f64(ser_buffer* buf, f64 v) { ser_buffer_grow(buf, 8); memcpy(buf->data + buf->size, &v, 8); buf->size += 8; } static void ser_write_bytes(ser_buffer* buf, const void* data, u32 len) { ser_buffer_grow(buf, len); memcpy(buf->data + buf->size, data, len); buf->size += len; } static void ser_write_value(ser_buffer* buf, lua_State* L, int idx, int depth) { int t = lua_type(L, idx); switch (t) { case LUA_TNIL: ser_write_u8(buf, SER_NIL); break; case LUA_TBOOLEAN: ser_write_u8(buf, SER_BOOL); ser_write_u8(buf, lua_toboolean(L, idx) ? 1 : 0); break; case LUA_TNUMBER: ser_write_u8(buf, SER_NUMBER); ser_write_f64(buf, lua_tonumber(L, idx)); break; case LUA_TSTRING: { usize len; const char* str = lua_tolstring(L, idx, &len); ser_write_u8(buf, SER_STRING); ser_write_u32(buf, (u32)len); ser_write_bytes(buf, str, (u32)len); break; } case LUA_TTABLE: { if (depth > 16) { ser_write_u8(buf, SER_NIL); break; } ser_write_u8(buf, SER_TABLE); lua_pushvalue(L, idx); int tbl = lua_gettop(L); lua_pushnil(L); while (lua_next(L, tbl) != 0) { ser_write_value(buf, L, -2, depth + 1); ser_write_value(buf, L, -1, depth + 1); lua_pop(L, 1); } lua_pop(L, 1); ser_write_u8(buf, SER_NIL); break; } default: ser_write_u8(buf, SER_NIL); break; } } u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data) { if (!script || !out_data) return 0; lua_State* L = script->L; ser_buffer buf; ser_buffer_init(&buf); lua_pushvalue(L, LUA_GLOBALSINDEX); lua_pushnil(L); while (lua_next(L, -2) != 0) { if (lua_type(L, -2) == LUA_TSTRING) { const char* name = lua_tostring(L, -2); int vtype = lua_type(L, -1); if (!pxl8_script_is_builtin_global(name) && vtype != LUA_TFUNCTION && vtype != LUA_TUSERDATA && vtype != LUA_TLIGHTUSERDATA && vtype != LUA_TTHREAD && vtype != 10) { ser_write_value(&buf, L, -2, 0); ser_write_value(&buf, L, -1, 0); } } lua_pop(L, 1); } lua_pop(L, 1); ser_write_u8(&buf, SER_NIL); *out_data = buf.data; return buf.size; } typedef struct { const u8* data; u32 size; u32 pos; } deser_buffer; static u8 deser_read_u8(deser_buffer* buf) { if (buf->pos >= buf->size) return 0; return buf->data[buf->pos++]; } static u32 deser_read_u32(deser_buffer* buf) { if (buf->pos + 4 > buf->size) return 0; u32 v = buf->data[buf->pos] | (buf->data[buf->pos+1] << 8) | (buf->data[buf->pos+2] << 16) | (buf->data[buf->pos+3] << 24); buf->pos += 4; return v; } static f64 deser_read_f64(deser_buffer* buf) { if (buf->pos + 8 > buf->size) return 0; f64 v; memcpy(&v, buf->data + buf->pos, 8); buf->pos += 8; return v; } static void deser_read_value(deser_buffer* buf, lua_State* L, int depth) { u8 type = deser_read_u8(buf); switch (type) { case SER_NIL: lua_pushnil(L); break; case SER_BOOL: lua_pushboolean(L, deser_read_u8(buf)); break; case SER_NUMBER: lua_pushnumber(L, deser_read_f64(buf)); break; case SER_STRING: { u32 len = deser_read_u32(buf); if (buf->pos + len <= buf->size) { lua_pushlstring(L, (const char*)(buf->data + buf->pos), len); buf->pos += len; } else { lua_pushnil(L); } break; } case SER_TABLE: { if (depth > 16) { lua_pushnil(L); break; } lua_newtable(L); while (buf->pos < buf->size) { u8 key_type = deser_read_u8(buf); if (key_type == SER_NIL) break; buf->pos--; deser_read_value(buf, L, depth + 1); deser_read_value(buf, L, depth + 1); lua_settable(L, -3); } break; } default: lua_pushnil(L); break; } } void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) { if (!script || !data || size == 0) return; lua_State* L = script->L; deser_buffer buf = { .data = data, .size = size, .pos = 0 }; while (buf.pos < buf.size) { u8 key_type = deser_read_u8(&buf); if (key_type == SER_NIL) break; buf.pos--; deser_read_value(&buf, L, 0); deser_read_value(&buf, L, 0); lua_settable(L, LUA_GLOBALSINDEX); } pxl8_debug("Deserialized globals from %u bytes", size); } void pxl8_script_free_serialized(u8* data) { pxl8_free(data); } pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) { if (!script || !path) return PXL8_ERROR_NULL_POINTER; pxl8_strncpy(script->main_path, path, sizeof(script->main_path)); script->watch_dir[0] = '.'; script->watch_dir[1] = '\0'; script->latest_mod_time = get_latest_script_mod_time(script->watch_dir); const char* ext = strrchr(path, '.'); pxl8_result result = PXL8_ERROR_INVALID_FORMAT; if (ext && strcmp(ext, ".fnl") == 0) { result = pxl8_script_run_fennel_file(script, path); } else if (ext && strcmp(ext, ".lua") == 0) { result = pxl8_script_run_file(script, path); } else { pxl8_script_set_error(script, "Unknown script type (expected .fnl or .lua)"); return PXL8_ERROR_INVALID_FORMAT; } if (result == PXL8_OK) { pxl8_info("Loaded script: %s", path); } else { pxl8_error("Failed to load script: %s", script->last_error); } return result; } static void pxl8_script_clear_cart_modules(pxl8_script* script) { lua_State* L = script->L; lua_getglobal(L, "package"); lua_getfield(L, -1, "loaded"); lua_pushnil(L); while (lua_next(L, -2) != 0) { lua_pop(L, 1); const char* name = lua_tostring(L, -1); if (name && strncmp(name, "pxl8", 4) != 0 && strcmp(name, "fennel") != 0 && strcmp(name, "ffi") != 0 && strcmp(name, "bit") != 0 && strcmp(name, "jit") != 0 && strcmp(name, "string") != 0 && strcmp(name, "table") != 0 && strcmp(name, "math") != 0 && strcmp(name, "io") != 0 && strcmp(name, "os") != 0 && strcmp(name, "debug") != 0 && strcmp(name, "coroutine") != 0 && strcmp(name, "package") != 0) { lua_pushvalue(L, -1); lua_pushnil(L); lua_settable(L, -4); } } lua_pop(L, 2); } bool pxl8_script_check_reload(pxl8_script* script) { if (!script || script->main_path[0] == '\0') { return false; } time_t current_mod_time = get_latest_script_mod_time(script->watch_dir); if (current_mod_time > script->latest_mod_time && current_mod_time != 0) { pxl8_info("Script files modified, reloading: %s", script->main_path); script->latest_mod_time = current_mod_time; pxl8_script_cleanup(script); pxl8_script_save_globals(script); pxl8_script_clear_cart_modules(script); const char* ext = strrchr(script->main_path, '.'); bool reloaded = false; if (ext && strcmp(ext, ".fnl") == 0) { if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) { reloaded = true; } } else if (ext && strcmp(ext, ".lua") == 0) { if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) { reloaded = true; } } if (reloaded) { pxl8_script_restore_globals(script); pxl8_script_call_function(script, "init"); } return reloaded; } return false; } static bool pxl8_script_is_incomplete_error(const char* error) { if (!error) return false; return strstr(error, "expected closing delimiter") != NULL || strstr(error, "unexpected end of source") != NULL || strstr(error, "expected whitespace before") != NULL || strstr(error, "unexpected end") != NULL; } bool pxl8_script_is_incomplete_input(pxl8_script* script) { if (!script) return false; return pxl8_script_is_incomplete_error(script->last_error); } static pxl8_resolution parse_resolution(const char* str) { if (strcmp(str, "240x160") == 0) return PXL8_RESOLUTION_240x160; if (strcmp(str, "320x180") == 0) return PXL8_RESOLUTION_320x180; if (strcmp(str, "320x240") == 0) return PXL8_RESOLUTION_320x240; if (strcmp(str, "640x360") == 0) return PXL8_RESOLUTION_640x360; if (strcmp(str, "640x480") == 0) return PXL8_RESOLUTION_640x480; if (strcmp(str, "800x600") == 0) return PXL8_RESOLUTION_800x600; if (strcmp(str, "960x540") == 0) return PXL8_RESOLUTION_960x540; return PXL8_RESOLUTION_640x360; } static pxl8_pixel_mode parse_pixel_mode(const char* str) { if (strcmp(str, "indexed") == 0) return PXL8_PIXEL_INDEXED; if (strcmp(str, "hicolor") == 0) return PXL8_PIXEL_HICOLOR; return PXL8_PIXEL_INDEXED; } pxl8_result pxl8_script_load_cart_manifest(pxl8_script* script, pxl8_cart* cart) { if (!script || !script->L || !cart) return PXL8_ERROR_NULL_POINTER; if (!pxl8_cart_file_exists(cart, "cart.fnl")) { return PXL8_OK; } u8* data = NULL; u32 size = 0; if (pxl8_cart_read_file(cart, "cart.fnl", &data, &size) != PXL8_OK) { return PXL8_OK; } lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { lua_pop(script->L, 1); pxl8_cart_free_file(data); return PXL8_OK; } lua_getfield(script->L, -1, "eval"); lua_pushlstring(script->L, (const char*)data, size); lua_createtable(script->L, 0, 1); lua_pushstring(script->L, "cart.fnl"); lua_setfield(script->L, -2, "filename"); pxl8_cart_free_file(data); if (lua_pcall(script->L, 2, 1, 0) != 0) { pxl8_warn("Failed to load cart.fnl: %s", lua_tostring(script->L, -1)); lua_pop(script->L, 2); return PXL8_OK; } if (lua_istable(script->L, -1)) { lua_getfield(script->L, -1, "title"); if (lua_isstring(script->L, -1)) { pxl8_cart_set_title(cart, lua_tostring(script->L, -1)); } lua_pop(script->L, 1); lua_getfield(script->L, -1, "resolution"); if (lua_isstring(script->L, -1)) { pxl8_cart_set_resolution(cart, parse_resolution(lua_tostring(script->L, -1))); } lua_pop(script->L, 1); lua_getfield(script->L, -1, "pixel-mode"); if (lua_isstring(script->L, -1)) { pxl8_cart_set_pixel_mode(cart, parse_pixel_mode(lua_tostring(script->L, -1))); } lua_pop(script->L, 1); lua_getfield(script->L, -1, "window-size"); if (lua_istable(script->L, -1)) { lua_rawgeti(script->L, -1, 1); lua_rawgeti(script->L, -2, 2); if (lua_isnumber(script->L, -2) && lua_isnumber(script->L, -1)) { pxl8_size size = {(i32)lua_tonumber(script->L, -2), (i32)lua_tonumber(script->L, -1)}; pxl8_cart_set_window_size(cart, size); } lua_pop(script->L, 2); } lua_pop(script->L, 1); } lua_pop(script->L, 2); return PXL8_OK; }