2025-08-13 15:04:49 -05:00
|
|
|
#define PXL8_AUTHORS "asrael <asrael@pxl8.org>"
|
|
|
|
|
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
|
|
|
|
|
#define PXL8_VERSION "0.1.0"
|
|
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
#include <stdio.h>
|
2025-09-29 11:26:52 -05:00
|
|
|
#include <stdlib.h>
|
2025-10-17 17:54:33 -05:00
|
|
|
#include <string.h>
|
2025-11-13 07:15:41 -06:00
|
|
|
|
2025-08-13 15:04:49 -05:00
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
#include "pxl8_game.h"
|
|
|
|
|
#include "pxl8_hal.h"
|
2025-12-02 11:02:23 -06:00
|
|
|
#include "pxl8_log.h"
|
2025-12-06 15:04:53 -06:00
|
|
|
#include "pxl8_macros.h"
|
2026-01-21 23:19:50 -06:00
|
|
|
#include "pxl8_mem.h"
|
2025-12-02 11:02:23 -06:00
|
|
|
#include "pxl8_repl.h"
|
2026-01-08 01:19:25 -06:00
|
|
|
#include "pxl8_replay.h"
|
2025-10-04 04:13:48 -05:00
|
|
|
#include "pxl8_script.h"
|
2026-01-08 01:19:25 -06:00
|
|
|
#include "pxl8_sfx.h"
|
2025-11-18 23:50:02 -06:00
|
|
|
#include "pxl8_sys.h"
|
2025-08-13 15:04:49 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
struct pxl8 {
|
|
|
|
|
pxl8_cart* cart;
|
|
|
|
|
pxl8_game* game;
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_repl* repl;
|
|
|
|
|
pxl8_log log;
|
2025-11-18 23:50:02 -06:00
|
|
|
const pxl8_hal* hal;
|
|
|
|
|
void* platform_data;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
#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
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8* pxl8_create(const pxl8_hal* hal) {
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8* sys = (pxl8*)pxl8_calloc(1, sizeof(pxl8));
|
2025-12-02 11:02:23 -06:00
|
|
|
if (!sys) return NULL;
|
|
|
|
|
|
|
|
|
|
pxl8_log_init(&sys->log);
|
|
|
|
|
|
|
|
|
|
if (!hal) {
|
|
|
|
|
pxl8_error("hal cannot be null");
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(sys);
|
2025-11-18 23:50:02 -06:00
|
|
|
return NULL;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
sys->hal = hal;
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
sys->game = (pxl8_game*)pxl8_calloc(1, sizeof(pxl8_game));
|
2025-11-18 23:50:02 -06:00
|
|
|
if (!sys->game) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("failed to allocate game");
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(sys);
|
2025-11-18 23:50:02 -06:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_destroy(pxl8* sys) {
|
|
|
|
|
if (!sys) return;
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
if (sys->game) pxl8_free(sys->game);
|
2025-12-02 11:02:23 -06:00
|
|
|
if (sys->cart) pxl8_cart_destroy(sys->cart);
|
|
|
|
|
if (sys->hal && sys->platform_data) sys->hal->destroy(sys->platform_data);
|
2025-11-18 23:50:02 -06:00
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(sys);
|
2025-11-18 23:50:02 -06:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
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 <folder> <out.pxc> Pack folder into cart file\n");
|
|
|
|
|
printf(" pxl8 bundle <in> <out> Bundle cart into executable\n");
|
|
|
|
|
printf(" pxl8 help Show this help\n");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
|
2025-12-02 11:02:23 -06:00
|
|
|
if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT;
|
2025-11-18 23:50:02 -06:00
|
|
|
|
|
|
|
|
pxl8_game* game = sys->game;
|
2025-08-13 15:04:49 -05:00
|
|
|
const char* script_arg = NULL;
|
2025-11-28 23:42:57 -06:00
|
|
|
bool bundle_mode = false;
|
2025-09-27 11:03:36 -05:00
|
|
|
bool pack_mode = false;
|
2025-12-07 15:52:54 -06:00
|
|
|
bool run_mode = false;
|
2025-09-27 11:03:36 -05:00
|
|
|
const char* pack_input = NULL;
|
|
|
|
|
const char* pack_output = NULL;
|
|
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
|
|
|
|
|
|
2025-10-04 04:13:48 -05:00
|
|
|
for (i32 i = 1; i < argc; i++) {
|
2025-12-07 15:52:54 -06:00
|
|
|
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) {
|
2025-10-17 17:54:33 -05:00
|
|
|
game->repl_mode = true;
|
2025-12-07 15:52:54 -06:00
|
|
|
} else if (strcmp(argv[i], "bundle") == 0 || strcmp(argv[i], "--bundle") == 0) {
|
2025-11-28 23:42:57 -06:00
|
|
|
bundle_mode = true;
|
|
|
|
|
if (i + 2 < argc) {
|
|
|
|
|
pack_input = argv[++i];
|
|
|
|
|
pack_output = argv[++i];
|
|
|
|
|
} else {
|
2025-12-07 15:52:54 -06:00
|
|
|
pxl8_error("bundle requires <folder|.pxc> <output>");
|
2025-11-28 23:42:57 -06:00
|
|
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
|
|
|
|
}
|
2025-12-07 15:52:54 -06:00
|
|
|
} else if (strcmp(argv[i], "pack") == 0 || strcmp(argv[i], "--pack") == 0) {
|
2025-09-27 11:03:36 -05:00
|
|
|
pack_mode = true;
|
|
|
|
|
if (i + 2 < argc) {
|
|
|
|
|
pack_input = argv[++i];
|
|
|
|
|
pack_output = argv[++i];
|
|
|
|
|
} else {
|
2025-12-07 15:52:54 -06:00
|
|
|
pxl8_error("pack requires <folder> <output.pxc>");
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
2025-09-27 11:03:36 -05:00
|
|
|
}
|
2025-08-13 15:04:49 -05:00
|
|
|
} else if (!script_arg) {
|
|
|
|
|
script_arg = argv[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-27 11:03:36 -05:00
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
if (!run_mode && !bundle_mode && !pack_mode) {
|
|
|
|
|
run_mode = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 23:42:57 -06:00
|
|
|
if (bundle_mode) {
|
|
|
|
|
char exe_path[1024];
|
|
|
|
|
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
|
|
|
|
|
if (len == -1) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("failed to resolve executable path");
|
2025-11-28 23:42:57 -06:00
|
|
|
return PXL8_ERROR_SYSTEM_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
exe_path[len] = '\0';
|
|
|
|
|
pxl8_result result = pxl8_cart_bundle(pack_input, pack_output, exe_path);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 11:03:36 -05:00
|
|
|
if (pack_mode) {
|
|
|
|
|
pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
|
2025-11-18 23:50:02 -06:00
|
|
|
return result;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
2025-11-18 23:50:02 -06:00
|
|
|
|
2025-12-06 15:04:53 -06:00
|
|
|
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;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 15:04:53 -06:00
|
|
|
const char* cart_path = script_arg;
|
|
|
|
|
char* original_cwd = getcwd(NULL, 0);
|
|
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
bool load_embedded = has_embedded && !run_mode;
|
2025-12-06 15:04:53 -06:00
|
|
|
bool load_from_path = false;
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2025-12-07 15:52:54 -06:00
|
|
|
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");
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(original_cwd);
|
2025-12-07 15:52:54 -06:00
|
|
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 15:04:53 -06:00
|
|
|
struct stat st;
|
|
|
|
|
load_from_path = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
|
2025-12-07 15:52:54 -06:00
|
|
|
strstr(cart_path, ".pxc");
|
2025-12-06 15:04:53 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(original_cwd);
|
2025-12-06 15:04:53 -06:00
|
|
|
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));
|
|
|
|
|
}
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(original_cwd);
|
2025-12-06 15:04:53 -06:00
|
|
|
|
|
|
|
|
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);
|
2025-11-18 23:50:02 -06:00
|
|
|
if (!sys->platform_data) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("failed to create platform context");
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 23:42:57 -06:00
|
|
|
game->gfx = pxl8_gfx_create(sys->hal, sys->platform_data, pixel_mode, resolution);
|
2025-10-17 17:54:33 -05:00
|
|
|
if (!game->gfx) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("failed to create graphics context");
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
if (pxl8_gfx_load_font_atlas(game->gfx) != PXL8_OK) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("failed to load font atlas");
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
game->mixer = pxl8_sfx_mixer_create(sys->hal);
|
2026-01-07 17:45:46 -06:00
|
|
|
if (!game->mixer) {
|
|
|
|
|
pxl8_error("failed to create audio mixer");
|
|
|
|
|
return PXL8_ERROR_INITIALIZATION_FAILED;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks());
|
|
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
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_chunk_cache(game->world));
|
|
|
|
|
pxl8_net_connect(game->net);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
#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
|
|
|
|
|
|
2025-12-06 15:04:53 -06:00
|
|
|
if (game->repl_mode) {
|
|
|
|
|
pxl8_info("starting in REPL mode with script: %s", game->script_path);
|
2025-09-27 11:03:36 -05:00
|
|
|
}
|
2025-09-28 13:10:29 -05:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
pxl8_script_set_gfx(game->script, game->gfx);
|
|
|
|
|
pxl8_script_set_input(game->script, &game->input);
|
2026-01-08 01:19:25 -06:00
|
|
|
pxl8_script_set_rng(game->script, &game->rng);
|
2026-01-07 17:45:46 -06:00
|
|
|
pxl8_script_set_sfx(game->script, game->mixer);
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_script_set_sys(game->script, sys);
|
2025-09-28 13:10:29 -05:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
if (game->script_path[0] != '\0') {
|
|
|
|
|
pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
|
|
|
|
|
game->script_loaded = (result == PXL8_OK);
|
|
|
|
|
|
2025-11-01 12:39:59 -05:00
|
|
|
if (game->script_loaded && !game->repl_mode) {
|
2025-12-06 15:04:53 -06:00
|
|
|
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));
|
|
|
|
|
}
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-01 12:39:59 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
game->last_time = sys->hal->get_ticks();
|
2025-10-17 17:54:33 -05:00
|
|
|
game->running = true;
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_OK;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_result pxl8_update(pxl8* sys) {
|
|
|
|
|
if (!sys || !sys->game) {
|
|
|
|
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
2025-10-17 17:54:33 -05:00
|
|
|
}
|
2025-08-13 15:04:49 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_game* game = sys->game;
|
|
|
|
|
u64 current_time = sys->hal->get_ticks();
|
2025-10-17 17:54:33 -05:00
|
|
|
f32 dt = (f32)(current_time - game->last_time) / 1000000000.0f;
|
2025-08-13 15:04:49 -05:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
game->last_time = current_time;
|
|
|
|
|
game->time += dt;
|
2025-11-09 06:30:17 -06:00
|
|
|
game->fps_accumulator += dt;
|
|
|
|
|
game->fps_frame_count++;
|
2025-12-02 11:02:23 -06:00
|
|
|
|
2025-11-09 06:30:17 -06:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
#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
|
2025-10-17 17:54:33 -05:00
|
|
|
|
2025-11-01 12:39:59 -05:00
|
|
|
if (game->repl_mode && !game->repl_started) {
|
|
|
|
|
if (game->script_loaded) {
|
|
|
|
|
pxl8_script_call_function(game->script, "init");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 11:02:23 -06:00
|
|
|
if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) {
|
|
|
|
|
const char* err_msg = pxl8_script_get_last_error(game->script);
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_error("failed to load pxl8 global: %s", err_msg);
|
2025-11-01 12:39:59 -05:00
|
|
|
}
|
2025-12-02 11:02:23 -06:00
|
|
|
|
|
|
|
|
sys->repl = pxl8_repl_create();
|
|
|
|
|
game->repl_started = true;
|
2025-11-01 12:39:59 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 11:02:23 -06:00
|
|
|
if (game->repl_mode && sys->repl) {
|
|
|
|
|
if (pxl8_repl_should_quit(sys->repl)) game->running = false;
|
2025-11-01 12:39:59 -05:00
|
|
|
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_repl_command* cmd = pxl8_repl_pop_command(sys->repl);
|
2025-08-13 15:04:49 -05:00
|
|
|
if (cmd) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_result result = pxl8_script_eval_repl(game->script, pxl8_repl_command_buffer(cmd));
|
2025-10-04 04:13:48 -05:00
|
|
|
if (result != PXL8_OK) {
|
2025-11-01 12:39:59 -05:00
|
|
|
if (pxl8_script_is_incomplete_input(game->script)) {
|
2025-12-03 09:41:33 -06:00
|
|
|
pxl8_repl_signal_complete(sys->repl);
|
2025-11-01 12:39:59 -05:00
|
|
|
} else {
|
|
|
|
|
pxl8_error("%s", pxl8_script_get_last_error(game->script));
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_repl_clear_accumulator(sys->repl);
|
2025-12-03 09:41:33 -06:00
|
|
|
pxl8_repl_signal_complete(sys->repl);
|
2025-11-01 12:39:59 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_repl_clear_accumulator(sys->repl);
|
2025-12-03 09:41:33 -06:00
|
|
|
pxl8_repl_signal_complete(sys->repl);
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
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, dt);
|
2025-11-28 23:42:57 -06:00
|
|
|
pxl8_gfx_update(game->gfx, dt);
|
2026-01-08 01:19:25 -06:00
|
|
|
pxl8_sfx_mixer_process(game->mixer);
|
2025-11-28 23:42:57 -06:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
if (game->script_loaded) {
|
|
|
|
|
pxl8_script_call_function_f32(game->script, "update", dt);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_OK;
|
2025-10-17 17:54:33 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_result pxl8_frame(pxl8* sys) {
|
2025-12-02 11:02:23 -06:00
|
|
|
if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT;
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
pxl8_game* game = sys->game;
|
2025-10-17 17:54:33 -05:00
|
|
|
pxl8_bounds bounds = pxl8_gfx_get_bounds(game->gfx);
|
|
|
|
|
|
|
|
|
|
if (game->script_loaded) {
|
|
|
|
|
pxl8_result frame_result = pxl8_script_call_function(game->script, "frame");
|
2025-10-04 04:13:48 -05:00
|
|
|
if (frame_result == PXL8_ERROR_SCRIPT_ERROR) {
|
2025-12-02 11:02:23 -06:00
|
|
|
pxl8_error("error calling frame: %s", pxl8_script_get_last_error(game->script));
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
pxl8_2d_clear(game->gfx, 32);
|
2025-10-04 11:55:04 -05:00
|
|
|
|
2025-11-28 23:42:57 -06:00
|
|
|
i32 render_w = pxl8_gfx_get_width(game->gfx);
|
|
|
|
|
i32 render_h = pxl8_gfx_get_height(game->gfx);
|
2025-10-04 11:55:04 -05:00
|
|
|
|
2025-11-28 23:42:57 -06:00
|
|
|
for (i32 y = 0; y < render_h; y += 24) {
|
|
|
|
|
for (i32 x = 0; x < render_w; x += 32) {
|
2025-10-17 17:54:33 -05:00
|
|
|
u32 color = ((x / 32) + (y / 24) + (i32)(game->time * 2)) % 8;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
pxl8_2d_rect_fill(game->gfx, x, y, 31, 23, color);
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-04 11:55:04 -05:00
|
|
|
|
2025-11-28 23:42:57 -06:00
|
|
|
pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, pxl8_gfx_get_width(game->gfx), pxl8_gfx_get_height(game->gfx)));
|
2025-10-17 17:54:33 -05:00
|
|
|
pxl8_gfx_upload_framebuffer(game->gfx);
|
|
|
|
|
pxl8_gfx_present(game->gfx);
|
2025-10-04 11:55:04 -05:00
|
|
|
|
2025-10-17 17:54:33 -05:00
|
|
|
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));
|
2025-12-02 11:02:23 -06:00
|
|
|
|
|
|
|
|
game->frame_count++;
|
2026-01-08 01:19:25 -06:00
|
|
|
|
|
|
|
|
#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
|
|
|
|
|
|
2025-11-19 22:18:08 -06:00
|
|
|
game->input.mouse_dx = 0;
|
|
|
|
|
game->input.mouse_dy = 0;
|
2025-10-17 17:54:33 -05:00
|
|
|
game->input.mouse_wheel_x = 0;
|
|
|
|
|
game->input.mouse_wheel_y = 0;
|
2025-08-13 15:04:49 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
return PXL8_OK;
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
void pxl8_quit(pxl8* sys) {
|
|
|
|
|
if (!sys || !sys->game) return;
|
|
|
|
|
|
|
|
|
|
pxl8_game* game = sys->game;
|
2025-10-04 11:55:04 -05:00
|
|
|
|
2025-12-06 15:04:53 -06:00
|
|
|
pxl8_info("Shutting down");
|
2025-11-01 12:39:59 -05:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
if (sys->cart) {
|
|
|
|
|
pxl8_cart_unmount(sys->cart);
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
2025-10-04 04:13:48 -05:00
|
|
|
|
2026-01-08 01:19:25 -06:00
|
|
|
#ifndef NDEBUG
|
|
|
|
|
pxl8_replay_destroy(game->debug_replay);
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
if (game->net) pxl8_net_destroy(game->net);
|
|
|
|
|
if (game->world) pxl8_world_destroy(game->world);
|
|
|
|
|
|
2026-01-07 17:45:46 -06:00
|
|
|
pxl8_sfx_mixer_destroy(game->mixer);
|
2025-10-17 17:54:33 -05:00
|
|
|
pxl8_gfx_destroy(game->gfx);
|
|
|
|
|
pxl8_script_destroy(game->script);
|
2025-08-13 15:04:49 -05:00
|
|
|
}
|
2025-11-11 23:26:51 -06:00
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
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;
|
2025-12-03 09:41:33 -06:00
|
|
|
if (!running && sys->repl) {
|
|
|
|
|
pxl8_repl_destroy(sys->repl);
|
|
|
|
|
sys->repl = NULL;
|
|
|
|
|
}
|
2025-11-18 23:50:02 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_world* pxl8_get_world(pxl8* sys) {
|
|
|
|
|
return (sys && sys->game) ? sys->game->world : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:50:02 -06:00
|
|
|
f32 pxl8_get_fps(const pxl8* sys) {
|
|
|
|
|
return (sys && sys->game) ? sys->game->fps : 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 22:18:08 -06:00
|
|
|
pxl8_gfx* pxl8_get_gfx(const pxl8* sys) {
|
2025-11-18 23:50:02 -06:00
|
|
|
return (sys && sys->game) ? sys->game->gfx : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 22:18:08 -06:00
|
|
|
pxl8_input_state* pxl8_get_input(const pxl8* sys) {
|
2025-11-18 23:50:02 -06:00
|
|
|
return (sys && sys->game) ? &sys->game->input : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_net* pxl8_get_net(const pxl8* sys) {
|
|
|
|
|
return (sys && sys->game) ? sys->game->net : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:45:46 -06:00
|
|
|
pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) {
|
|
|
|
|
return (sys && sys->game) ? sys->game->mixer : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 11:51:23 -06:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 22:18:08 -06:00
|
|
|
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};
|
|
|
|
|
}
|
|
|
|
|
}
|