This commit is contained in:
asrael 2025-11-01 12:39:59 -05:00
parent e862b02019
commit 0ed7fc4496
No known key found for this signature in database
GPG key ID: 2786557804DFAE24
21 changed files with 1267 additions and 148 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
.ccls-cache/
.ccls
**.DS_Store
.pxl8_history
*.aseprite-extension

View file

@ -1,3 +1,14 @@
# pxl8
## Quick Start
```bash
./pxl8.sh build # Build framework
./pxl8.sh run # Run demo
./pxl8.sh run --repl # Run demo w/ repl
```
**Note** These functions can also take an optional project arg:
> [!WARNING]
> <strong>Heavy development. So... <em>here be dragons :3</em></strong>

10
pxl8.sh
View file

@ -101,15 +101,14 @@ print_usage() {
echo " ./pxl8.sh <command> [options]"
echo
echo -e "${BOLD}COMMANDS:${NC}"
echo " aseprite Package Aseprite extensions"
echo " build Build pxl8"
echo " clean Remove build artifacts"
echo " help Show this help message"
echo " run Build and run pxl8 (optional: cart.pxc or folder)"
echo
echo " update Download/update all dependencies"
echo " vendor Fetch source for dependencies (ex. SDL3)"
echo
echo " help Show this help message"
echo
echo -e "${BOLD}OPTIONS:${NC}"
echo " --all Clean both build artifacts and dependencies"
echo " --deps Clean only dependencies"
@ -517,6 +516,11 @@ case "$COMMAND" in
update_sdl
;;
aseprite)
print_info "Packaging Aseprite extensions"
bash tools/aseprite/package.sh
;;
help|--help|-h|"")
print_usage
;;

View file

@ -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)
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_Y = 2
pxl8.TILE_SOLID = 4

View file

