#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_ase.h" #include "pxl8_game.h" #include "pxl8_hal.h" #include "pxl8_log.h" #include "pxl8_macros.h" #include "pxl8_mem.h" #include "pxl8_repl.h" #include "pxl8_replay.h" #include "pxl8_script.h" #include "pxl8_sfx.h" #include "pxl8_sys.h" #include "pxl8_world.h" struct pxl8 { pxl8_cart* cart; pxl8_game* game; pxl8_repl* repl; pxl8_log log; const pxl8_hal* hal; void* platform_data; }; #ifndef NDEBUG static void pxl8_audio_event_callback(u8 event_type, u8 context_id, u8 note, f32 volume, void* userdata) { pxl8_game* game = (pxl8_game*)userdata; if (game && game->debug_replay) { pxl8_replay_write_audio_event(game->debug_replay, game->frame_count, event_type, context_id, note, volume); } } #endif pxl8* pxl8_create(const pxl8_hal* hal) { pxl8* sys = (pxl8*)pxl8_calloc(1, sizeof(pxl8)); if (!sys) return NULL; pxl8_log_init(&sys->log); if (!hal) { pxl8_error("hal cannot be null"); pxl8_free(sys); return NULL; } sys->hal = hal; sys->game = (pxl8_game*)pxl8_calloc(1, sizeof(pxl8_game)); if (!sys->game) { pxl8_error("failed to allocate game"); pxl8_free(sys); return NULL; } return sys; } void pxl8_destroy(pxl8* sys) { if (!sys) return; if (sys->game) pxl8_free(sys->game); if (sys->cart) pxl8_cart_destroy(sys->cart); if (sys->hal && sys->platform_data) sys->hal->destroy(sys->platform_data); pxl8_free(sys); } static void pxl8_print_help(void) { printf("pxl8 %s - pixel art game framework\n", PXL8_VERSION); printf("%s\n\n", PXL8_COPYRIGHT); printf("Usage: pxl8 [path] [--repl]\n\n"); printf(" pxl8 Run main.fnl from current directory\n"); printf(" pxl8 ./game Run game from folder\n"); printf(" pxl8 game.pxc Run packed cart file\n"); printf(" pxl8 --repl Run with REPL enabled\n\n"); printf("Other commands:\n"); printf(" pxl8 pack Pack folder into cart file\n"); printf(" pxl8 bundle Bundle cart into executable\n"); printf(" pxl8 remap-ase Remap ASE to palette by hue/lum\n"); printf(" pxl8 help Show this help\n"); } pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT; pxl8_game* game = sys->game; const char* script_arg = NULL; bool bundle_mode = false; bool pack_mode = false; bool remap_palette_mode = false; bool run_mode = false; const char* pack_input = NULL; const char* pack_output = NULL; const char* remap_palette = NULL; bool has_embedded = pxl8_cart_has_embedded(argv[0]); for (i32 i = 1; i < argc; i++) { if (strcmp(argv[i], "help") == 0 || strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { pxl8_print_help(); return PXL8_ERROR_INVALID_ARGUMENT; } else if (strcmp(argv[i], "run") == 0) { run_mode = true; } else if (strcmp(argv[i], "--repl") == 0) { game->repl_mode = true; } else if (strcmp(argv[i], "bundle") == 0 || 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 "); return PXL8_ERROR_INVALID_ARGUMENT; } } else if (strcmp(argv[i], "pack") == 0 || 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 (strcmp(argv[i], "remap-ase") == 0) { remap_palette_mode = true; if (i + 3 < argc) { pack_input = argv[++i]; pack_output = argv[++i]; remap_palette = argv[++i]; } else { pxl8_error("remap-ase requires "); return PXL8_ERROR_INVALID_ARGUMENT; } } else if (!script_arg) { script_arg = argv[i]; } } if (!run_mode && !bundle_mode && !pack_mode && !remap_palette_mode) { run_mode = true; } 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; } if (remap_palette_mode) { u32 palette[256]; u32 palette_count = 0; pxl8_result result = pxl8_ase_load_palette(remap_palette, palette, &palette_count); if (result != PXL8_OK) { pxl8_error("failed to load palette: %s", remap_palette); return result; } pxl8_ase_remap_config config = { .palette = palette, .palette_count = palette_count, .hue_tolerance = 0.08f }; return pxl8_ase_remap(pack_input, pack_output, &config); } pxl8_info("Starting up"); 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; char* original_cwd = getcwd(NULL, 0); bool load_embedded = has_embedded && !run_mode; bool load_from_path = false; if (!load_embedded && run_mode) { if (!cart_path) { if (access("main.fnl", F_OK) == 0 || access("main.lua", F_OK) == 0) { cart_path = "."; } else { pxl8_error("no main.fnl or main.lua found in current directory"); pxl8_free(original_cwd); return PXL8_ERROR_INITIALIZATION_FAILED; } } struct stat st; load_from_path = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) || strstr(cart_path, ".pxc"); } if (load_embedded || load_from_path) { sys->cart = pxl8_cart_create(); pxl8_result load_result = load_embedded ? pxl8_cart_load_embedded(sys->cart, argv[0]) : pxl8_cart_load(sys->cart, cart_path); if (!sys->cart || load_result != PXL8_OK) { pxl8_error("failed to load cart%s%s", load_from_path ? ": " : "", load_from_path ? cart_path : ""); if (sys->cart) pxl8_cart_destroy(sys->cart); sys->cart = NULL; pxl8_free(original_cwd); return PXL8_ERROR_INITIALIZATION_FAILED; } pxl8_cart_mount(sys->cart); pxl8_script_load_cart_manifest(game->script, sys->cart); if (load_from_path) { pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd); } pxl8_strncpy(game->script_path, "main.fnl", sizeof(game->script_path)); } else if (script_arg) { pxl8_strncpy(game->script_path, script_arg, sizeof(game->script_path)); } pxl8_free(original_cwd); const char* window_title = pxl8_cart_get_title(sys->cart); if (!window_title) window_title = "pxl8"; pxl8_resolution resolution = pxl8_cart_get_resolution(sys->cart); pxl8_pixel_mode pixel_mode = pxl8_cart_get_pixel_mode(sys->cart); pxl8_size window_size = pxl8_cart_get_window_size(sys->cart); pxl8_size render_size = pxl8_get_resolution_dimensions(resolution); sys->platform_data = sys->hal->create(render_size.w, render_size.h, window_title, window_size.w, window_size.h); 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, pixel_mode, 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->mixer = pxl8_sfx_mixer_create(sys->hal); if (!game->mixer) { pxl8_error("failed to create audio mixer"); return PXL8_ERROR_INITIALIZATION_FAILED; } pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks()); game->world = pxl8_world_create(); if (!game->world) { pxl8_error("failed to create world"); return PXL8_ERROR_INITIALIZATION_FAILED; } pxl8_net_config net_cfg = { .address = "127.0.0.1", .port = 7777 }; game->net = pxl8_net_create(&net_cfg); if (game->net) { pxl8_net_set_chunk_cache(game->net, pxl8_world_get_chunk_cache(game->world)); pxl8_net_set_world(game->net, game->world); pxl8_net_connect(game->net); } #ifndef NDEBUG game->debug_replay = pxl8_replay_create_buffer(60, 60); pxl8_sfx_mixer_set_event_callback(game->mixer, pxl8_audio_event_callback, game); #endif if (game->repl_mode) { pxl8_info("starting in REPL mode with script: %s", game->script_path); } pxl8_script_set_gfx(game->script, game->gfx); pxl8_script_set_input(game->script, &game->input); pxl8_script_set_rng(game->script, &game->rng); pxl8_script_set_sfx(game->script, game->mixer); 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_result init_result = pxl8_script_call_function(game->script, "init"); if (init_result != PXL8_OK) { pxl8_script_error("%s", pxl8_script_get_last_error(game->script)); } } } game->last_time = sys->hal->get_ticks(); game->running = true; #ifdef PXL8_ASYNC_THREADS if (game->net) { pxl8_net_start_thread(game->net); } if (game->world) { pxl8_world_start_sim_thread(game->world, game->net); } #endif 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; game->fps_accumulator = 0.0f; game->fps_frame_count = 0; } #ifndef NDEBUG u32 rng_state_before_reload = game->rng.state; #endif bool reloaded = pxl8_script_check_reload(game->script); #ifndef NDEBUG if (reloaded) { game->rng.state = rng_state_before_reload; pxl8_debug("Hot-reload: restored RNG state 0x%08X", rng_state_before_reload); } #else (void)reloaded; #endif if (game->repl_mode && !game->repl_started) { if (game->script_loaded) { pxl8_script_call_function(game->script, "init"); } if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) { const char* err_msg = pxl8_script_get_last_error(game->script); pxl8_error("failed to load pxl8 global: %s", err_msg); } sys->repl = pxl8_repl_create(); game->repl_started = true; } if (game->repl_mode && sys->repl) { if (pxl8_repl_should_quit(sys->repl)) game->running = false; pxl8_repl_command* cmd = pxl8_repl_pop_command(sys->repl); if (cmd) { pxl8_result result = pxl8_script_eval_repl(game->script, pxl8_repl_command_buffer(cmd)); if (result != PXL8_OK) { if (pxl8_script_is_incomplete_input(game->script)) { pxl8_repl_signal_complete(sys->repl); } else { pxl8_error("%s", pxl8_script_get_last_error(game->script)); pxl8_repl_clear_accumulator(sys->repl); pxl8_repl_signal_complete(sys->repl); } } else { pxl8_repl_clear_accumulator(sys->repl); pxl8_repl_signal_complete(sys->repl); } } } #ifdef PXL8_ASYNC_THREADS if (game->world) { pxl8_input_msg msg = {0}; msg.move_x = (pxl8_key_down(&game->input, "d") ? 1.0f : 0.0f) - (pxl8_key_down(&game->input, "a") ? 1.0f : 0.0f); msg.move_y = (pxl8_key_down(&game->input, "w") ? 1.0f : 0.0f) - (pxl8_key_down(&game->input, "s") ? 1.0f : 0.0f); if (game->input.mouse_relative_mode) { msg.look_dx = (f32)pxl8_mouse_dx(&game->input); msg.look_dy = (f32)pxl8_mouse_dy(&game->input); } msg.buttons = pxl8_key_down(&game->input, "space") ? 1 : 0; pxl8_world_push_input(game->world, &msg); } pxl8_net_update(game->net, dt); #else if (game->net) { while (pxl8_net_poll(game->net)) {} pxl8_net_update(game->net, dt); pxl8_world_sync(game->world, game->net); } pxl8_world_update(game->world, &game->input, dt); #endif pxl8_gfx_update(game->gfx, dt); pxl8_sfx_mixer_process(game->mixer); 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_2d_clear(game->gfx, 32); i32 render_w = pxl8_gfx_get_width(game->gfx); i32 render_h = pxl8_gfx_get_height(game->gfx); 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_2d_rect_fill(game->gfx, x, y, 31, 23, color); } } } 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_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->frame_count++; #ifndef NDEBUG if (game->debug_replay) { if (game->frame_count % 60 == 0) { pxl8_replay_write_keyframe(game->debug_replay, game->frame_count, game->time, &game->rng, &game->input); } else { pxl8_replay_write_input(game->debug_replay, game->frame_count, &game->prev_input, &game->input); } game->prev_input = game->input; } #endif game->input.mouse_dx = 0; game->input.mouse_dy = 0; game->input.mouse_wheel_x = 0; game->input.mouse_wheel_y = 0; return PXL8_OK; } void pxl8_quit(pxl8* sys) { if (!sys || !sys->game) return; pxl8_game* game = sys->game; pxl8_info("Shutting down"); #ifdef PXL8_ASYNC_THREADS if (game->world) { pxl8_world_stop_sim_thread(game->world); } if (game->net) { pxl8_net_stop_thread(game->net); } #endif if (sys->cart) { pxl8_cart_unmount(sys->cart); } #ifndef NDEBUG pxl8_replay_destroy(game->debug_replay); #endif if (game->net) pxl8_net_destroy(game->net); if (game->world) pxl8_world_destroy(game->world); pxl8_sfx_mixer_destroy(game->mixer); 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; if (!running && sys->repl) { pxl8_repl_destroy(sys->repl); sys->repl = NULL; } } } pxl8_world* pxl8_get_world(pxl8* sys) { return (sys && sys->game) ? sys->game->world : NULL; } 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_net* pxl8_get_net(const pxl8* sys) { return (sys && sys->game) ? sys->game->net : NULL; } pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) { return (sys && sys->game) ? sys->game->mixer : NULL; } 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; } } 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}; } }