#define PXL8_AUTHORS "asrael " #define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org" #define PXL8_VERSION "0.1.0" #include #include #include #include #include #include "pxl8_cart.h" #include "pxl8_game.h" #include "pxl8_hal.h" #include "pxl8_macros.h" #include "pxl8_script.h" #include "pxl8_sys.h" #include "pxl8_types.h" struct pxl8 { pxl8_cart* cart; pxl8_game* game; const pxl8_hal* hal; void* platform_data; }; pxl8* pxl8_create(const pxl8_hal* hal) { if (!hal) { pxl8_error("HAL cannot be NULL"); return NULL; } pxl8* sys = (pxl8*)calloc(1, sizeof(pxl8)); if (!sys) { pxl8_error("Failed to allocate pxl8 system"); return NULL; } sys->hal = hal; sys->game = (pxl8_game*)calloc(1, sizeof(pxl8_game)); if (!sys->game) { pxl8_error("Failed to allocate game"); free(sys); return NULL; } return sys; } void pxl8_destroy(pxl8* sys) { if (!sys) return; if (sys->game) { free(sys->game); } if (sys->cart) { pxl8_cart_destroy(sys->cart); } if (sys->hal && sys->platform_data) { sys->hal->destroy(sys->platform_data); } free(sys); } pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { if (!sys || !sys->game) { return PXL8_ERROR_INVALID_ARGUMENT; } pxl8_game* game = sys->game; game->color_mode = PXL8_COLOR_MODE_MEGA; game->resolution = PXL8_RESOLUTION_640x360; const char* script_arg = NULL; bool pack_mode = false; const char* pack_input = NULL; const char* pack_output = NULL; for (i32 i = 1; i < argc; i++) { if (strcmp(argv[i], "--repl") == 0) { game->repl_mode = true; } else if (strcmp(argv[i], "--pack") == 0) { pack_mode = true; if (i + 2 < argc) { pack_input = argv[++i]; pack_output = argv[++i]; } else { pxl8_error("--pack requires "); return PXL8_ERROR_INVALID_ARGUMENT; } } else if (!script_arg) { script_arg = argv[i]; } } if (pack_mode) { pxl8_result result = pxl8_cart_pack(pack_input, pack_output); return result; } if (game->repl_mode) { fprintf(stderr, "\033[38;2;184;187;38m[pxl8]\033[0m Starting in REPL mode with script: %s\n", game->script_path); } pxl8_info("Starting up"); sys->platform_data = sys->hal->create(game->color_mode, game->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); if (!game->gfx) { pxl8_error("Failed to create graphics context"); return PXL8_ERROR_INITIALIZATION_FAILED; } if (pxl8_gfx_load_font_atlas(game->gfx) != PXL8_OK) { pxl8_error("Failed to load font atlas"); return PXL8_ERROR_INITIALIZATION_FAILED; } game->script = pxl8_script_create(); 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"; struct stat st; bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) || (cart_path && strstr(cart_path, ".pxc")); if (is_cart) { char* original_cwd = getcwd(NULL, 0); sys->cart = pxl8_cart_create(); if (!sys->cart) { pxl8_error("Failed to create cart"); return PXL8_ERROR_INITIALIZATION_FAILED; } if (pxl8_cart_load(sys->cart, cart_path) == PXL8_OK) { pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd); 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; } free(original_cwd); } else if (script_arg) { 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); pxl8_script_set_sys(game->script, sys); if (game->script_path[0] != '\0') { pxl8_result result = pxl8_script_load_main(game->script, game->script_path); game->script_loaded = (result == PXL8_OK); if (game->script_loaded && !game->repl_mode) { pxl8_script_call_function(game->script, "init"); } } game->last_time = sys->hal->get_ticks(); game->running = true; return PXL8_OK; } pxl8_result pxl8_update(pxl8* sys) { if (!sys || !sys->game) { return PXL8_ERROR_INVALID_ARGUMENT; } pxl8_game* game = sys->game; u64 current_time = sys->hal->get_ticks(); f32 dt = (f32)(current_time - game->last_time) / 1000000000.0f; game->last_time = current_time; game->time += dt; game->fps_accumulator += dt; game->fps_frame_count++; if (game->fps_accumulator >= 1.0f) { game->fps = (f32)game->fps_frame_count / game->fps_accumulator; if (!game->repl_mode) { pxl8_debug("FPS: %.1f (%.2fms)", game->fps, (game->fps_accumulator / game->fps_frame_count) * 1000.0f); } game->fps_accumulator = 0.0f; game->fps_frame_count = 0; } pxl8_script_check_reload(game->script); if (game->repl_mode && !game->repl_started) { if (game->script_loaded) { pxl8_script_call_function(game->script, "init"); } game->repl = pxl8_script_repl_create(); if (game->repl) { fprintf(stderr, "\033[38;2;184;187;38m[pxl8 REPL]\033[0m Fennel %s - Tab for completions, Ctrl-D to exit\n", "1.5.1"); if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) { fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(game->script)); } pxl8_script_repl_init(game->repl); game->repl_started = true; } } if (game->repl_mode && game->repl) { if (pxl8_script_repl_should_quit(game->repl)) { game->running = false; } pxl8_script_repl_command* cmd = pxl8_script_repl_pop_command(game->repl); if (cmd) { pxl8_result result = pxl8_script_eval_repl(game->script, pxl8_script_repl_command_buffer(cmd)); if (result != PXL8_OK) { if (pxl8_script_is_incomplete_input(game->script)) { } else { pxl8_error("%s", pxl8_script_get_last_error(game->script)); pxl8_script_repl_clear_accumulator(game->repl); } } else { pxl8_script_repl_clear_accumulator(game->repl); } pxl8_script_repl_eval_complete(game->repl); } } if (game->script_loaded) { pxl8_script_call_function_f32(game->script, "update", dt); } return PXL8_OK; } pxl8_result pxl8_frame(pxl8* sys) { if (!sys || !sys->game) { return PXL8_ERROR_INVALID_ARGUMENT; } pxl8_game* game = sys->game; pxl8_bounds bounds = pxl8_gfx_get_bounds(game->gfx); if (game->script_loaded) { pxl8_result frame_result = pxl8_script_call_function(game->script, "frame"); if (frame_result == PXL8_ERROR_SCRIPT_ERROR) { pxl8_error("Error calling frame: %s", pxl8_script_get_last_error(game->script)); } } else { pxl8_clear(game->gfx, 32); pxl8_size render_size = pxl8_get_resolution_dimensions(game->resolution); for (i32 y = 0; y < render_size.h; y += 24) { for (i32 x = 0; x < render_size.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_upload_framebuffer(game->gfx); pxl8_gfx_upload_atlas(game->gfx); pxl8_gfx_present(game->gfx); memset(game->input.keys_pressed, 0, sizeof(game->input.keys_pressed)); memset(game->input.keys_released, 0, sizeof(game->input.keys_released)); memset(game->input.mouse_buttons_pressed, 0, sizeof(game->input.mouse_buttons_pressed)); memset(game->input.mouse_buttons_released, 0, sizeof(game->input.mouse_buttons_released)); game->input.mouse_dx = 0; game->input.mouse_dy = 0; game->input.mouse_wheel_x = 0; game->input.mouse_wheel_y = 0; game->frame_count++; return PXL8_OK; } void pxl8_quit(pxl8* sys) { if (!sys || !sys->game) return; pxl8_game* game = sys->game; if (game->repl_mode && game->repl) { fprintf(stderr, "\r\033[K"); fflush(stderr); pxl8_script_repl_shutdown(game->repl); pxl8_script_repl_destroy(game->repl); } pxl8_info("Shutting down"); if (sys->cart) { pxl8_cart_unmount(sys->cart); } pxl8_gfx_destroy(game->gfx); pxl8_script_destroy(game->script); } bool pxl8_is_running(const pxl8* sys) { return sys && sys->game && sys->game->running; } void pxl8_set_running(pxl8* sys, bool running) { if (sys && sys->game) { sys->game->running = running; } } f32 pxl8_get_fps(const pxl8* sys) { return (sys && sys->game) ? sys->game->fps : 0.0f; } pxl8_gfx* pxl8_get_gfx(const pxl8* sys) { return (sys && sys->game) ? sys->game->gfx : NULL; } 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); } void pxl8_set_cursor(pxl8* sys, pxl8_cursor cursor) { if (!sys || !sys->hal || !sys->hal->set_cursor) return; sys->hal->set_cursor(sys->platform_data, cursor); } void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled) { if (!sys || !sys->hal || !sys->hal->set_relative_mouse_mode) return; sys->hal->set_relative_mouse_mode(sys->platform_data, enabled); if (sys->game) { sys->game->input.mouse_relative_mode = 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}; case PXL8_RESOLUTION_320x180: return (pxl8_size){320, 180}; case PXL8_RESOLUTION_320x240: return (pxl8_size){320, 240}; case PXL8_RESOLUTION_640x360: return (pxl8_size){640, 360}; case PXL8_RESOLUTION_640x480: return (pxl8_size){640, 480}; case PXL8_RESOLUTION_800x600: return (pxl8_size){800, 600}; case PXL8_RESOLUTION_960x540: return (pxl8_size){960, 540}; default: return (pxl8_size){640, 360}; } }