@ -2,6 +2,7 @@
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
#define PXL8_VERSION "0.1.0"
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
@ -122,17 +123,9 @@ pxl8_game_result pxl8_init(pxl8_game* game, i32 argc, char* argv[]) {
if (game->script_path[0] != '\0') {
pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
game->script_loaded = (result == PXL8_OK);
}
if (game->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) {
fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(game->script));
}
if (game->script_loaded && !game->repl_mode) {
pxl8_script_call_function(game->script, "init");
}
}
@ -156,14 +149,42 @@ pxl8_game_result pxl8_update(pxl8_game* game) {
pxl8_script_check_reload(game->script);
if (game->repl_mode && !game->repl_started) {
if (game->script_loaded) {
pxl8_script_call_function(game->script, "init");
}
game->repl = pxl8_script_repl_create();
if (game->repl) {
fprintf(stderr, "\033[38;2;184;187;38m[pxl8 REPL]\033[0m Fennel %s - Tab for completions, Ctrl-D to exit\n", "1.5.1");
if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) {
fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(game->script));
}
pxl8_script_repl_init(game->repl);
game->repl_started = true;
}
}
if (game->repl_mode && game->repl) {
if (pxl8_script_repl_should_quit(game->repl)) {
game->running = false;
}
pxl8_script_repl_command* cmd = pxl8_script_repl_pop_command(game->repl);
if (cmd) {
pxl8_result result = pxl8_script_eval(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 (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);
}
pxl8_script_repl_command_free(cmd);
} else {
pxl8_script_repl_clear_accumulator(game->repl);
}
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) {
if (!game) return;
pxl8_info("Shutting down");
if (game->repl_mode && game->repl) {
fprintf(stderr, "\r\033[K");
fflush(stderr);
pxl8_script_repl_shutdown(game->repl);
pxl8_script_repl_destroy(game->repl);
}
pxl8_info("Shutting down");
if (game->cart) {
pxl8_cart_unload(game->cart);
free(game->cart);

View file

@ -143,10 +143,135 @@ static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* pa
return PXL8_OK;
}
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
if (chunk_size < 9) {
static pxl8_result parse_user_data_chunk(pxl8_stream* stream, pxl8_ase_user_data* user_data) {
u32 flags = pxl8_read_u32(stream);
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->x = 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);
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;
}
cel->width = pxl8_read_u16(stream);
cel->height = pxl8_read_u16(stream);
if (dest_len != pixels_size) {
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;
u32 compressed_data_size = chunk_size - 20;
cel->tilemap.width = pxl8_read_u16(stream);
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);
if (!cel->pixel_data) {
u32 tile_count = cel->tilemap.width * cel->tilemap.height;
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;
}
const u8* compressed_data = pxl8_read_ptr(stream, compressed_data_size);
mz_ulong dest_len = pixel_data_size;
i32 result = mz_uncompress(cel->pixel_data, &dest_len, compressed_data, compressed_data_size);
if (result != MZ_OK) {
pxl8_error("Failed to decompress cel data: miniz error %d", result);
free(cel->pixel_data);
cel->pixel_data = NULL;
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
for (u32 i = 0; i < tile_count; i++) {
if (cel->tilemap.bits_per_tile == 32) {
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 4 + 0]) |
((u32)temp_buffer[i * 4 + 1] << 8) |
((u32)temp_buffer[i * 4 + 2] << 16) |
((u32)temp_buffer[i * 4 + 3] << 24);
} else if (cel->tilemap.bits_per_tile == 16) {
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) {
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixel_data_size, dest_len);
}
free(temp_buffer);
}
return PXL8_OK;
@ -261,6 +432,9 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
break;
}
u32 last_tileset_idx = 0xFFFFFFFF;
u32 tileset_tile_idx = 0;
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
u32 chunk_start = pxl8_stream_position(&stream);
@ -301,16 +475,25 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
case PXL8_ASE_CHUNK_CEL: {
pxl8_ase_cel cel = {0};
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
if (result == PXL8_OK && cel.pixel_data) {
u32 copy_width = (cel.width < frame->width) ? cel.width : frame->width;
u32 copy_height = (cel.height < frame->height) ? cel.height : frame->height;
if (result == PXL8_OK) {
frame->cels = (pxl8_ase_cel*)realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel));
if (!frame->cels) {
result = PXL8_ERROR_OUT_OF_MEMORY;
break;
}
frame->cels[frame->cel_count] = cel;
frame->cel_count++;
if (cel.cel_type == 2 && cel.image.pixels) {
u32 copy_width = (cel.image.width < frame->width) ? cel.image.width : frame->width;
u32 copy_height = (cel.image.height < frame->height) ? cel.image.height : frame->height;
for (u32 y = 0; y < copy_height; y++) {
u32 src_offset = y * cel.width;
u32 src_offset = y * cel.image.width;
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.pixel_data[src_offset + x];
u8 src_pixel = cel.image.pixels[src_offset + x];
bool is_transparent = false;
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
@ -324,7 +507,7 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
}
}
}
free(cel.pixel_data);
}
}
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);
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:
break;
}
@ -365,8 +577,17 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
if (ase_file->frames) {
for (u32 i = 0; i < ase_file->frame_count; i++) {
if (ase_file->frames[i].pixels) {
free(ase_file->frames[i].pixels);
if (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);
@ -385,5 +606,29 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
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));
}

View file

