wip repl
This commit is contained in:
parent
e862b02019
commit
e49fbede9a
21 changed files with 1280 additions and 148 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
||||||
.ccls-cache/
|
.ccls-cache/
|
||||||
.ccls
|
.ccls
|
||||||
**.DS_Store
|
**.DS_Store
|
||||||
|
|
||||||
|
.pxl8_history
|
||||||
|
*.aseprite-extension
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -1,6 +1,30 @@
|
||||||
|
# pxl8
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> <strong>Heavy development. So... <em>here be dragons :3</em></strong>
|
> <strong>Heavy development. So... <em>here be dragons :3</em></strong>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Fennel scripting** - Lisp-on-Lua with interactive REPL and hot-reload
|
||||||
|
- **Aseprite integration** - Load sprites, palettes, and tilemaps with custom tile properties
|
||||||
|
- **Classic color modes** - FAMI (NES), MEGA (Genesis), GBA, SNES, HICOLOR
|
||||||
|
- **Tilemap system** - Multi-layer tilemaps with collision and custom tile data
|
||||||
|
- **SDL3 backend** - Cross-platform windowing, input, and GPU rendering
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./pxl8.sh build # Build framework
|
||||||
|
./pxl8.sh run demo # Run demo
|
||||||
|
./pxl8.sh run # Launch interactive REPL
|
||||||
|
```
|
||||||
|
|
||||||
|
Projects need `main.fnl` with `init()`, `update(dt)`, and `frame()` functions.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- `tools/aseprite/tile-props/` - Aseprite extension for editing tile properties
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
@@@@@@@@@@@@@@@@@@@@@**^^""~~~"^@@^*@*@@**@@@@@@@@@
|
@@@@@@@@@@@@@@@@@@@@@**^^""~~~"^@@^*@*@@**@@@@@@@@@
|
||||||
@@@@@@@@@@@@@*^^'"~ , - ' '; ,@@b. ' -e@@@@@@@@@
|
@@@@@@@@@@@@@*^^'"~ , - ' '; ,@@b. ' -e@@@@@@@@@
|
||||||
|
|
|
||||||
10
pxl8.sh
10
pxl8.sh
|
|
@ -101,15 +101,14 @@ print_usage() {
|
||||||
echo " ./pxl8.sh <command> [options]"
|
echo " ./pxl8.sh <command> [options]"
|
||||||
echo
|
echo
|
||||||
echo -e "${BOLD}COMMANDS:${NC}"
|
echo -e "${BOLD}COMMANDS:${NC}"
|
||||||
|
echo " aseprite Package Aseprite extensions"
|
||||||
echo " build Build pxl8"
|
echo " build Build pxl8"
|
||||||
echo " clean Remove build artifacts"
|
echo " clean Remove build artifacts"
|
||||||
|
echo " help Show this help message"
|
||||||
echo " run Build and run pxl8 (optional: cart.pxc or folder)"
|
echo " run Build and run pxl8 (optional: cart.pxc or folder)"
|
||||||
echo
|
|
||||||
echo " update Download/update all dependencies"
|
echo " update Download/update all dependencies"
|
||||||
echo " vendor Fetch source for dependencies (ex. SDL3)"
|
echo " vendor Fetch source for dependencies (ex. SDL3)"
|
||||||
echo
|
echo
|
||||||
echo " help Show this help message"
|
|
||||||
echo
|
|
||||||
echo -e "${BOLD}OPTIONS:${NC}"
|
echo -e "${BOLD}OPTIONS:${NC}"
|
||||||
echo " --all Clean both build artifacts and dependencies"
|
echo " --all Clean both build artifacts and dependencies"
|
||||||
echo " --deps Clean only dependencies"
|
echo " --deps Clean only dependencies"
|
||||||
|
|
@ -517,6 +516,11 @@ case "$COMMAND" in
|
||||||
update_sdl
|
update_sdl
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
aseprite)
|
||||||
|
print_info "Packaging Aseprite extensions"
|
||||||
|
bash tools/aseprite/package.sh
|
||||||
|
;;
|
||||||
|
|
||||||
help|--help|-h|"")
|
help|--help|-h|"")
|
||||||
print_usage
|
print_usage
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,24 @@ function pxl8.tilemap_check_collision(tilemap, x, y, w, h)
|
||||||
return C.pxl8_tilemap_check_collision(tilemap, x, y, w, h)
|
return C.pxl8_tilemap_check_collision(tilemap, x, y, w, h)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local tile_data = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
function pxl8.tilemap_get_tile_data(tilemap, tile_id)
|
||||||
|
if not tilemap or tile_id == 0 then return nil end
|
||||||
|
if not tile_data[tilemap] then return nil end
|
||||||
|
return tile_data[tilemap][tile_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
function pxl8.tilemap_load_ase(tilemap, filepath, layer)
|
||||||
|
return C.pxl8_tilemap_load_ase(tilemap, filepath, layer or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function pxl8.tilemap_set_tile_data(tilemap, tile_id, data)
|
||||||
|
if not tilemap or tile_id == 0 then return end
|
||||||
|
if not tile_data[tilemap] then tile_data[tilemap] = {} end
|
||||||
|
tile_data[tilemap][tile_id] = data
|
||||||
|
end
|
||||||
|
|
||||||
pxl8.TILE_FLIP_X = 1
|
pxl8.TILE_FLIP_X = 1
|
||||||
pxl8.TILE_FLIP_Y = 2
|
pxl8.TILE_FLIP_Y = 2
|
||||||
pxl8.TILE_SOLID = 4
|
pxl8.TILE_SOLID = 4
|
||||||
|
|
|
||||||
55
src/pxl8.c
55
src/pxl8.c
|
|
@ -2,6 +2,7 @@
|
||||||
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
|
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
|
||||||
#define PXL8_VERSION "0.1.0"
|
#define PXL8_VERSION "0.1.0"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
@ -122,20 +123,12 @@ pxl8_game_result pxl8_init(pxl8_game* game, i32 argc, char* argv[]) {
|
||||||
if (game->script_path[0] != '\0') {
|
if (game->script_path[0] != '\0') {
|
||||||
pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
|
pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
|
||||||
game->script_loaded = (result == PXL8_OK);
|
game->script_loaded = (result == PXL8_OK);
|
||||||
}
|
|
||||||
|
|
||||||
if (game->repl_mode) {
|
|
||||||
game->repl = pxl8_script_repl_create();
|
|
||||||
if (game->repl) {
|
|
||||||
pxl8_script_repl_init(game->repl);
|
|
||||||
fprintf(stderr, "\033[38;2;184;187;38m[pxl8 REPL]\033[0m Fennel %s - Tab for completions, Ctrl-C to exit\n", "1.5.1");
|
|
||||||
|
|
||||||
if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) {
|
if (game->script_loaded && !game->repl_mode) {
|
||||||
fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(game->script));
|
pxl8_script_call_function(game->script, "init");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
game->last_time = game->hal->get_ticks();
|
game->last_time = game->hal->get_ticks();
|
||||||
game->running = true;
|
game->running = true;
|
||||||
|
|
||||||
|
|
@ -156,14 +149,42 @@ pxl8_game_result pxl8_update(pxl8_game* game) {
|
||||||
|
|
||||||
pxl8_script_check_reload(game->script);
|
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 (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);
|
pxl8_script_repl_command* cmd = pxl8_script_repl_pop_command(game->repl);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
pxl8_result result = pxl8_script_eval(game->script, pxl8_script_repl_command_buffer(cmd));
|
pxl8_result result = pxl8_script_eval_repl(game->script, pxl8_script_repl_command_buffer(cmd));
|
||||||
if (result != PXL8_OK) {
|
if (result != PXL8_OK) {
|
||||||
pxl8_error("%s", pxl8_script_get_last_error(game->script));
|
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_command_free(cmd);
|
pxl8_script_repl_eval_complete(game->repl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,13 +277,15 @@ pxl8_game_result pxl8_frame(pxl8_game* game) {
|
||||||
void pxl8_quit(pxl8_game* game) {
|
void pxl8_quit(pxl8_game* game) {
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
pxl8_info("Shutting down");
|
|
||||||
|
|
||||||
if (game->repl_mode && game->repl) {
|
if (game->repl_mode && game->repl) {
|
||||||
|
fprintf(stderr, "\r\033[K");
|
||||||
|
fflush(stderr);
|
||||||
pxl8_script_repl_shutdown(game->repl);
|
pxl8_script_repl_shutdown(game->repl);
|
||||||
pxl8_script_repl_destroy(game->repl);
|
pxl8_script_repl_destroy(game->repl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxl8_info("Shutting down");
|
||||||
|
|
||||||
if (game->cart) {
|
if (game->cart) {
|
||||||
pxl8_cart_unload(game->cart);
|
pxl8_cart_unload(game->cart);
|
||||||
free(game->cart);
|
free(game->cart);
|
||||||
|
|
|
||||||
325
src/pxl8_ase.c
325
src/pxl8_ase.c
|
|
@ -143,11 +143,136 @@ static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* pa
|
||||||
return PXL8_OK;
|
return PXL8_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
|
static pxl8_result parse_user_data_chunk(pxl8_stream* stream, pxl8_ase_user_data* user_data) {
|
||||||
if (chunk_size < 9) {
|
u32 flags = pxl8_read_u32(stream);
|
||||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
||||||
|
user_data->has_text = (flags & 1) != 0;
|
||||||
|
user_data->has_color = (flags & 2) != 0;
|
||||||
|
|
||||||
|
if (user_data->has_text) {
|
||||||
|
u16 text_len = pxl8_read_u16(stream);
|
||||||
|
if (text_len > 0) {
|
||||||
|
user_data->text = (char*)malloc(text_len + 1);
|
||||||
|
if (!user_data->text) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
pxl8_read_bytes(stream, user_data->text, text_len);
|
||||||
|
user_data->text[text_len] = '\0';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user_data->has_color) {
|
||||||
|
u8 r = pxl8_read_u8(stream);
|
||||||
|
u8 g = pxl8_read_u8(stream);
|
||||||
|
u8 b = pxl8_read_u8(stream);
|
||||||
|
u8 a = pxl8_read_u8(stream);
|
||||||
|
user_data->color = (a << 24) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & 4) {
|
||||||
|
pxl8_skip_bytes(stream, 4);
|
||||||
|
u32 num_properties = pxl8_read_u32(stream);
|
||||||
|
|
||||||
|
if (num_properties > 0) {
|
||||||
|
user_data->properties = (pxl8_ase_property*)calloc(num_properties, sizeof(pxl8_ase_property));
|
||||||
|
if (!user_data->properties) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
user_data->property_count = num_properties;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < num_properties; i++) {
|
||||||
|
u16 name_len = pxl8_read_u16(stream);
|
||||||
|
if (name_len > 0) {
|
||||||
|
user_data->properties[i].name = (char*)malloc(name_len + 1);
|
||||||
|
if (!user_data->properties[i].name) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
pxl8_read_bytes(stream, user_data->properties[i].name, name_len);
|
||||||
|
user_data->properties[i].name[name_len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 type = pxl8_read_u16(stream);
|
||||||
|
user_data->properties[i].type = type;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
user_data->properties[i].bool_val = pxl8_read_u8(stream) != 0;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
user_data->properties[i].int_val = pxl8_read_i32(stream);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
user_data->properties[i].float_val = pxl8_read_f32(stream);
|
||||||
|
break;
|
||||||
|
case 8: {
|
||||||
|
u16 str_len = pxl8_read_u16(stream);
|
||||||
|
if (str_len > 0) {
|
||||||
|
user_data->properties[i].string_val = (char*)malloc(str_len + 1);
|
||||||
|
if (!user_data->properties[i].string_val) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
pxl8_read_bytes(stream, user_data->properties[i].string_val, str_len);
|
||||||
|
user_data->properties[i].string_val[str_len] = '\0';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
pxl8_skip_bytes(stream, 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PXL8_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static pxl8_result parse_tileset_chunk(pxl8_stream* stream, pxl8_ase_tileset* tileset) {
|
||||||
|
tileset->id = pxl8_read_u32(stream);
|
||||||
|
tileset->flags = pxl8_read_u32(stream);
|
||||||
|
tileset->tile_count = pxl8_read_u32(stream);
|
||||||
|
tileset->tile_width = pxl8_read_u16(stream);
|
||||||
|
tileset->tile_height = pxl8_read_u16(stream);
|
||||||
|
tileset->base_index = pxl8_read_i16(stream);
|
||||||
|
pxl8_skip_bytes(stream, 14);
|
||||||
|
|
||||||
|
u16 name_len = pxl8_read_u16(stream);
|
||||||
|
if (name_len > 0) {
|
||||||
|
tileset->name = (char*)malloc(name_len + 1);
|
||||||
|
if (!tileset->name) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
pxl8_read_bytes(stream, tileset->name, name_len);
|
||||||
|
tileset->name[name_len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileset->flags & 1) {
|
||||||
|
u32 external_file_id = pxl8_read_u32(stream);
|
||||||
|
u32 tileset_id = pxl8_read_u32(stream);
|
||||||
|
(void)external_file_id;
|
||||||
|
(void)tileset_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileset->flags & 2) {
|
||||||
|
u32 compressed_size = pxl8_read_u32(stream);
|
||||||
|
tileset->pixels_size = tileset->tile_width * tileset->tile_height * tileset->tile_count;
|
||||||
|
tileset->pixels = (u8*)malloc(tileset->pixels_size);
|
||||||
|
if (!tileset->pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
|
||||||
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||||
|
mz_ulong dest_len = tileset->pixels_size;
|
||||||
|
i32 result = mz_uncompress(tileset->pixels, &dest_len, compressed_data, compressed_size);
|
||||||
|
if (result != MZ_OK) {
|
||||||
|
pxl8_error("Failed to decompress tileset data: miniz error %d", result);
|
||||||
|
free(tileset->pixels);
|
||||||
|
tileset->pixels = NULL;
|
||||||
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tileset->tile_user_data = (pxl8_ase_user_data*)calloc(tileset->tile_count, sizeof(pxl8_ase_user_data));
|
||||||
|
if (!tileset->tile_user_data) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
|
||||||
|
return PXL8_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
|
||||||
|
if (chunk_size < 9) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
|
|
||||||
cel->layer_index = pxl8_read_u16(stream);
|
cel->layer_index = pxl8_read_u16(stream);
|
||||||
cel->x = pxl8_read_i16(stream);
|
cel->x = pxl8_read_i16(stream);
|
||||||
cel->y = pxl8_read_i16(stream);
|
cel->y = pxl8_read_i16(stream);
|
||||||
|
|
@ -156,34 +281,80 @@ static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase
|
||||||
pxl8_skip_bytes(stream, 7);
|
pxl8_skip_bytes(stream, 7);
|
||||||
|
|
||||||
if (cel->cel_type == 2) {
|
if (cel->cel_type == 2) {
|
||||||
if (chunk_size < 20) {
|
if (chunk_size < 20) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
|
|
||||||
|
cel->image.width = pxl8_read_u16(stream);
|
||||||
|
cel->image.height = pxl8_read_u16(stream);
|
||||||
|
|
||||||
|
u32 pixels_size = cel->image.width * cel->image.height;
|
||||||
|
u32 compressed_size = chunk_size - 20;
|
||||||
|
|
||||||
|
cel->image.pixels = (u8*)malloc(pixels_size);
|
||||||
|
if (!cel->image.pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
|
||||||
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||||
|
mz_ulong dest_len = pixels_size;
|
||||||
|
i32 result = mz_uncompress(cel->image.pixels, &dest_len, compressed_data, compressed_size);
|
||||||
|
if (result != MZ_OK) {
|
||||||
|
pxl8_error("Failed to decompress cel data: miniz error %d", result);
|
||||||
|
free(cel->image.pixels);
|
||||||
|
cel->image.pixels = NULL;
|
||||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
}
|
}
|
||||||
|
|
||||||
cel->width = pxl8_read_u16(stream);
|
if (dest_len != pixels_size) {
|
||||||
cel->height = pxl8_read_u16(stream);
|
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixels_size, dest_len);
|
||||||
|
}
|
||||||
|
} else if (cel->cel_type == 3) {
|
||||||
|
if (chunk_size < 36) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
|
|
||||||
u32 pixel_data_size = cel->width * cel->height;
|
cel->tilemap.width = pxl8_read_u16(stream);
|
||||||
u32 compressed_data_size = chunk_size - 20;
|
cel->tilemap.height = pxl8_read_u16(stream);
|
||||||
|
cel->tilemap.bits_per_tile = pxl8_read_u16(stream);
|
||||||
|
cel->tilemap.tile_id_mask = pxl8_read_u32(stream);
|
||||||
|
cel->tilemap.x_flip_mask = pxl8_read_u32(stream);
|
||||||
|
cel->tilemap.y_flip_mask = pxl8_read_u32(stream);
|
||||||
|
cel->tilemap.diag_flip_mask = pxl8_read_u32(stream);
|
||||||
|
pxl8_skip_bytes(stream, 10);
|
||||||
|
|
||||||
cel->pixel_data = (u8*)malloc(pixel_data_size);
|
u32 tile_count = cel->tilemap.width * cel->tilemap.height;
|
||||||
if (!cel->pixel_data) {
|
u32 bytes_per_tile = (cel->tilemap.bits_per_tile + 7) / 8;
|
||||||
|
u32 uncompressed_size = tile_count * bytes_per_tile;
|
||||||
|
u32 compressed_size = chunk_size - 36;
|
||||||
|
|
||||||
|
u8* temp_buffer = (u8*)malloc(uncompressed_size);
|
||||||
|
if (!temp_buffer) return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
|
||||||
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
||||||
|
mz_ulong dest_len = uncompressed_size;
|
||||||
|
i32 result = mz_uncompress(temp_buffer, &dest_len, compressed_data, compressed_size);
|
||||||
|
if (result != MZ_OK) {
|
||||||
|
pxl8_error("Failed to decompress tilemap data: miniz error %d", result);
|
||||||
|
free(temp_buffer);
|
||||||
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
||||||
|
}
|
||||||
|
|
||||||
|
cel->tilemap.tiles = (u32*)calloc(tile_count, sizeof(u32));
|
||||||
|
if (!cel->tilemap.tiles) {
|
||||||
|
free(temp_buffer);
|
||||||
return PXL8_ERROR_OUT_OF_MEMORY;
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const u8* compressed_data = pxl8_read_ptr(stream, compressed_data_size);
|
for (u32 i = 0; i < tile_count; i++) {
|
||||||
mz_ulong dest_len = pixel_data_size;
|
if (cel->tilemap.bits_per_tile == 32) {
|
||||||
i32 result = mz_uncompress(cel->pixel_data, &dest_len, compressed_data, compressed_data_size);
|
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 4 + 0]) |
|
||||||
if (result != MZ_OK) {
|
((u32)temp_buffer[i * 4 + 1] << 8) |
|
||||||
pxl8_error("Failed to decompress cel data: miniz error %d", result);
|
((u32)temp_buffer[i * 4 + 2] << 16) |
|
||||||
free(cel->pixel_data);
|
((u32)temp_buffer[i * 4 + 3] << 24);
|
||||||
cel->pixel_data = NULL;
|
} else if (cel->tilemap.bits_per_tile == 16) {
|
||||||
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 2 + 0]) |
|
||||||
|
((u32)temp_buffer[i * 2 + 1] << 8);
|
||||||
|
} else if (cel->tilemap.bits_per_tile == 8) {
|
||||||
|
cel->tilemap.tiles[i] = temp_buffer[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dest_len != pixel_data_size) {
|
free(temp_buffer);
|
||||||
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixel_data_size, dest_len);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PXL8_OK;
|
return PXL8_OK;
|
||||||
|
|
@ -261,6 +432,9 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 last_tileset_idx = 0xFFFFFFFF;
|
||||||
|
u32 tileset_tile_idx = 0;
|
||||||
|
|
||||||
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
|
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
|
||||||
u32 chunk_start = pxl8_stream_position(&stream);
|
u32 chunk_start = pxl8_stream_position(&stream);
|
||||||
|
|
||||||
|
|
@ -301,30 +475,39 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
|
||||||
case PXL8_ASE_CHUNK_CEL: {
|
case PXL8_ASE_CHUNK_CEL: {
|
||||||
pxl8_ase_cel cel = {0};
|
pxl8_ase_cel cel = {0};
|
||||||
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
|
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
|
||||||
if (result == PXL8_OK && cel.pixel_data) {
|
if (result == PXL8_OK) {
|
||||||
u32 copy_width = (cel.width < frame->width) ? cel.width : frame->width;
|
frame->cels = (pxl8_ase_cel*)realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel));
|
||||||
u32 copy_height = (cel.height < frame->height) ? cel.height : frame->height;
|
if (!frame->cels) {
|
||||||
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
frame->cels[frame->cel_count] = cel;
|
||||||
|
frame->cel_count++;
|
||||||
|
|
||||||
for (u32 y = 0; y < copy_height; y++) {
|
if (cel.cel_type == 2 && cel.image.pixels) {
|
||||||
u32 src_offset = y * cel.width;
|
u32 copy_width = (cel.image.width < frame->width) ? cel.image.width : frame->width;
|
||||||
u32 dst_offset = (y + cel.y) * frame->width + cel.x;
|
u32 copy_height = (cel.image.height < frame->height) ? cel.image.height : frame->height;
|
||||||
if (dst_offset + copy_width <= pixel_count) {
|
|
||||||
for (u32 x = 0; x < copy_width; x++) {
|
|
||||||
u8 src_pixel = cel.pixel_data[src_offset + x];
|
|
||||||
bool is_transparent = false;
|
|
||||||
|
|
||||||
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
|
for (u32 y = 0; y < copy_height; y++) {
|
||||||
u32 color = ase_file->palette.colors[src_pixel];
|
u32 src_offset = y * cel.image.width;
|
||||||
is_transparent = ((color >> 24) & 0xFF) == 0;
|
u32 dst_offset = (y + cel.y) * frame->width + cel.x;
|
||||||
}
|
if (dst_offset + copy_width <= pixel_count) {
|
||||||
|
for (u32 x = 0; x < copy_width; x++) {
|
||||||
|
u8 src_pixel = cel.image.pixels[src_offset + x];
|
||||||
|
bool is_transparent = false;
|
||||||
|
|
||||||
if (!is_transparent) {
|
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
|
||||||
frame->pixels[dst_offset + x] = src_pixel;
|
u32 color = ase_file->palette.colors[src_pixel];
|
||||||
|
is_transparent = ((color >> 24) & 0xFF) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_transparent) {
|
||||||
|
frame->pixels[dst_offset + x] = src_pixel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
free(cel.pixel_data);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -337,6 +520,35 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
|
||||||
result = parse_palette_chunk(&stream, &ase_file->palette);
|
result = parse_palette_chunk(&stream, &ase_file->palette);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PXL8_ASE_CHUNK_TILESET: {
|
||||||
|
ase_file->tilesets = (pxl8_ase_tileset*)realloc(ase_file->tilesets,
|
||||||
|
(ase_file->tileset_count + 1) * sizeof(pxl8_ase_tileset));
|
||||||
|
if (!ase_file->tilesets) {
|
||||||
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&ase_file->tilesets[ase_file->tileset_count], 0, sizeof(pxl8_ase_tileset));
|
||||||
|
result = parse_tileset_chunk(&stream, &ase_file->tilesets[ase_file->tileset_count]);
|
||||||
|
if (result == PXL8_OK) {
|
||||||
|
last_tileset_idx = ase_file->tileset_count;
|
||||||
|
tileset_tile_idx = 0;
|
||||||
|
ase_file->tileset_count++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PXL8_ASE_CHUNK_USER_DATA: {
|
||||||
|
if (last_tileset_idx != 0xFFFFFFFF) {
|
||||||
|
pxl8_ase_tileset* tileset = &ase_file->tilesets[last_tileset_idx];
|
||||||
|
if (tileset_tile_idx < tileset->tile_count) {
|
||||||
|
result = parse_user_data_chunk(&stream, &tileset->tile_user_data[tileset_tile_idx]);
|
||||||
|
tileset_tile_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -365,8 +577,17 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
|
||||||
|
|
||||||
if (ase_file->frames) {
|
if (ase_file->frames) {
|
||||||
for (u32 i = 0; i < ase_file->frame_count; i++) {
|
for (u32 i = 0; i < ase_file->frame_count; i++) {
|
||||||
if (ase_file->frames[i].pixels) {
|
if (ase_file->frames[i].pixels) free(ase_file->frames[i].pixels);
|
||||||
free(ase_file->frames[i].pixels);
|
if (ase_file->frames[i].cels) {
|
||||||
|
for (u32 j = 0; j < ase_file->frames[i].cel_count; j++) {
|
||||||
|
pxl8_ase_cel* cel = &ase_file->frames[i].cels[j];
|
||||||
|
if (cel->cel_type == 2 && cel->image.pixels) {
|
||||||
|
free(cel->image.pixels);
|
||||||
|
} else if (cel->cel_type == 3 && cel->tilemap.tiles) {
|
||||||
|
free(cel->tilemap.tiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ase_file->frames[i].cels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
free(ase_file->frames);
|
free(ase_file->frames);
|
||||||
|
|
@ -385,5 +606,29 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
|
||||||
free(ase_file->layers);
|
free(ase_file->layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ase_file->tilesets) {
|
||||||
|
for (u32 i = 0; i < ase_file->tileset_count; i++) {
|
||||||
|
if (ase_file->tilesets[i].name) free(ase_file->tilesets[i].name);
|
||||||
|
if (ase_file->tilesets[i].pixels) free(ase_file->tilesets[i].pixels);
|
||||||
|
if (ase_file->tilesets[i].tile_user_data) {
|
||||||
|
for (u32 j = 0; j < ase_file->tilesets[i].tile_count; j++) {
|
||||||
|
pxl8_ase_user_data* ud = &ase_file->tilesets[i].tile_user_data[j];
|
||||||
|
if (ud->text) free(ud->text);
|
||||||
|
if (ud->properties) {
|
||||||
|
for (u32 k = 0; k < ud->property_count; k++) {
|
||||||
|
if (ud->properties[k].name) free(ud->properties[k].name);
|
||||||
|
if (ud->properties[k].type == 8 && ud->properties[k].string_val) {
|
||||||
|
free(ud->properties[k].string_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ud->properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ase_file->tilesets[i].tile_user_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ase_file->tilesets);
|
||||||
|
}
|
||||||
|
|
||||||
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
#define PXL8_ASE_CHUNK_LAYER 0x2004
|
#define PXL8_ASE_CHUNK_LAYER 0x2004
|
||||||
#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004
|
#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004
|
||||||
#define PXL8_ASE_CHUNK_PALETTE 0x2019
|
#define PXL8_ASE_CHUNK_PALETTE 0x2019
|
||||||
|
#define PXL8_ASE_CHUNK_TILESET 0x2023
|
||||||
|
#define PXL8_ASE_CHUNK_USER_DATA 0x2020
|
||||||
|
|
||||||
typedef struct pxl8_ase_header {
|
typedef struct pxl8_ase_header {
|
||||||
u32 file_size;
|
u32 file_size;
|
||||||
|
|
@ -46,9 +48,25 @@ typedef struct pxl8_ase_cel {
|
||||||
i16 y;
|
i16 y;
|
||||||
u8 opacity;
|
u8 opacity;
|
||||||
u16 cel_type;
|
u16 cel_type;
|
||||||
u16 width;
|
|
||||||
u16 height;
|
union {
|
||||||
u8* pixel_data;
|
struct {
|
||||||
|
u16 width;
|
||||||
|
u16 height;
|
||||||
|
u8* pixels;
|
||||||
|
} image;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
u16 width;
|
||||||
|
u16 height;
|
||||||
|
u16 bits_per_tile;
|
||||||
|
u32 tile_id_mask;
|
||||||
|
u32 x_flip_mask;
|
||||||
|
u32 y_flip_mask;
|
||||||
|
u32 diag_flip_mask;
|
||||||
|
u32* tiles;
|
||||||
|
} tilemap;
|
||||||
|
};
|
||||||
} pxl8_ase_cel;
|
} pxl8_ase_cel;
|
||||||
|
|
||||||
typedef struct pxl8_ase_layer {
|
typedef struct pxl8_ase_layer {
|
||||||
|
|
@ -75,8 +93,43 @@ typedef struct pxl8_ase_frame {
|
||||||
i16 y;
|
i16 y;
|
||||||
u16 duration;
|
u16 duration;
|
||||||
u8* pixels;
|
u8* pixels;
|
||||||
|
u32 cel_count;
|
||||||
|
pxl8_ase_cel* cels;
|
||||||
} pxl8_ase_frame;
|
} pxl8_ase_frame;
|
||||||
|
|
||||||
|
typedef struct pxl8_ase_property {
|
||||||
|
char* name;
|
||||||
|
u32 type;
|
||||||
|
union {
|
||||||
|
bool bool_val;
|
||||||
|
i32 int_val;
|
||||||
|
f32 float_val;
|
||||||
|
char* string_val;
|
||||||
|
};
|
||||||
|
} pxl8_ase_property;
|
||||||
|
|
||||||
|
typedef struct pxl8_ase_user_data {
|
||||||
|
char* text;
|
||||||
|
u32 color;
|
||||||
|
bool has_color;
|
||||||
|
bool has_text;
|
||||||
|
u32 property_count;
|
||||||
|
pxl8_ase_property* properties;
|
||||||
|
} pxl8_ase_user_data;
|
||||||
|
|
||||||
|
typedef struct pxl8_ase_tileset {
|
||||||
|
u32 id;
|
||||||
|
u32 flags;
|
||||||
|
u32 tile_count;
|
||||||
|
u16 tile_width;
|
||||||
|
u16 tile_height;
|
||||||
|
i16 base_index;
|
||||||
|
char* name;
|
||||||
|
u32 pixels_size;
|
||||||
|
u8* pixels;
|
||||||
|
pxl8_ase_user_data* tile_user_data;
|
||||||
|
} pxl8_ase_tileset;
|
||||||
|
|
||||||
typedef struct pxl8_ase_file {
|
typedef struct pxl8_ase_file {
|
||||||
u32 frame_count;
|
u32 frame_count;
|
||||||
pxl8_ase_frame* frames;
|
pxl8_ase_frame* frames;
|
||||||
|
|
@ -84,6 +137,8 @@ typedef struct pxl8_ase_file {
|
||||||
u32 layer_count;
|
u32 layer_count;
|
||||||
pxl8_ase_layer* layers;
|
pxl8_ase_layer* layers;
|
||||||
pxl8_ase_palette palette;
|
pxl8_ase_palette palette;
|
||||||
|
u32 tileset_count;
|
||||||
|
pxl8_ase_tileset* tilesets;
|
||||||
} pxl8_ase_file;
|
} pxl8_ase_file;
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
||||||
|
|
@ -288,25 +288,20 @@ void pxl8_cart_unmount(pxl8_cart* cart) {
|
||||||
pxl8_info("Unmounted cart: %s", cart->name);
|
pxl8_info("Unmounted cart: %s", cart->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
char* pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path) {
|
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size) {
|
||||||
if (!cart || !cart->base_path || !relative_path) return NULL;
|
if (!cart || !cart->base_path || !relative_path || !out_path || out_size == 0) return false;
|
||||||
|
|
||||||
char* full_path = malloc(512);
|
i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path);
|
||||||
if (!full_path) return NULL;
|
return written >= 0 && (size_t)written < out_size;
|
||||||
|
|
||||||
snprintf(full_path, 512, "%s/%s", cart->base_path, relative_path);
|
|
||||||
return full_path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
|
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
|
||||||
if (!cart || !cart->base_path || !path) return false;
|
if (!cart || !cart->base_path || !path) return false;
|
||||||
|
|
||||||
char* full_path = pxl8_cart_resolve_path(cart, path);
|
char full_path[512];
|
||||||
if (!full_path) return false;
|
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
|
||||||
|
|
||||||
bool exists = access(full_path, F_OK) == 0;
|
return access(full_path, F_OK) == 0;
|
||||||
free(full_path);
|
|
||||||
return exists;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
|
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const char* pxl8_cart_get_name(const pxl8_cart* cart);
|
||||||
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
|
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path);
|
||||||
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
|
pxl8_result pxl8_cart_mount(pxl8_cart* cart);
|
||||||
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
|
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path);
|
||||||
char* pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path);
|
bool pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path, char* out_path, size_t out_size);
|
||||||
void pxl8_cart_unload(pxl8_cart* cart);
|
void pxl8_cart_unload(pxl8_cart* cart);
|
||||||
void pxl8_cart_unmount(pxl8_cart* cart);
|
void pxl8_cart_unmount(pxl8_cart* cart);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ typedef struct pxl8_game {
|
||||||
f32 time;
|
f32 time;
|
||||||
|
|
||||||
bool repl_mode;
|
bool repl_mode;
|
||||||
|
bool repl_started;
|
||||||
bool running;
|
bool running;
|
||||||
bool script_loaded;
|
bool script_loaded;
|
||||||
char script_path[256];
|
char script_path[256];
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
|
||||||
do { \
|
do { \
|
||||||
char timestamp[16]; \
|
char timestamp[16]; \
|
||||||
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
||||||
fprintf(stderr, PXL8_LOG_DEBUG "[%s DEBUG]" PXL8_LOG_RESET \
|
fprintf(stderr, "\r\033[K" PXL8_LOG_DEBUG "[%s DEBUG]" PXL8_LOG_RESET \
|
||||||
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
||||||
fprintf(stderr, __VA_ARGS__); \
|
fprintf(stderr, __VA_ARGS__); \
|
||||||
fprintf(stderr, "\n"); \
|
fprintf(stderr, "\n"); \
|
||||||
|
|
@ -38,7 +38,7 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
|
||||||
do { \
|
do { \
|
||||||
char timestamp[16]; \
|
char timestamp[16]; \
|
||||||
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
||||||
fprintf(stderr, PXL8_LOG_TRACE "[%s TRACE]" PXL8_LOG_RESET \
|
fprintf(stderr, "\r\033[K" PXL8_LOG_TRACE "[%s TRACE]" PXL8_LOG_RESET \
|
||||||
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
||||||
fprintf(stderr, __VA_ARGS__); \
|
fprintf(stderr, __VA_ARGS__); \
|
||||||
fprintf(stderr, "\n"); \
|
fprintf(stderr, "\n"); \
|
||||||
|
|
@ -56,30 +56,33 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
|
||||||
do { \
|
do { \
|
||||||
char timestamp[16]; \
|
char timestamp[16]; \
|
||||||
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
||||||
fprintf(stderr, PXL8_LOG_ERROR "[%s ERROR]" PXL8_LOG_RESET \
|
fprintf(stderr, "\r\033[K" PXL8_LOG_ERROR "[%s ERROR]" PXL8_LOG_RESET \
|
||||||
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
||||||
fprintf(stderr, __VA_ARGS__); \
|
fprintf(stderr, __VA_ARGS__); \
|
||||||
fprintf(stderr, "\n"); \
|
fprintf(stderr, "\n"); \
|
||||||
|
\
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
#define pxl8_warn(...) \
|
#define pxl8_warn(...) \
|
||||||
do { \
|
do { \
|
||||||
char timestamp[16]; \
|
char timestamp[16]; \
|
||||||
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
||||||
fprintf(stderr, PXL8_LOG_WARN "[%s WARN]" PXL8_LOG_RESET \
|
fprintf(stderr, "\r\033[K" PXL8_LOG_WARN "[%s WARN]" PXL8_LOG_RESET \
|
||||||
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
" %s:%d: ", timestamp, __FILE__, __LINE__); \
|
||||||
fprintf(stderr, __VA_ARGS__); \
|
fprintf(stderr, __VA_ARGS__); \
|
||||||
fprintf(stderr, "\n"); \
|
fprintf(stderr, "\n"); \
|
||||||
|
\
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
#define pxl8_info(...) \
|
#define pxl8_info(...) \
|
||||||
do { \
|
do { \
|
||||||
char timestamp[16]; \
|
char timestamp[16]; \
|
||||||
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
pxl8_log_timestamp(timestamp, sizeof(timestamp)); \
|
||||||
fprintf(stdout, PXL8_LOG_INFO "[%s INFO]" PXL8_LOG_RESET \
|
fprintf(stdout, "\r\033[K" PXL8_LOG_INFO "[%s INFO]" PXL8_LOG_RESET \
|
||||||
" ", timestamp); \
|
" ", timestamp); \
|
||||||
fprintf(stdout, __VA_ARGS__); \
|
fprintf(stdout, __VA_ARGS__); \
|
||||||
fprintf(stdout, "\n"); \
|
fprintf(stdout, "\n"); \
|
||||||
|
\
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
#ifndef pxl8_min
|
#ifndef pxl8_min
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
@ -23,6 +24,49 @@ struct pxl8_script {
|
||||||
time_t latest_mod_time;
|
time_t latest_mod_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define PXL8_MAX_REPL_COMMAND_SIZE 4096
|
||||||
|
|
||||||
|
static void pxl8_script_repl_promote_locals(const char* input, char* output, size_t output_size) {
|
||||||
|
size_t i = 0;
|
||||||
|
size_t 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) {
|
||||||
|
strncpy(&output[j], "(global ", 8);
|
||||||
|
j += 8;
|
||||||
|
i += 7;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[j++] = input[i++];
|
||||||
|
}
|
||||||
|
output[j] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
static const char* pxl8_ffi_cdefs =
|
static const char* pxl8_ffi_cdefs =
|
||||||
"typedef uint8_t u8;\n"
|
"typedef uint8_t u8;\n"
|
||||||
"typedef uint16_t u16;\n"
|
"typedef uint16_t u16;\n"
|
||||||
|
|
@ -70,10 +114,17 @@ static const char* pxl8_ffi_cdefs =
|
||||||
"void pxl8_lua_trace(const char* msg);\n"
|
"void pxl8_lua_trace(const char* msg);\n"
|
||||||
"void pxl8_lua_warn(const char* msg);\n"
|
"void pxl8_lua_warn(const char* msg);\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"typedef u32 pxl8_tile;\n"
|
||||||
"typedef struct pxl8_tilemap pxl8_tilemap;\n"
|
"typedef struct pxl8_tilemap pxl8_tilemap;\n"
|
||||||
"typedef struct pxl8_tilesheet pxl8_tilesheet;\n"
|
"typedef struct pxl8_tilesheet pxl8_tilesheet;\n"
|
||||||
"pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);\n"
|
"pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);\n"
|
||||||
"void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);\n"
|
"void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);\n"
|
||||||
|
"u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap);\n"
|
||||||
|
"pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n"
|
||||||
|
"u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap);\n"
|
||||||
|
"void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id);\n"
|
||||||
|
"u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap);\n"
|
||||||
|
"void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data);\n"
|
||||||
"bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);\n"
|
"bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);\n"
|
||||||
"u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n"
|
"u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n"
|
||||||
"bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);\n"
|
"bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);\n"
|
||||||
|
|
@ -82,6 +133,7 @@ static const char* pxl8_ffi_cdefs =
|
||||||
"void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);\n"
|
"void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);\n"
|
||||||
"void pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);\n"
|
"void pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);\n"
|
||||||
"i32 pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);\n"
|
"i32 pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);\n"
|
||||||
|
"i32 pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);\n"
|
||||||
"pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);\n"
|
"pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);\n"
|
||||||
"void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);\n"
|
"void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);\n"
|
||||||
"i32 pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx);\n"
|
"i32 pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx);\n"
|
||||||
|
|
@ -314,33 +366,40 @@ void pxl8_script_set_ui(pxl8_script* script, pxl8_ui* ui) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char** out_basename) {
|
static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, size_t basename_size) {
|
||||||
char* filename_copy = strdup(filename);
|
char filename_copy[PATH_MAX];
|
||||||
|
strncpy(filename_copy, filename, sizeof(filename_copy) - 1);
|
||||||
|
filename_copy[sizeof(filename_copy) - 1] = '\0';
|
||||||
|
|
||||||
char* last_slash = strrchr(filename_copy, '/');
|
char* last_slash = strrchr(filename_copy, '/');
|
||||||
*out_basename = (char*)filename;
|
|
||||||
|
|
||||||
if (last_slash) {
|
if (last_slash) {
|
||||||
*last_slash = '\0';
|
*last_slash = '\0';
|
||||||
char* script_dir = realpath(filename_copy, NULL);
|
char script_dir[PATH_MAX];
|
||||||
char* original_cwd = getcwd(NULL, 0);
|
char original_cwd[PATH_MAX];
|
||||||
if (script_dir && original_cwd) {
|
|
||||||
|
if (realpath(filename_copy, script_dir) && getcwd(original_cwd, sizeof(original_cwd))) {
|
||||||
chdir(script_dir);
|
chdir(script_dir);
|
||||||
pxl8_script_set_cart_path(script, script_dir, original_cwd);
|
pxl8_script_set_cart_path(script, script_dir, original_cwd);
|
||||||
*out_basename = strdup(last_slash + 1);
|
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';
|
||||||
}
|
}
|
||||||
free(script_dir);
|
} else {
|
||||||
free(original_cwd);
|
strncpy(out_basename, filename, basename_size - 1);
|
||||||
|
out_basename[basename_size - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
free(filename_copy);
|
|
||||||
return PXL8_OK;
|
return PXL8_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
||||||
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
|
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
|
||||||
char* basename;
|
char basename[PATH_MAX];
|
||||||
pxl8_script_prepare_path(script, filename, &basename);
|
pxl8_script_prepare_path(script, filename, basename, sizeof(basename));
|
||||||
|
|
||||||
pxl8_result result = PXL8_OK;
|
pxl8_result result = PXL8_OK;
|
||||||
if (luaL_dofile(script->L, basename) != 0) {
|
if (luaL_dofile(script->L, basename) != 0) {
|
||||||
|
|
@ -351,7 +410,6 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
||||||
script->last_error[0] = '\0';
|
script->last_error[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (basename != filename) free(basename);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,14 +450,13 @@ void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const
|
||||||
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
|
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
|
||||||
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
|
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
|
||||||
char* basename;
|
char basename[PATH_MAX];
|
||||||
pxl8_script_prepare_path(script, filename, &basename);
|
pxl8_script_prepare_path(script, filename, basename, sizeof(basename));
|
||||||
|
|
||||||
lua_getglobal(script->L, "fennel");
|
lua_getglobal(script->L, "fennel");
|
||||||
if (lua_isnil(script->L, -1)) {
|
if (lua_isnil(script->L, -1)) {
|
||||||
pxl8_script_set_error(script, "Fennel not loaded");
|
pxl8_script_set_error(script, "Fennel not loaded");
|
||||||
lua_pop(script->L, 1);
|
lua_pop(script->L, 1);
|
||||||
if (basename != filename) free(basename);
|
|
||||||
return PXL8_ERROR_SCRIPT_ERROR;
|
return PXL8_ERROR_SCRIPT_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,14 +473,19 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam
|
||||||
}
|
}
|
||||||
|
|
||||||
lua_pop(script->L, 1);
|
lua_pop(script->L, 1);
|
||||||
if (basename != filename) free(basename);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) {
|
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;
|
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");
|
lua_getglobal(script->L, "fennel");
|
||||||
if (lua_isnil(script->L, -1)) {
|
if (lua_isnil(script->L, -1)) {
|
||||||
pxl8_script_set_error(script, "Fennel not loaded");
|
pxl8_script_set_error(script, "Fennel not loaded");
|
||||||
|
|
@ -434,17 +496,61 @@ pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) {
|
||||||
lua_getfield(script->L, -1, "eval");
|
lua_getfield(script->L, -1, "eval");
|
||||||
lua_pushstring(script->L, code);
|
lua_pushstring(script->L, code);
|
||||||
|
|
||||||
if (lua_pcall(script->L, 1, 1, 0) != 0) {
|
lua_newtable(script->L);
|
||||||
pxl8_script_set_error(script, lua_tostring(script->L, -1));
|
lua_pushstring(script->L, "useMetadata");
|
||||||
lua_remove(script->L, -2);
|
lua_pushboolean(script->L, true);
|
||||||
|
lua_settable(script->L, -3);
|
||||||
|
|
||||||
|
if (lua_pcall(script->L, 2, 1, 0) != 0) {
|
||||||
|
const char* error = lua_tostring(script->L, -1);
|
||||||
|
if (error) {
|
||||||
|
char cleaned_error[2048];
|
||||||
|
size_t j = 0;
|
||||||
|
for (size_t 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;
|
return PXL8_ERROR_SCRIPT_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
lua_remove(script->L, -2);
|
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';
|
script->last_error[0] = '\0';
|
||||||
return PXL8_OK;
|
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) {
|
pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name) {
|
||||||
if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER;
|
if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
|
||||||
|
|
@ -567,7 +673,6 @@ pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
|
||||||
|
|
||||||
if (result == PXL8_OK) {
|
if (result == PXL8_OK) {
|
||||||
pxl8_info("Loaded script: %s", path);
|
pxl8_info("Loaded script: %s", path);
|
||||||
pxl8_script_call_function(script, "init");
|
|
||||||
} else {
|
} else {
|
||||||
pxl8_warn("Failed to load script: %s", script->last_error);
|
pxl8_warn("Failed to load script: %s", script->last_error);
|
||||||
}
|
}
|
||||||
|
|
@ -604,21 +709,33 @@ bool pxl8_script_check_reload(pxl8_script* script) {
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <linenoise.h>
|
#include <linenoise.h>
|
||||||
|
|
||||||
#define PXL8_MAX_REPL_COMMANDS 4096
|
#define PXL8_REPL_RING_BUFFER_SIZE 64
|
||||||
|
|
||||||
struct pxl8_script_repl_command {
|
struct pxl8_script_repl_command {
|
||||||
char buffer[PXL8_MAX_REPL_COMMANDS];
|
char buffer[PXL8_MAX_REPL_COMMAND_SIZE];
|
||||||
struct pxl8_script_repl_command* next;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct pxl8_script_repl {
|
struct pxl8_script_repl {
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
pthread_mutex_t mutex;
|
pthread_mutex_t mutex;
|
||||||
pxl8_script_repl_command* queue_head;
|
pthread_cond_t cond;
|
||||||
pxl8_script_repl_command* queue_tail;
|
pxl8_script_repl_command ring_buffer[PXL8_REPL_RING_BUFFER_SIZE];
|
||||||
|
u32 head;
|
||||||
|
u32 tail;
|
||||||
bool running;
|
bool running;
|
||||||
|
bool should_quit;
|
||||||
|
bool waiting_for_eval;
|
||||||
|
char accumulator[PXL8_MAX_REPL_COMMAND_SIZE];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
static void pxl8_script_repl_completion(const char* buf, linenoiseCompletions* lc) {
|
static void pxl8_script_repl_completion(const char* buf, linenoiseCompletions* lc) {
|
||||||
const char* fennel_keywords[] = {
|
const char* fennel_keywords[] = {
|
||||||
"fn", "let", "var", "set", "global", "local",
|
"fn", "let", "var", "set", "global", "local",
|
||||||
|
|
@ -687,30 +804,49 @@ static void* pxl8_script_repl_stdin_thread(void* user_data) {
|
||||||
linenoiseSetHintsCallback(pxl8_script_repl_hints);
|
linenoiseSetHintsCallback(pxl8_script_repl_hints);
|
||||||
linenoiseHistoryLoad(history_file);
|
linenoiseHistoryLoad(history_file);
|
||||||
|
|
||||||
while (repl->running && (line = linenoise(">> "))) {
|
while (repl->running) {
|
||||||
if (strlen(line) > 0) {
|
pthread_mutex_lock(&repl->mutex);
|
||||||
linenoiseHistoryAdd(line);
|
bool in_multiline = (repl->accumulator[0] != '\0');
|
||||||
linenoiseHistorySave(history_file);
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
|
||||||
pxl8_script_repl_command* cmd = (pxl8_script_repl_command*)malloc(sizeof(pxl8_script_repl_command));
|
const char* prompt = in_multiline ? ".. " : ">> ";
|
||||||
if (cmd) {
|
line = linenoise(prompt);
|
||||||
strncpy(cmd->buffer, line, PXL8_MAX_REPL_COMMANDS - 1);
|
|
||||||
cmd->buffer[PXL8_MAX_REPL_COMMANDS - 1] = '\0';
|
|
||||||
cmd->next = NULL;
|
|
||||||
|
|
||||||
pthread_mutex_lock(&repl->mutex);
|
if (!line) break;
|
||||||
if (repl->queue_tail) {
|
|
||||||
repl->queue_tail->next = cmd;
|
if (strlen(line) > 0 || in_multiline) {
|
||||||
repl->queue_tail = cmd;
|
if (!in_multiline) {
|
||||||
} else {
|
linenoiseHistoryAdd(line);
|
||||||
repl->queue_head = repl->queue_tail = cmd;
|
linenoiseHistorySave(history_file);
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&repl->mutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
|
|
||||||
|
if (repl->accumulator[0] != '\0') {
|
||||||
|
strncat(repl->accumulator, "\n", PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);
|
||||||
|
}
|
||||||
|
strncat(repl->accumulator, line, PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1);
|
||||||
|
|
||||||
|
u32 next_tail = (repl->tail + 1) % PXL8_REPL_RING_BUFFER_SIZE;
|
||||||
|
if (next_tail != repl->head) {
|
||||||
|
strncpy(repl->ring_buffer[repl->tail].buffer, repl->accumulator, PXL8_MAX_REPL_COMMAND_SIZE - 1);
|
||||||
|
repl->ring_buffer[repl->tail].buffer[PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0';
|
||||||
|
repl->tail = next_tail;
|
||||||
|
repl->waiting_for_eval = true;
|
||||||
|
|
||||||
|
while (repl->waiting_for_eval && repl->running) {
|
||||||
|
pthread_cond_wait(&repl->cond, &repl->mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
}
|
}
|
||||||
linenoiseFree(line);
|
linenoiseFree(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
|
repl->should_quit = true;
|
||||||
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -727,38 +863,41 @@ void pxl8_script_repl_destroy(pxl8_script_repl* repl) {
|
||||||
void pxl8_script_repl_init(pxl8_script_repl* repl) {
|
void pxl8_script_repl_init(pxl8_script_repl* repl) {
|
||||||
if (!repl) return;
|
if (!repl) return;
|
||||||
|
|
||||||
repl->queue_head = NULL;
|
repl->head = 0;
|
||||||
repl->queue_tail = NULL;
|
repl->tail = 0;
|
||||||
repl->running = true;
|
repl->running = true;
|
||||||
|
repl->waiting_for_eval = false;
|
||||||
|
repl->accumulator[0] = '\0';
|
||||||
pthread_mutex_init(&repl->mutex, NULL);
|
pthread_mutex_init(&repl->mutex, NULL);
|
||||||
|
pthread_cond_init(&repl->cond, NULL);
|
||||||
pthread_create(&repl->thread, NULL, pxl8_script_repl_stdin_thread, repl);
|
pthread_create(&repl->thread, NULL, pxl8_script_repl_stdin_thread, repl);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_script_repl_shutdown(pxl8_script_repl* repl) {
|
void pxl8_script_repl_shutdown(pxl8_script_repl* repl) {
|
||||||
if (!repl) return;
|
if (!repl) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
repl->running = false;
|
repl->running = false;
|
||||||
pthread_join(repl->thread, NULL);
|
pthread_cond_signal(&repl->cond);
|
||||||
pthread_mutex_destroy(&repl->mutex);
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
|
||||||
pxl8_script_repl_command* cmd = repl->queue_head;
|
pthread_cancel(repl->thread);
|
||||||
while (cmd) {
|
pthread_join(repl->thread, NULL);
|
||||||
pxl8_script_repl_command* next = cmd->next;
|
|
||||||
free(cmd);
|
system("stty sane 2>/dev/null");
|
||||||
cmd = next;
|
|
||||||
}
|
pthread_cond_destroy(&repl->cond);
|
||||||
|
pthread_mutex_destroy(&repl->mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl) {
|
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl) {
|
||||||
if (!repl) return NULL;
|
if (!repl) return NULL;
|
||||||
|
|
||||||
pthread_mutex_lock(&repl->mutex);
|
pthread_mutex_lock(&repl->mutex);
|
||||||
pxl8_script_repl_command* cmd = repl->queue_head;
|
pxl8_script_repl_command* cmd = NULL;
|
||||||
if (cmd) {
|
if (repl->head != repl->tail) {
|
||||||
repl->queue_head = cmd->next;
|
cmd = &repl->ring_buffer[repl->head];
|
||||||
if (!repl->queue_head) {
|
repl->head = (repl->head + 1) % PXL8_REPL_RING_BUFFER_SIZE;
|
||||||
repl->queue_tail = NULL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&repl->mutex);
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
return cmd;
|
return cmd;
|
||||||
|
|
@ -768,6 +907,34 @@ const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd) {
|
||||||
return cmd ? cmd->buffer : NULL;
|
return cmd ? cmd->buffer : NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pxl8_script_repl_command_free(pxl8_script_repl_command* cmd) {
|
bool pxl8_script_repl_should_quit(pxl8_script_repl* repl) {
|
||||||
free(cmd);
|
if (!repl) return false;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
|
bool should_quit = repl->should_quit;
|
||||||
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
|
||||||
|
return should_quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_script_repl_eval_complete(pxl8_script_repl* repl) {
|
||||||
|
if (!repl) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
|
repl->waiting_for_eval = false;
|
||||||
|
pthread_cond_signal(&repl->cond);
|
||||||
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_script_repl_clear_accumulator(pxl8_script_repl* repl) {
|
||||||
|
if (!repl) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&repl->mutex);
|
||||||
|
repl->accumulator[0] = '\0';
|
||||||
|
pthread_mutex_unlock(&repl->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pxl8_script_is_incomplete_input(pxl8_script* script) {
|
||||||
|
if (!script) return false;
|
||||||
|
return pxl8_script_is_incomplete_error(script->last_error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ void pxl8_script_set_ui(pxl8_script* script, pxl8_ui* ui);
|
||||||
pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name);
|
pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name);
|
||||||
pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg);
|
pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg);
|
||||||
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code);
|
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code);
|
||||||
|
pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code);
|
||||||
pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name);
|
pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name);
|
||||||
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename);
|
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename);
|
||||||
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename);
|
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename);
|
||||||
|
|
@ -32,12 +33,17 @@ pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path);
|
||||||
bool pxl8_script_check_reload(pxl8_script* script);
|
bool pxl8_script_check_reload(pxl8_script* script);
|
||||||
|
|
||||||
pxl8_script_repl* pxl8_script_repl_create(void);
|
pxl8_script_repl* pxl8_script_repl_create(void);
|
||||||
void pxl8_script_repl_destroy(pxl8_script_repl* repl);
|
|
||||||
void pxl8_script_repl_init(pxl8_script_repl* repl);
|
void pxl8_script_repl_init(pxl8_script_repl* repl);
|
||||||
void pxl8_script_repl_shutdown(pxl8_script_repl* repl);
|
void pxl8_script_repl_shutdown(pxl8_script_repl* repl);
|
||||||
|
void pxl8_script_repl_destroy(pxl8_script_repl* repl);
|
||||||
|
|
||||||
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl);
|
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl);
|
||||||
const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd);
|
const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd);
|
||||||
void pxl8_script_repl_command_free(pxl8_script_repl_command* cmd);
|
void pxl8_script_repl_eval_complete(pxl8_script_repl* repl);
|
||||||
|
void pxl8_script_repl_clear_accumulator(pxl8_script_repl* repl);
|
||||||
|
|
||||||
|
bool pxl8_script_repl_should_quit(pxl8_script_repl* repl);
|
||||||
|
bool pxl8_script_is_incomplete_input(pxl8_script* script);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "pxl8_ase.h"
|
||||||
#include "pxl8_macros.h"
|
#include "pxl8_macros.h"
|
||||||
#include "pxl8_tilemap.h"
|
#include "pxl8_tilemap.h"
|
||||||
#include "pxl8_tilesheet.h"
|
#include "pxl8_tilesheet.h"
|
||||||
|
|
||||||
|
struct pxl8_tilesheet {
|
||||||
|
u8* data;
|
||||||
|
bool* tile_valid;
|
||||||
|
u32 height;
|
||||||
|
u32 tile_size;
|
||||||
|
u32 tiles_per_row;
|
||||||
|
u32 total_tiles;
|
||||||
|
u32 width;
|
||||||
|
pxl8_color_mode color_mode;
|
||||||
|
u32 ref_count;
|
||||||
|
pxl8_tile_animation* animations;
|
||||||
|
u32 animation_count;
|
||||||
|
pxl8_tile_properties* properties;
|
||||||
|
};
|
||||||
|
|
||||||
struct pxl8_tilemap_layer {
|
struct pxl8_tilemap_layer {
|
||||||
u32 allocated_chunks;
|
u32 allocated_chunks;
|
||||||
u32 chunk_count;
|
u32 chunk_count;
|
||||||
|
|
@ -23,6 +39,8 @@ struct pxl8_tilemap {
|
||||||
pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS];
|
pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS];
|
||||||
u32 tile_size;
|
u32 tile_size;
|
||||||
pxl8_tilesheet* tilesheet;
|
pxl8_tilesheet* tilesheet;
|
||||||
|
u32 tile_user_data_capacity;
|
||||||
|
void** tile_user_data;
|
||||||
u32 width;
|
u32 width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -63,6 +81,15 @@ pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size) {
|
||||||
tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
|
tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
|
||||||
tilemap->active_layers = 1;
|
tilemap->active_layers = 1;
|
||||||
|
|
||||||
|
tilemap->tilesheet = pxl8_tilesheet_create(tilemap->tile_size);
|
||||||
|
if (!tilemap->tilesheet) {
|
||||||
|
free(tilemap);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilemap->tile_user_data = NULL;
|
||||||
|
tilemap->tile_user_data_capacity = 0;
|
||||||
|
|
||||||
u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
|
u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
|
||||||
u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
|
u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
|
||||||
|
|
||||||
|
|
@ -79,6 +106,7 @@ pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size) {
|
||||||
for (u32 j = 0; j < i; j++) {
|
for (u32 j = 0; j < i; j++) {
|
||||||
free(tilemap->layers[j].chunks);
|
free(tilemap->layers[j].chunks);
|
||||||
}
|
}
|
||||||
|
if (tilemap->tilesheet) pxl8_tilesheet_destroy(tilemap->tilesheet);
|
||||||
free(tilemap);
|
free(tilemap);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -102,13 +130,48 @@ void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tilemap->tilesheet) {
|
if (tilemap->tilesheet) pxl8_tilesheet_unref(tilemap->tilesheet);
|
||||||
pxl8_tilesheet_unref(tilemap->tilesheet);
|
if (tilemap->tile_user_data) free(tilemap->tile_user_data);
|
||||||
}
|
|
||||||
|
|
||||||
free(tilemap);
|
free(tilemap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap) {
|
||||||
|
return tilemap ? tilemap->width : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap) {
|
||||||
|
return tilemap ? tilemap->height : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap) {
|
||||||
|
return tilemap ? tilemap->tile_size : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data) {
|
||||||
|
if (!tilemap || tile_id == 0) return;
|
||||||
|
|
||||||
|
if (tile_id >= tilemap->tile_user_data_capacity) {
|
||||||
|
u32 new_capacity = tile_id + 64;
|
||||||
|
void** new_data = realloc(tilemap->tile_user_data, new_capacity * sizeof(void*));
|
||||||
|
if (!new_data) return;
|
||||||
|
|
||||||
|
for (u32 i = tilemap->tile_user_data_capacity; i < new_capacity; i++) {
|
||||||
|
new_data[i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilemap->tile_user_data = new_data;
|
||||||
|
tilemap->tile_user_data_capacity = new_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilemap->tile_user_data[tile_id] = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id) {
|
||||||
|
if (!tilemap || tile_id == 0 || tile_id >= tilemap->tile_user_data_capacity) return NULL;
|
||||||
|
return tilemap->tile_user_data[tile_id];
|
||||||
|
}
|
||||||
|
|
||||||
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) {
|
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) {
|
||||||
if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER;
|
if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
|
||||||
|
|
@ -422,3 +485,128 @@ void pxl8_tilemap_compress(pxl8_tilemap* tilemap) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer) {
|
||||||
|
if (!tilemap || !filepath) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
if (!tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
pxl8_ase_file ase_file = {0};
|
||||||
|
pxl8_result result = pxl8_ase_load(filepath, &ase_file);
|
||||||
|
if (result != PXL8_OK) {
|
||||||
|
pxl8_error("Failed to load ASE file: %s", filepath);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ase_file.tileset_count == 0) {
|
||||||
|
pxl8_error("ASE file has no tileset - must be created as Tilemap in Aseprite: %s", filepath);
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_ase_tileset* tileset = &ase_file.tilesets[0];
|
||||||
|
|
||||||
|
if (tileset->tile_width != tilemap->tile_size || tileset->tile_height != tilemap->tile_size) {
|
||||||
|
pxl8_error("Tileset tile size (%ux%u) doesn't match tilemap tile size (%u)",
|
||||||
|
tileset->tile_width, tileset->tile_height, tilemap->tile_size);
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_info("Loading tilemap from %s: %u tiles, %ux%u each",
|
||||||
|
filepath, tileset->tile_count, tileset->tile_width, tileset->tile_height);
|
||||||
|
|
||||||
|
if (!(tileset->flags & 2)) {
|
||||||
|
pxl8_error("Tileset has no embedded tiles - external tilesets not yet supported");
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tileset->pixels) {
|
||||||
|
pxl8_error("Tileset has no pixel data");
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 tiles_per_row = 16;
|
||||||
|
u32 tilesheet_rows = (tileset->tile_count + tiles_per_row - 1) / tiles_per_row;
|
||||||
|
u32 tilesheet_width = tiles_per_row * tilemap->tile_size;
|
||||||
|
u32 tilesheet_height = tilesheet_rows * tilemap->tile_size;
|
||||||
|
|
||||||
|
if (tilemap->tilesheet->data) free(tilemap->tilesheet->data);
|
||||||
|
tilemap->tilesheet->data = calloc(tilesheet_width * tilesheet_height, 1);
|
||||||
|
if (!tilemap->tilesheet->data) {
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilemap->tilesheet->width = tilesheet_width;
|
||||||
|
tilemap->tilesheet->height = tilesheet_height;
|
||||||
|
tilemap->tilesheet->tiles_per_row = tiles_per_row;
|
||||||
|
tilemap->tilesheet->total_tiles = tileset->tile_count;
|
||||||
|
tilemap->tilesheet->color_mode = PXL8_COLOR_MODE_MEGA;
|
||||||
|
|
||||||
|
if (tilemap->tilesheet->tile_valid) free(tilemap->tilesheet->tile_valid);
|
||||||
|
tilemap->tilesheet->tile_valid = calloc(tileset->tile_count + 1, sizeof(bool));
|
||||||
|
|
||||||
|
for (u32 i = 0; i < tileset->tile_count; i++) {
|
||||||
|
u32 sheet_row = i / tiles_per_row;
|
||||||
|
u32 sheet_col = i % tiles_per_row;
|
||||||
|
u32 src_offset = i * tilemap->tile_size * tilemap->tile_size;
|
||||||
|
|
||||||
|
for (u32 y = 0; y < tilemap->tile_size; y++) {
|
||||||
|
for (u32 x = 0; x < tilemap->tile_size; x++) {
|
||||||
|
u32 dst_x = sheet_col * tilemap->tile_size + x;
|
||||||
|
u32 dst_y = sheet_row * tilemap->tile_size + y;
|
||||||
|
u32 dst_idx = dst_y * tilesheet_width + dst_x;
|
||||||
|
u32 src_idx = src_offset + y * tilemap->tile_size + x;
|
||||||
|
|
||||||
|
if (src_idx < tileset->pixels_size && dst_idx < tilesheet_width * tilesheet_height) {
|
||||||
|
tilemap->tilesheet->data[dst_idx] = tileset->pixels[src_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tilemap->tilesheet->tile_valid[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_info("Loaded %u tiles into tilesheet (%ux%u)", tileset->tile_count, tilesheet_width, tilesheet_height);
|
||||||
|
|
||||||
|
if (ase_file.frame_count == 0 || !ase_file.frames) {
|
||||||
|
pxl8_error("ASE file has no frames");
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_ase_frame* frame = &ase_file.frames[0];
|
||||||
|
pxl8_ase_cel* tilemap_cel = NULL;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < frame->cel_count; i++) {
|
||||||
|
if (frame->cels[i].cel_type == 3) {
|
||||||
|
tilemap_cel = &frame->cels[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tilemap_cel || !tilemap_cel->tilemap.tiles) {
|
||||||
|
pxl8_error("No tilemap cel found in frame 0");
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_info("Found tilemap cel: %ux%u tiles", tilemap_cel->tilemap.width, tilemap_cel->tilemap.height);
|
||||||
|
|
||||||
|
for (u32 ty = 0; ty < tilemap_cel->tilemap.height && ty < tilemap->height; ty++) {
|
||||||
|
for (u32 tx = 0; tx < tilemap_cel->tilemap.width && tx < tilemap->width; tx++) {
|
||||||
|
u32 tile_idx = ty * tilemap_cel->tilemap.width + tx;
|
||||||
|
u32 tile_value = tilemap_cel->tilemap.tiles[tile_idx];
|
||||||
|
u16 tile_id = (u16)(tile_value & tilemap_cel->tilemap.tile_id_mask);
|
||||||
|
u8 flags = 0;
|
||||||
|
if (tile_value & tilemap_cel->tilemap.x_flip_mask) flags |= PXL8_TILE_FLIP_X;
|
||||||
|
if (tile_value & tilemap_cel->tilemap.y_flip_mask) flags |= PXL8_TILE_FLIP_Y;
|
||||||
|
pxl8_tilemap_set_tile(tilemap, layer, tx, ty, tile_id, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pxl8_ase_destroy(&ase_file);
|
||||||
|
return PXL8_OK;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,13 @@ extern "C" {
|
||||||
|
|
||||||
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);
|
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);
|
||||||
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);
|
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);
|
||||||
|
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap);
|
||||||
|
pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);
|
||||||
|
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap);
|
||||||
|
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id);
|
||||||
|
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap);
|
||||||
|
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);
|
||||||
|
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data);
|
||||||
|
|
||||||
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);
|
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);
|
||||||
void pxl8_tilemap_compress(pxl8_tilemap* tilemap);
|
void pxl8_tilemap_compress(pxl8_tilemap* tilemap);
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,36 @@ u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_
|
||||||
return anim->frames[anim->current_frame];
|
return anim->frames[anim->current_frame];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels) {
|
||||||
|
if (!tilesheet || !pixels || tile_id == 0 || tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT;
|
||||||
|
if (!tilesheet->data) return PXL8_ERROR_NULL_POINTER;
|
||||||
|
|
||||||
|
u32 tile_x = (tile_id - 1) % tilesheet->tiles_per_row;
|
||||||
|
u32 tile_y = (tile_id - 1) / tilesheet->tiles_per_row;
|
||||||
|
u32 bytes_per_pixel = (tilesheet->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
|
||||||
|
|
||||||
|
for (u32 py = 0; py < tilesheet->tile_size; py++) {
|
||||||
|
for (u32 px = 0; px < tilesheet->tile_size; px++) {
|
||||||
|
u32 src_idx = py * tilesheet->tile_size + px;
|
||||||
|
u32 dst_x = tile_x * tilesheet->tile_size + px;
|
||||||
|
u32 dst_y = tile_y * tilesheet->tile_size + py;
|
||||||
|
u32 dst_idx = (dst_y * tilesheet->width + dst_x) * bytes_per_pixel;
|
||||||
|
|
||||||
|
if (bytes_per_pixel == 4) {
|
||||||
|
((u32*)tilesheet->data)[dst_idx / 4] = pixels[src_idx];
|
||||||
|
} else {
|
||||||
|
tilesheet->data[dst_idx] = pixels[src_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tilesheet->tile_valid) {
|
||||||
|
tilesheet->tile_valid[tile_id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PXL8_OK;
|
||||||
|
}
|
||||||
|
|
||||||
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id,
|
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id,
|
||||||
const pxl8_tile_properties* props) {
|
const pxl8_tile_properties* props) {
|
||||||
if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return;
|
if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ void pxl8_tilesheet_render_tile(
|
||||||
i32 y,
|
i32 y,
|
||||||
u8 flags
|
u8 flags
|
||||||
);
|
);
|
||||||
|
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels);
|
||||||
void pxl8_tilesheet_set_tile_property(
|
void pxl8_tilesheet_set_tile_property(
|
||||||
pxl8_tilesheet* tilesheet,
|
pxl8_tilesheet* tilesheet,
|
||||||
u16 tile_id,
|
u16 tile_id,
|
||||||
|
|
|
||||||
21
tools/aseprite/package.sh
Executable file
21
tools/aseprite/package.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
EXTENSION_NAME="tile-props"
|
||||||
|
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)/$EXTENSION_NAME"
|
||||||
|
ZIP_FILE="$(cd "$(dirname "$0")" && pwd)/${EXTENSION_NAME}.aseprite-extension"
|
||||||
|
|
||||||
|
echo "Creating extension package: ${EXTENSION_NAME}.aseprite-extension"
|
||||||
|
cd "$(dirname "$SOURCE_DIR")"
|
||||||
|
rm -f "$ZIP_FILE"
|
||||||
|
zip -q -r "$ZIP_FILE" "$EXTENSION_NAME"
|
||||||
|
|
||||||
|
echo "✓ Extension package created: $ZIP_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "To install in Aseprite:"
|
||||||
|
echo "1. Open Aseprite"
|
||||||
|
echo "2. Go to Edit → Preferences → Extensions"
|
||||||
|
echo "3. Click 'Add Extension'"
|
||||||
|
echo "4. Select: $ZIP_FILE"
|
||||||
|
echo "5. Restart Aseprite"
|
||||||
41
tools/aseprite/tile-props/README.md
Normal file
41
tools/aseprite/tile-props/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# pxl8 Tile Properties Extension
|
||||||
|
|
||||||
|
Aseprite extension for editing custom tile properties that are exported to pxl8 engine.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy this directory to your Aseprite extensions folder:
|
||||||
|
- **Windows**: `%APPDATA%\Aseprite\extensions\tile-properties`
|
||||||
|
- **macOS**: `~/Library/Application Support/Aseprite/extensions/tile-properties`
|
||||||
|
- **Linux**: `~/.config/aseprite/extensions/tile-properties`
|
||||||
|
|
||||||
|
2. Restart Aseprite
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open a tilemap sprite in Aseprite (File → New → Tilemap)
|
||||||
|
2. Create your tileset with tiles
|
||||||
|
3. Select a tile in the Tileset panel
|
||||||
|
4. Go to **Edit → Edit Tile Properties** (menu will be enabled when a tile is selected)
|
||||||
|
5. Add/edit custom properties:
|
||||||
|
- **Name**: Property key (e.g., `solid`, `terrain`, `move_cost`)
|
||||||
|
- **Type**: `boolean`, `number`, or `string`
|
||||||
|
- **Value**: The property value
|
||||||
|
6. Click **Apply** to save
|
||||||
|
|
||||||
|
## Example Properties
|
||||||
|
|
||||||
|
For a grass tile in a tactical RPG:
|
||||||
|
- `solid` (boolean): `false`
|
||||||
|
- `terrain` (string): `grass`
|
||||||
|
- `move_cost` (number): `1`
|
||||||
|
- `defense_bonus` (number): `0`
|
||||||
|
|
||||||
|
For a wall tile:
|
||||||
|
- `solid` (boolean): `true`
|
||||||
|
- `terrain` (string): `wall`
|
||||||
|
- `blocks_sight` (boolean): `true`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Properties are saved directly into the Aseprite file using the tile properties API. When pxl8 loads the tilemap, these properties are automatically extracted and made available in your Fennel/Lua code via `pxl8.tilemap_get_tile_data()`.
|
||||||
283
tools/aseprite/tile-props/main.lua
Normal file
283
tools/aseprite/tile-props/main.lua
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
-- pxl8 tile properties editor
|
||||||
|
-- provides a ui for editing custom properties of tilemap tiles
|
||||||
|
|
||||||
|
local DEBUG = false
|
||||||
|
|
||||||
|
local function log(msg)
|
||||||
|
if DEBUG then
|
||||||
|
print("[tile-properties] " .. msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getSelectedTile()
|
||||||
|
log("getSelectedTile() called")
|
||||||
|
|
||||||
|
if not app then
|
||||||
|
log("ERROR: app is nil!")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local sprite = app.sprite
|
||||||
|
log("sprite: " .. tostring(sprite))
|
||||||
|
if not sprite then
|
||||||
|
log("No sprite selected")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local layer = app.layer
|
||||||
|
log("layer: " .. tostring(layer))
|
||||||
|
if not layer or not layer.isTilemap then
|
||||||
|
log("Layer is not a tilemap")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local tileset = layer.tileset
|
||||||
|
log("tileset: " .. tostring(tileset))
|
||||||
|
if not tileset then
|
||||||
|
log("No tileset in layer")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local tileIndex = app.fgTile
|
||||||
|
log("tileIndex: " .. tostring(tileIndex))
|
||||||
|
if not tileIndex or tileIndex < 0 then
|
||||||
|
log("Invalid tile index")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
log("Selected tile: tileset=" .. tostring(tileset) .. ", index=" .. tostring(tileIndex))
|
||||||
|
return tileset, tileIndex
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getTileProperties(tileset, tileIndex)
|
||||||
|
log("getTileProperties() called for tile index: " .. tostring(tileIndex))
|
||||||
|
|
||||||
|
local tile = tileset:tile(tileIndex)
|
||||||
|
if not tile then
|
||||||
|
log("Could not get tile object")
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tile.properties then
|
||||||
|
log("Tile has no properties")
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = {}
|
||||||
|
for key, value in pairs(tile.properties) do
|
||||||
|
local propType = "string"
|
||||||
|
if type(value) == "boolean" then
|
||||||
|
propType = "boolean"
|
||||||
|
elseif type(value) == "number" then
|
||||||
|
propType = "number"
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(props, {
|
||||||
|
key = key,
|
||||||
|
type = propType,
|
||||||
|
value = value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
log("Found " .. #props .. " properties")
|
||||||
|
return props
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setTileProperties(tileset, tileIndex, props)
|
||||||
|
log("setTileProperties() called for tile index: " .. tostring(tileIndex))
|
||||||
|
|
||||||
|
local tile = tileset:tile(tileIndex)
|
||||||
|
if not tile then
|
||||||
|
log("Could not get tile object")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tile.properties = {}
|
||||||
|
local count = 0
|
||||||
|
for _, prop in ipairs(props) do
|
||||||
|
if prop.key and prop.key ~= "" then
|
||||||
|
local value = prop.value
|
||||||
|
if prop.type == "boolean" then
|
||||||
|
value = (value == true or value == "true")
|
||||||
|
elseif prop.type == "number" then
|
||||||
|
value = tonumber(value) or 0
|
||||||
|
end
|
||||||
|
tile.properties[prop.key] = value
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
log("Set " .. count .. " properties")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function showPropertyEditor(existingProps)
|
||||||
|
log("showPropertyEditor() called")
|
||||||
|
|
||||||
|
local tileset, tileIndex = getSelectedTile()
|
||||||
|
|
||||||
|
if not tileset then
|
||||||
|
log("No tileset selected, showing alert")
|
||||||
|
app.alert("Please select a tile in the tileset")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log("Getting properties for tile")
|
||||||
|
local properties = existingProps or getTileProperties(tileset, tileIndex)
|
||||||
|
|
||||||
|
local dlg = Dialog("Tile Properties - Tile #" .. tileIndex)
|
||||||
|
|
||||||
|
dlg:label{ text="Properties:" }
|
||||||
|
|
||||||
|
for i, prop in ipairs(properties) do
|
||||||
|
dlg:separator()
|
||||||
|
dlg:entry{
|
||||||
|
id = "key_" .. i,
|
||||||
|
label = "Name:",
|
||||||
|
text = prop.key
|
||||||
|
}
|
||||||
|
dlg:combobox{
|
||||||
|
id = "type_" .. i,
|
||||||
|
label = "Type:",
|
||||||
|
option = prop.type,
|
||||||
|
options = { "boolean", "number", "string" },
|
||||||
|
onchange = function()
|
||||||
|
-- Save all current field values and apply type conversions
|
||||||
|
for j = 1, #properties do
|
||||||
|
properties[j].key = dlg.data["key_" .. j] or properties[j].key
|
||||||
|
local newType = dlg.data["type_" .. j] or properties[j].type
|
||||||
|
local oldType = properties[j].type
|
||||||
|
|
||||||
|
-- Apply default values when type changes
|
||||||
|
if newType ~= oldType then
|
||||||
|
if newType == "boolean" then
|
||||||
|
properties[j].value = false
|
||||||
|
elseif newType == "number" then
|
||||||
|
properties[j].value = 0
|
||||||
|
else -- string
|
||||||
|
properties[j].value = ""
|
||||||
|
end
|
||||||
|
else
|
||||||
|
properties[j].value = dlg.data["value_" .. j] or properties[j].value
|
||||||
|
end
|
||||||
|
|
||||||
|
properties[j].type = newType
|
||||||
|
end
|
||||||
|
dlg:close()
|
||||||
|
showPropertyEditor(properties)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
if prop.type == "boolean" then
|
||||||
|
dlg:check{
|
||||||
|
id = "value_" .. i,
|
||||||
|
text = "",
|
||||||
|
selected = prop.value
|
||||||
|
}
|
||||||
|
elseif prop.type == "number" then
|
||||||
|
dlg:number{
|
||||||
|
id = "value_" .. i,
|
||||||
|
label = "Value:",
|
||||||
|
text = tostring(prop.value),
|
||||||
|
decimals = 0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
dlg:entry{
|
||||||
|
id = "value_" .. i,
|
||||||
|
label = "Value:",
|
||||||
|
text = tostring(prop.value)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
dlg:button{
|
||||||
|
id = "delete_" .. i,
|
||||||
|
text = "Delete",
|
||||||
|
onclick = function()
|
||||||
|
-- Save current field values before deleting
|
||||||
|
for j = 1, #properties do
|
||||||
|
properties[j].key = dlg.data["key_" .. j] or properties[j].key
|
||||||
|
properties[j].type = dlg.data["type_" .. j] or properties[j].type
|
||||||
|
properties[j].value = dlg.data["value_" .. j] or properties[j].value
|
||||||
|
end
|
||||||
|
table.remove(properties, i)
|
||||||
|
dlg:close()
|
||||||
|
showPropertyEditor(properties)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
dlg:separator()
|
||||||
|
dlg:button{
|
||||||
|
text = "Add Property",
|
||||||
|
onclick = function()
|
||||||
|
-- Save current field values before adding new property
|
||||||
|
for i = 1, #properties do
|
||||||
|
properties[i].key = dlg.data["key_" .. i] or properties[i].key
|
||||||
|
properties[i].type = dlg.data["type_" .. i] or properties[i].type
|
||||||
|
properties[i].value = dlg.data["value_" .. i] or properties[i].value
|
||||||
|
end
|
||||||
|
table.insert(properties, {
|
||||||
|
key = "",
|
||||||
|
type = "string",
|
||||||
|
value = ""
|
||||||
|
})
|
||||||
|
dlg:close()
|
||||||
|
showPropertyEditor(properties)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg:separator()
|
||||||
|
dlg:button{
|
||||||
|
text = "Apply",
|
||||||
|
onclick = function()
|
||||||
|
local newProps = {}
|
||||||
|
for i = 1, #properties do
|
||||||
|
local key = dlg.data["key_" .. i]
|
||||||
|
local propType = dlg.data["type_" .. i]
|
||||||
|
local value = dlg.data["value_" .. i]
|
||||||
|
|
||||||
|
if key and key ~= "" then
|
||||||
|
table.insert(newProps, {
|
||||||
|
key = key,
|
||||||
|
type = propType,
|
||||||
|
value = value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setTileProperties(tileset, tileIndex, newProps)
|
||||||
|
dlg:close()
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg:button{ text = "Cancel" }
|
||||||
|
|
||||||
|
dlg:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
function init(plugin)
|
||||||
|
log("=== PLUGIN INIT START ===")
|
||||||
|
|
||||||
|
if not plugin then
|
||||||
|
print("[tile-properties] ERROR: plugin is nil!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register in Sprite menu
|
||||||
|
plugin:newCommand{
|
||||||
|
id = "TilePropertiesEditor",
|
||||||
|
title = "Tile Properties",
|
||||||
|
group = "sprite_properties",
|
||||||
|
onenabled = function()
|
||||||
|
local tileset, tileIndex = getSelectedTile()
|
||||||
|
return tileset ~= nil and tileIndex ~= nil
|
||||||
|
end,
|
||||||
|
onclick = showPropertyEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Command registered in Sprite menu")
|
||||||
|
|
||||||
|
log("=== PLUGIN INIT COMPLETE ===")
|
||||||
|
end
|
||||||
|
|
||||||
|
function exit(plugin)
|
||||||
|
log("=== PLUGIN EXIT ===")
|
||||||
|
end
|
||||||
17
tools/aseprite/tile-props/package.json
Normal file
17
tools/aseprite/tile-props/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "tile-props",
|
||||||
|
"displayName": "pxl8 tile props",
|
||||||
|
"description": "Edit custom properties for tilemap tiles",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "pxl8.org"
|
||||||
|
},
|
||||||
|
"categories": ["Scripts"],
|
||||||
|
"contributes": {
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"path": "./main.lua"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue