From 0ed7fc4496b8d693fc8699cc26655a272b7cd3d4 Mon Sep 17 00:00:00 2001 From: asrael Date: Sat, 1 Nov 2025 12:39:59 -0500 Subject: [PATCH] wip repl --- .gitignore | 3 + README.md | 11 + pxl8.sh | 10 +- src/lua/pxl8.lua | 18 ++ src/pxl8.c | 55 +++-- src/pxl8_ase.c | 325 ++++++++++++++++++++++--- src/pxl8_ase.h | 61 ++++- src/pxl8_cart.c | 19 +- src/pxl8_cart.h | 2 +- src/pxl8_game.h | 1 + src/pxl8_macros.h | 13 +- src/pxl8_script.c | 293 +++++++++++++++++----- src/pxl8_script.h | 10 +- src/pxl8_tilemap.c | 194 ++++++++++++++- src/pxl8_tilemap.h | 7 + src/pxl8_tilesheet.c | 30 +++ src/pxl8_tilesheet.h | 1 + tools/aseprite/package.sh | 21 ++ tools/aseprite/tile-props/README.md | 41 ++++ tools/aseprite/tile-props/main.lua | 283 +++++++++++++++++++++ tools/aseprite/tile-props/package.json | 17 ++ 21 files changed, 1267 insertions(+), 148 deletions(-) create mode 100755 tools/aseprite/package.sh create mode 100644 tools/aseprite/tile-props/README.md create mode 100644 tools/aseprite/tile-props/main.lua create mode 100644 tools/aseprite/tile-props/package.json diff --git a/.gitignore b/.gitignore index 8d840a8..5a08441 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .ccls-cache/ .ccls **.DS_Store + +.pxl8_history +*.aseprite-extension diff --git a/README.md b/README.md index 4a9cc58..b8da0f3 100644 --- a/README.md +++ b/README.md @@ -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] > Heavy development. So... here be dragons :3 diff --git a/pxl8.sh b/pxl8.sh index 9d22e65..b8dd467 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -101,15 +101,14 @@ print_usage() { echo " ./pxl8.sh [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 ;; diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index ab1dec5..fe04eff 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -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 diff --git a/src/pxl8.c b/src/pxl8.c index face084..7d7e4ee 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -2,6 +2,7 @@ #define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org" #define PXL8_VERSION "0.1.0" +#include #include #include #include @@ -122,20 +123,12 @@ 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"); } } - + game->last_time = game->hal->get_ticks(); game->running = true; @@ -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) { - 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) { 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); diff --git a/src/pxl8_ase.c b/src/pxl8_ase.c index 39ca5b6..fef5a47 100644 --- a/src/pxl8_ase.c +++ b/src/pxl8_ase.c @@ -143,11 +143,136 @@ 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) { - return PXL8_ERROR_ASE_MALFORMED_CHUNK; +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); 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); 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,30 +475,39 @@ 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++; - for (u32 y = 0; y < copy_height; y++) { - u32 src_offset = y * cel.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]; - bool is_transparent = false; + 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; - if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) { - u32 color = ase_file->palette.colors[src_pixel]; - is_transparent = ((color >> 24) & 0xFF) == 0; - } + for (u32 y = 0; y < copy_height; y++) { + 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.image.pixels[src_offset + x]; + bool is_transparent = false; - if (!is_transparent) { - frame->pixels[dst_offset + x] = src_pixel; + if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) { + 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; } @@ -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)); } diff --git a/src/pxl8_ase.h b/src/pxl8_ase.h index 947b87a..f85a251 100644 --- a/src/pxl8_ase.h +++ b/src/pxl8_ase.h @@ -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; - u16 width; - u16 height; - u8* pixel_data; + + union { + 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; 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 diff --git a/src/pxl8_cart.c b/src/pxl8_cart.c index fd451fb..1d3defa 100644 --- a/src/pxl8_cart.c +++ b/src/pxl8_cart.c @@ -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) { diff --git a/src/pxl8_cart.h b/src/pxl8_cart.h index 016224b..2fae5e4 100644 --- a/src/pxl8_cart.h +++ b/src/pxl8_cart.h @@ -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); diff --git a/src/pxl8_game.h b/src/pxl8_game.h index 3f7325d..8d9b865 100644 --- a/src/pxl8_game.h +++ b/src/pxl8_game.h @@ -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]; diff --git a/src/pxl8_macros.h b/src/pxl8_macros.h index 55b180f..e379c6e 100644 --- a/src/pxl8_macros.h +++ b/src/pxl8_macros.h @@ -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 diff --git a/src/pxl8_script.c b/src/pxl8_script.c index 816a6fb..92d40e8 100644 --- a/src/pxl8_script.c +++ b/src/pxl8_script.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -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 #include -#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) { - linenoiseHistoryAdd(line); - linenoiseHistorySave(history_file); + while (repl->running) { + pthread_mutex_lock(&repl->mutex); + bool in_multiline = (repl->accumulator[0] != '\0'); + pthread_mutex_unlock(&repl->mutex); - 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; + const char* prompt = in_multiline ? ".. " : ">> "; + line = linenoise(prompt); - 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; - } - pthread_mutex_unlock(&repl->mutex); + if (!line) break; + + if (strlen(line) > 0 || in_multiline) { + if (!in_multiline) { + linenoiseHistoryAdd(line); + linenoiseHistorySave(history_file); } + + 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); } + 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); } diff --git a/src/pxl8_script.h b/src/pxl8_script.h index 8f96a2c..8f41fe4 100644 --- a/src/pxl8_script.h +++ b/src/pxl8_script.h @@ -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 } diff --git a/src/pxl8_tilemap.c b/src/pxl8_tilemap.c index f811612..0cdc76d 100644 --- a/src/pxl8_tilemap.c +++ b/src/pxl8_tilemap.c @@ -1,10 +1,26 @@ #include #include +#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; +} diff --git a/src/pxl8_tilemap.h b/src/pxl8_tilemap.h index 699113b..005f11a 100644 --- a/src/pxl8_tilemap.h +++ b/src/pxl8_tilemap.h @@ -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); diff --git a/src/pxl8_tilesheet.c b/src/pxl8_tilesheet.c index cfc6748..87b4abb 100644 --- a/src/pxl8_tilesheet.c +++ b/src/pxl8_tilesheet.c @@ -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; diff --git a/src/pxl8_tilesheet.h b/src/pxl8_tilesheet.h index 1cdb9b4..223d86d 100644 --- a/src/pxl8_tilesheet.h +++ b/src/pxl8_tilesheet.h @@ -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, diff --git a/tools/aseprite/package.sh b/tools/aseprite/package.sh new file mode 100755 index 0000000..fd28687 --- /dev/null +++ b/tools/aseprite/package.sh @@ -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" diff --git a/tools/aseprite/tile-props/README.md b/tools/aseprite/tile-props/README.md new file mode 100644 index 0000000..e64fea2 --- /dev/null +++ b/tools/aseprite/tile-props/README.md @@ -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()`. diff --git a/tools/aseprite/tile-props/main.lua b/tools/aseprite/tile-props/main.lua new file mode 100644 index 0000000..d9634cb --- /dev/null +++ b/tools/aseprite/tile-props/main.lua @@ -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 diff --git a/tools/aseprite/tile-props/package.json b/tools/aseprite/tile-props/package.json new file mode 100644 index 0000000..61e0c33 --- /dev/null +++ b/tools/aseprite/tile-props/package.json @@ -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" + } + ] + } +}