@ -8,6 +8,8 @@
#define PXL8_ASE_CHUNK_LAYER 0x2004
#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004
#define PXL8_ASE_CHUNK_PALETTE 0x2019
#define PXL8_ASE_CHUNK_TILESET 0x2023
#define PXL8_ASE_CHUNK_USER_DATA 0x2020
typedef struct pxl8_ase_header {
u32 file_size;
@ -46,9 +48,25 @@ typedef struct pxl8_ase_cel {
i16 y;
u8 opacity;
u16 cel_type;
union {
struct {
u16 width;
u16 height;
u8* pixel_data;
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;
typedef struct pxl8_ase_layer {
@ -75,8 +93,43 @@ typedef struct pxl8_ase_frame {
i16 y;
u16 duration;
u8* pixels;
u32 cel_count;
pxl8_ase_cel* cels;
} 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 {
u32 frame_count;
pxl8_ase_frame* frames;
@ -84,6 +137,8 @@ typedef struct pxl8_ase_file {
u32 layer_count;
pxl8_ase_layer* layers;
pxl8_ase_palette palette;
u32 tileset_count;
pxl8_ase_tileset* tilesets;
} pxl8_ase_file;
#ifdef __cplusplus

View file

@ -288,25 +288,20 @@ void pxl8_cart_unmount(pxl8_cart* cart) {
pxl8_info("Unmounted cart: %s", cart->name);
}
char* pxl8_cart_resolve_path(const pxl8_cart* cart, const char* relative_path) {
if (!cart || !cart->base_path || !relative_path) return NULL;
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 || !out_path || out_size == 0) return false;
char* full_path = malloc(512);
if (!full_path) return NULL;
snprintf(full_path, 512, "%s/%s", cart->base_path, relative_path);
return full_path;
i32 written = snprintf(out_path, out_size, "%s/%s", cart->base_path, relative_path);
return written >= 0 && (size_t)written < out_size;
}
bool pxl8_cart_file_exists(const pxl8_cart* cart, const char* path) {
if (!cart || !cart->base_path || !path) return false;
char* full_path = pxl8_cart_resolve_path(cart, path);
if (!full_path) return false;
char full_path[512];
if (!pxl8_cart_resolve_path(cart, path, full_path, sizeof(full_path))) return false;
bool exists = access(full_path, F_OK) == 0;
free(full_path);
return exists;
return access(full_path, F_OK) == 0;
}
pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {

View file

@ -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_mount(pxl8_cart* cart);
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_unmount(pxl8_cart* cart);

View file

@ -28,6 +28,7 @@ typedef struct pxl8_game {
f32 time;
bool repl_mode;
bool repl_started;
bool running;
bool script_loaded;
char script_path[256];

View file

@ -28,7 +28,7 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
do { \
char timestamp[16]; \
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__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
@ -38,7 +38,7 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
do { \
char timestamp[16]; \
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__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
@ -56,30 +56,33 @@ static inline void pxl8_log_timestamp(char* buffer, size_t size) {
do { \
char timestamp[16]; \
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__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
\
} while(0)
#define pxl8_warn(...) \
do { \
char timestamp[16]; \
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__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
\
} while(0)
#define pxl8_info(...) \
do { \
char timestamp[16]; \
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); \
fprintf(stdout, __VA_ARGS__); \
fprintf(stdout, "\n"); \
\
} while(0)
#ifndef pxl8_min

View file

@ -1,4 +1,5 @@
#include <dirent.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
@ -23,6 +24,49 @@ struct pxl8_script {
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 =
"typedef uint8_t u8;\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_warn(const char* msg);\n"
"\n"
"typedef u32 pxl8_tile;\n"
"typedef struct pxl8_tilemap pxl8_tilemap;\n"
"typedef struct pxl8_tilesheet pxl8_tilesheet;\n"
"pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);\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"
"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"
@ -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_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_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);\n"
"pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);\n"
"void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);\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) {
char* filename_copy = strdup(filename);
static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, size_t basename_size) {
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, '/');
*out_basename = (char*)filename;
if (last_slash) {
*last_slash = '\0';
char* script_dir = realpath(filename_copy, NULL);
char* original_cwd = getcwd(NULL, 0);
if (script_dir && original_cwd) {
char script_dir[PATH_MAX];
char original_cwd[PATH_MAX];
if (realpath(filename_copy, script_dir) && getcwd(original_cwd, sizeof(original_cwd))) {
chdir(script_dir);
pxl8_script_set_cart_path(script, script_dir, original_cwd);
*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);
free(original_cwd);
} else {
strncpy(out_basename, filename, basename_size - 1);
out_basename[basename_size - 1] = '\0';
}
free(filename_copy);
return PXL8_OK;
}
pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
char* basename;
pxl8_script_prepare_path(script, filename, &basename);
char basename[PATH_MAX];
pxl8_script_prepare_path(script, filename, basename, sizeof(basename));
pxl8_result result = PXL8_OK;
if (luaL_dofile(script->L, basename) != 0) {
@ -351,7 +410,6 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
script->last_error[0] = '\0';
}
if (basename != filename) free(basename);
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) {
if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;
char* basename;
pxl8_script_prepare_path(script, filename, &basename);
char basename[PATH_MAX];
pxl8_script_prepare_path(script, filename, basename, sizeof(basename));
lua_getglobal(script->L, "fennel");
if (lua_isnil(script->L, -1)) {
pxl8_script_set_error(script, "Fennel not loaded");
lua_pop(script->L, 1);
if (basename != filename) free(basename);
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);
if (basename != filename) free(basename);
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;
char transformed_code[PXL8_MAX_REPL_COMMAND_SIZE];
if (repl_mode) {
pxl8_script_repl_promote_locals(code, transformed_code, sizeof(transformed_code));
code = transformed_code;
}
lua_getglobal(script->L, "fennel");
if (lua_isnil(script->L, -1)) {
pxl8_script_set_error(script, "Fennel not loaded");
@ -434,17 +496,61 @@ pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) {
lua_getfield(script->L, -1, "eval");
lua_pushstring(script->L, code);
if (lua_pcall(script->L, 1, 1, 0) != 0) {
pxl8_script_set_error(script, lua_tostring(script->L, -1));
lua_remove(script->L, -2);
lua_newtable(script->L);
lua_pushstring(script->L, "useMetadata");
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;
}
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';
return PXL8_OK;
}
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) {
return pxl8_script_eval_internal(script, code, false);
}
pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code) {
return pxl8_script_eval_internal(script, code, true);
}
pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name) {
if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER;
@ -567,7 +673,6 @@ pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
if (result == PXL8_OK) {
pxl8_info("Loaded script: %s", path);
pxl8_script_call_function(script, "init");
} else {
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 <linenoise.h>
#define PXL8_MAX_REPL_COMMANDS 4096
#define PXL8_REPL_RING_BUFFER_SIZE 64
struct pxl8_script_repl_command {
char buffer[PXL8_MAX_REPL_COMMANDS];
struct pxl8_script_repl_command* next;
char buffer[PXL8_MAX_REPL_COMMAND_SIZE];
};
struct pxl8_script_repl {
pthread_t thread;
pthread_mutex_t mutex;
pxl8_script_repl_command* queue_head;
pxl8_script_repl_command* queue_tail;
pthread_cond_t cond;
pxl8_script_repl_command ring_buffer[PXL8_REPL_RING_BUFFER_SIZE];
u32 head;
u32 tail;
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) {
const char* fennel_keywords[] = {
"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);
linenoiseHistoryLoad(history_file);
while (repl->running && (line = linenoise(">> "))) {
if (strlen(line) > 0) {
while (repl->running) {
pthread_mutex_lock(&repl->mutex);
bool in_multiline = (repl->accumulator[0] != '\0');
pthread_mutex_unlock(&repl->mutex);
const char* prompt = in_multiline ? ".. " : ">> ";
line = linenoise(prompt);
if (!line) break;
if (strlen(line) > 0 || in_multiline) {
if (!in_multiline) {
linenoiseHistoryAdd(line);
linenoiseHistorySave(history_file);
pxl8_script_repl_command* cmd = (pxl8_script_repl_command*)malloc(sizeof(pxl8_script_repl_command));
if (cmd) {
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 (repl->queue_tail) {
repl->queue_tail->next = cmd;
repl->queue_tail = cmd;
} else {
repl->queue_head = repl->queue_tail = cmd;
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);
}
pthread_mutex_lock(&repl->mutex);
repl->should_quit = true;
pthread_mutex_unlock(&repl->mutex);
return NULL;
}
@ -727,38 +863,41 @@ void pxl8_script_repl_destroy(pxl8_script_repl* repl) {
void pxl8_script_repl_init(pxl8_script_repl* repl) {
if (!repl) return;
repl->queue_head = NULL;
repl->queue_tail = NULL;
repl->head = 0;
repl->tail = 0;
repl->running = true;
repl->waiting_for_eval = false;
repl->accumulator[0] = '\0';
pthread_mutex_init(&repl->mutex, NULL);
pthread_cond_init(&repl->cond, NULL);
pthread_create(&repl->thread, NULL, pxl8_script_repl_stdin_thread, repl);
}
void pxl8_script_repl_shutdown(pxl8_script_repl* repl) {
if (!repl) return;
pthread_mutex_lock(&repl->mutex);
repl->running = false;
pthread_join(repl->thread, NULL);
pthread_mutex_destroy(&repl->mutex);
pthread_cond_signal(&repl->cond);
pthread_mutex_unlock(&repl->mutex);
pxl8_script_repl_command* cmd = repl->queue_head;
while (cmd) {
pxl8_script_repl_command* next = cmd->next;
free(cmd);
cmd = next;
}
pthread_cancel(repl->thread);
pthread_join(repl->thread, NULL);
system("stty sane 2>/dev/null");
pthread_cond_destroy(&repl->cond);
pthread_mutex_destroy(&repl->mutex);
}
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl) {
if (!repl) return NULL;
pthread_mutex_lock(&repl->mutex);
pxl8_script_repl_command* cmd = repl->queue_head;
if (cmd) {
repl->queue_head = cmd->next;
if (!repl->queue_head) {
repl->queue_tail = NULL;
}
pxl8_script_repl_command* cmd = NULL;
if (repl->head != repl->tail) {
cmd = &repl->ring_buffer[repl->head];
repl->head = (repl->head + 1) % PXL8_REPL_RING_BUFFER_SIZE;
}
pthread_mutex_unlock(&repl->mutex);
return cmd;
@ -768,6 +907,34 @@ const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd) {
return cmd ? cmd->buffer : NULL;
}
void pxl8_script_repl_command_free(pxl8_script_repl_command* cmd) {
free(cmd);
bool pxl8_script_repl_should_quit(pxl8_script_repl* repl) {
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);
}

View file

@ -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_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_repl(pxl8_script* script, const char* code);
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_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);
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_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);
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
}

View file

@ -1,10 +1,26 @@
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_macros.h"
#include "pxl8_tilemap.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 {
u32 allocated_chunks;
u32 chunk_count;
@ -23,6 +39,8 @@ struct pxl8_tilemap {
pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS];
u32 tile_size;
pxl8_tilesheet* tilesheet;
u32 tile_user_data_capacity;
void** tile_user_data;
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->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_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++) {
free(tilemap->layers[j].chunks);
}
if (tilemap->tilesheet) pxl8_tilesheet_destroy(tilemap->tilesheet);
free(tilemap);
return NULL;
}
@ -102,13 +130,48 @@ void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) {
}
}
if (tilemap->tilesheet) {
pxl8_tilesheet_unref(tilemap->tilesheet);
}
if (tilemap->tilesheet) pxl8_tilesheet_unref(tilemap->tilesheet);
if (tilemap->tile_user_data) free(tilemap->tile_user_data);
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) {
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;
}

View file

@ -91,6 +91,13 @@ extern "C" {
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);
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);
void pxl8_tilemap_compress(pxl8_tilemap* tilemap);

View file

@ -270,6 +270,36 @@ u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_
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,
const pxl8_tile_properties* props) {
if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return;

View file

@ -46,6 +46,7 @@ void pxl8_tilesheet_render_tile(
i32 y,
u8 flags
);
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels);
void pxl8_tilesheet_set_tile_property(
pxl8_tilesheet* tilesheet,
u16 tile_id,

21
tools/aseprite/package.sh Executable file
View 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"

View 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()`.

View 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

View 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"
}
]
}
}