diff --git a/.gitignore b/.gitignore index 471615ab84..b9771d146e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ res/keeperfx_icon.ico /deps/*.tar.gz /cppcheck.cache /src/ver_defs.h +keeperfx.log +crash_log.txt diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 88331444dc..ef56dfc675 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,17 +1,85 @@ { "configurations": [ { - "name": "MinGW", + "name": "WSL MinGW", "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/src", + "${workspaceFolder}/deps/zlib/include", + "${workspaceFolder}/deps/spng/include", + "${workspaceFolder}/sdl/include", + "${workspaceFolder}/sdl/include/SDL2", + "${workspaceFolder}/deps/enet/include", + "${workspaceFolder}/deps/centijson/include", + "${workspaceFolder}/deps/astronomy/include", + "${workspaceFolder}/deps/openal/include", + "${workspaceFolder}/deps/luajit/include", + "${workspaceFolder}/deps/miniupnpc/include", + "${workspaceFolder}/deps/libnatpmp/include" ], - "defines": [], - "compilerPath": "/usr/bin/i686-w64-mingw32-gcc", - "cStandard": "c17", - "cppStandard": "gnu++17", + "defines": [ + "SPNG_STATIC=1", + "AL_LIBTYPE_STATIC", + "USE_PRE_FILE=1", + "BFDEBUG_LEVEL=0" + ], + "cStandard": "gnu11", + "cppStandard": "gnu++14", "intelliSenseMode": "windows-gcc-x86", - //"configurationProvider": "ms-vscode.makefile-tools", - //"compileCommands": "${workspaceFolder}/compile_commands.json", + "compileCommands": "${workspaceFolder}/compile_commands.json" + }, + { + "name": "Linux MinGW", + "includePath": [ + "${workspaceFolder}/src", + "${workspaceFolder}/deps/zlib/include", + "${workspaceFolder}/deps/spng/include", + "${workspaceFolder}/sdl/include", + "${workspaceFolder}/sdl/include/SDL2", + "${workspaceFolder}/deps/enet/include", + "${workspaceFolder}/deps/centijson/include", + "${workspaceFolder}/deps/astronomy/include", + "${workspaceFolder}/deps/openal/include", + "${workspaceFolder}/deps/luajit/include", + "${workspaceFolder}/deps/miniupnpc/include", + "${workspaceFolder}/deps/libnatpmp/include" + ], + "defines": [ + "SPNG_STATIC=1", + "AL_LIBTYPE_STATIC", + "USE_PRE_FILE=1", + "BFDEBUG_LEVEL=0" + ], + "compilerPath": "/usr/bin/i686-w64-mingw32-gcc", + "cStandard": "gnu11", + "cppStandard": "gnu++14", + "intelliSenseMode": "linux-gcc-x86" + }, + { + "name": "Windows MinGW", + "includePath": [ + "${workspaceFolder}/src", + "${workspaceFolder}/deps/zlib/include", + "${workspaceFolder}/deps/spng/include", + "${workspaceFolder}/sdl/include", + "${workspaceFolder}/sdl/include/SDL2", + "${workspaceFolder}/deps/enet/include", + "${workspaceFolder}/deps/centijson/include", + "${workspaceFolder}/deps/astronomy/include", + "${workspaceFolder}/deps/openal/include", + "${workspaceFolder}/deps/luajit/include", + "${workspaceFolder}/deps/miniupnpc/include", + "${workspaceFolder}/deps/libnatpmp/include" + ], + "defines": [ + "SPNG_STATIC=1", + "AL_LIBTYPE_STATIC", + "USE_PRE_FILE=1", + "BFDEBUG_LEVEL=0" + ], + "compilerPath": "C:\\msys64\\mingw32\\bin\\gcc.exe", + "cStandard": "gnu11", + "cppStandard": "gnu++14", + "intelliSenseMode": "windows-gcc-x86" } ], "version": 4 diff --git a/config/fxdata/lenses.cfg b/config/fxdata/lenses.cfg index 429e9628f6..78e9ae1586 100644 --- a/config/fxdata/lenses.cfg +++ b/config/fxdata/lenses.cfg @@ -2,7 +2,7 @@ ; Lens definitions are now starting. ; Note that lens can't use "Mist" and "Displacement" transformation -; at the same time - don't use both of them in one lens +; at the same time - don't use both of them in one lens. Overlay and Mist may be used together. [lens0] Name = NULL @@ -13,6 +13,9 @@ Name = LENS_WIBBLE ; For displacement kind=1, following parameters are: ; magnitude and period Displacement = 1 9 10 +; Overlay has two parameters: file name of a RAW texture (or name of a custom lens), +; Alpha transparency scaled 0..255, where 255 is fully opaque +; Overlay = frac00.raw 0 [lens2] Name = FISH_EYE @@ -26,7 +29,7 @@ Displacement = 3 1 1 [lens4] Name = MIST_WATER -; Mist effect has three parameters: file name of a RAW texture, +; Mist effect has three parameters: file name of a RAW texture (or name of a custom lens), ; lightness scaled 0..63, and ghost position (irrelevant) Mist = frac00.raw 0 0 diff --git a/src/config.c b/src/config.c index 691b47bf0e..9c174aa912 100644 --- a/src/config.c +++ b/src/config.c @@ -1008,7 +1008,7 @@ void set_defaults(const struct NamedFieldSet* named_fields_set, const char *conf named_fields_set->names[i].name = (char*)name_NamedField->field + i * named_fields_set->struct_size; named_fields_set->names[i].num = i; } - named_fields_set->names[named_fields_set->max_count - 1].name = NULL; // must be null for get_id + // Don't set terminator here - it will be set after parsing based on actual count, lens name lookups may depend on it } } @@ -1047,6 +1047,17 @@ TbBool parse_named_field_blocks(char *buf, long len, const char *config_textname parse_named_field_block(buf, len, config_textname, flags, blockname_null, named_fields_set->named_fields, named_fields_set, i); } + // Set the terminator safely within the allocated range + if (named_fields_set->names != NULL && named_fields_set->count_field != NULL) + { + int terminator_index = *named_fields_set->count_field; + if (terminator_index >= named_fields_set->max_count) + { + terminator_index = named_fields_set->max_count - 1; + } + named_fields_set->names[terminator_index].name = NULL; + } + return true; } diff --git a/src/config_lenses.c b/src/config_lenses.c index c134928795..771b90391d 100644 --- a/src/config_lenses.c +++ b/src/config_lenses.c @@ -26,6 +26,7 @@ #include "config.h" #include "thing_doors.h" +#include "custom_sprites.h" #include "keeperfx.hpp" #include "post_inc.h" @@ -49,6 +50,7 @@ const struct ConfigFileData keeper_lenses_file_data = { static int64_t value_mist(const struct NamedField* named_field, const char* value_text, const struct NamedFieldSet* named_fields_set, int idx, const char* src_str, unsigned char flags); static int64_t value_pallete(const struct NamedField* named_field, const char* value_text, const struct NamedFieldSet* named_fields_set, int idx, const char* src_str, unsigned char flags); static int64_t value_displace(const struct NamedField* named_field, const char* value_text, const struct NamedFieldSet* named_fields_set, int idx, const char* src_str, unsigned char flags); +static int64_t value_overlay(const struct NamedField* named_field, const char* value_text, const struct NamedFieldSet* named_fields_set, int idx, const char* src_str, unsigned char flags); const struct NamedField lenses_data_named_fields[] = { //name //pos //field //default //min //max //NamedCommand @@ -60,6 +62,8 @@ const struct NamedField lenses_data_named_fields[] = { {"DISPLACEMENT", 1, field(lenses_conf.lenses[0].displace_magnitude), 0, 0, 511, NULL, value_default, assign_default}, {"DISPLACEMENT", 2, field(lenses_conf.lenses[0].displace_period), 1, 0, 511, NULL, value_displace, assign_default}, {"PALETTE", 0, field(lenses_conf.lenses[0].palette), 0, 0, 0, NULL, value_pallete, assign_null}, + {"OVERLAY", 0, field(lenses_conf.lenses[0].overlay_file), 0, 0, 0, NULL, value_overlay, assign_null}, + {"OVERLAY", 1, field(lenses_conf.lenses[0].overlay_alpha), 128, 0, 255, NULL, value_default, assign_default}, {NULL}, }; @@ -107,6 +111,42 @@ static int64_t value_pallete(const struct NamedField* named_field, const char* v return 0; } +static int64_t value_overlay(const struct NamedField* named_field, const char* value_text, const struct NamedFieldSet* named_fields_set, int idx, const char* src_str, unsigned char flags) +{ + SYNCDBG (9, "value_overlay called: argnum=%d, value='%s', lens=%d", named_field->argnum, value_text, idx); + + if (value_text == NULL || value_text[0] == '\0') { + CONFWRNLOG("Empty overlay name for \"%s\" parameter in [%s%d] block of lens.cfg file.", + named_field->name, named_fields_set->block_basename, idx); + return 0; + } + + lenses_conf.lenses[idx].flags |= LCF_HasOverlay; + struct LensConfig* lenscfg = &lenses_conf.lenses[idx]; + + // Clear mist flag and mist_file if this lens only has an overlay + // This prevents garbage mist data from previous lens configurations + lenscfg->flags &= ~LCF_HasMist; + lenscfg->mist_file[0] = '\0'; + + // Only store the overlay name when processing position 0 (the name field) + // Position 1 is the alpha value, handled by value_default + if (named_field->argnum == 0) + { + // Store the overlay name (a reference name from JSON) + strncpy(lenscfg->overlay_file, value_text, DISKPATH_SIZE - 1); + lenscfg->overlay_file[DISKPATH_SIZE - 1] = '\0'; + + SYNCDBG(9, "Registered overlay name '%s' for lens %d", value_text, idx); + } + else + { + SYNCLOG("Skipping overlay name storage for argnum=%d (alpha value)", named_field->argnum); + } + + return 0; +} + static TbBool load_lenses_config_file(const char *fname, unsigned short flags) { SYNCDBG(0,"%s file \"%s\".",((flags & CnfLd_ListOnly) == 0)?"Reading":"Parsing",fname); diff --git a/src/config_lenses.h b/src/config_lenses.h index 64d7cbda27..299ae8fae5 100644 --- a/src/config_lenses.h +++ b/src/config_lenses.h @@ -29,12 +29,13 @@ extern "C" { #endif /******************************************************************************/ -#define LENS_ITEMS_MAX 32 +#define LENS_ITEMS_MAX 256 enum LensConfigFlags { LCF_HasMist = 0x01, LCF_HasDisplace = 0x02, LCF_HasPalette = 0x04, + LCF_HasOverlay = 0x08, }; struct LensConfig { @@ -47,6 +48,8 @@ struct LensConfig { short displace_kind; short displace_magnitude; short displace_period; + char overlay_file[DISKPATH_SIZE]; + short overlay_alpha; }; struct LensesConfig { diff --git a/src/creature_control.c b/src/creature_control.c index 51e9b487b0..e1f3b2183b 100644 --- a/src/creature_control.c +++ b/src/creature_control.c @@ -179,6 +179,7 @@ struct Thing *create_and_control_creature_as_controller(struct PlayerInfo *playe if (thing->class_id == TCls_Creature) { struct CreatureModelConfig* crconf = creature_stats_get_from_thing(thing); + SYNCDBG(7,"Possessing creature '%s', eye_effect=%d", crconf->name, crconf->eye_effect); setup_eye_lens(crconf->eye_effect); } } diff --git a/src/custom_sprites.c b/src/custom_sprites.c index 77d62264a9..613df6268a 100644 --- a/src/custom_sprites.c +++ b/src/custom_sprites.c @@ -85,10 +85,31 @@ static struct NamedCommand added_sprites[KEEPERSPRITE_ADD_NUM]; static struct NamedCommand added_icons[GUI_PANEL_SPRITES_NEW]; static int num_added_sprite = 0; static int num_added_icons = 0; + +#define MAX_LENS_OVERLAYS 64 +static struct LensOverlayData added_lens_overlays[MAX_LENS_OVERLAYS]; +static int num_added_lens_overlays = 0; + +#define MAX_LENS_MISTS 64 +static struct LensMistData added_lens_mists[MAX_LENS_MISTS]; +static int num_added_lens_mists = 0; + unsigned char base_pal[PALETTE_SIZE]; int total_sprite_zip_count = 0; +// Indicates what custom assets to load +enum CustomLoadFlags { + /// @brief Custom sprites + CLF_Sprites = 0x1, + /// @brief Custom icons + CLF_Icons = 0x2, + /// @brief Lens overlays + CLF_LensOverlays = 0x4, + /// @brief Lens mists + CLF_LensMists = 0x8 +}; + static unsigned char big_scratch_data[1024*1024*16] = {0}; unsigned char *big_scratch = big_scratch_data; @@ -99,6 +120,9 @@ static TbBool add_custom_sprite(const char *path); static TbBool add_custom_json(const char *path, const char *name, TbBool (*process)(const char *path, unzFile zip, VALUE *root)); +static TbBool process_lens_overlay(const char *path, unzFile zip, VALUE *root); +static TbBool process_lens_mist(const char *path, unzFile zip, VALUE *root); + static TbBool process_icon(const char *path, unzFile zip, VALUE *root); static int cmp_named_command(const void *a, const void *b); @@ -209,17 +233,27 @@ static int load_file_sprites(const char *path, const char *file_desc) int add_flag = 0; if (add_custom_sprite(path)) { - add_flag |= 0x1; + add_flag |= CLF_Sprites; } if (add_custom_json(path, "icons.json", &process_icon)) { - add_flag |= 0x2; + add_flag |= CLF_Icons; + } + + if (add_custom_json(path, "lenses.json", &process_lens_overlay)) + { + add_flag |= CLF_LensOverlays; + } + + if (add_custom_json(path, "mists.json", &process_lens_mist)) + { + add_flag |= CLF_LensMists; } if (file_desc != NULL) { - if (add_flag & 0x1) + if (add_flag & CLF_Sprites) { JUSTLOG("Loaded per-map sprites from %s", file_desc); } @@ -228,7 +262,7 @@ static int load_file_sprites(const char *path, const char *file_desc) SYNCDBG(0, "Unable to load per-map sprites from %s", file_desc); } - if (add_flag & 0x2) + if (add_flag & CLF_Icons) { JUSTLOG("Loaded per-map icons from %s", file_desc); } @@ -236,6 +270,23 @@ static int load_file_sprites(const char *path, const char *file_desc) { SYNCDBG(0, "Unable to load per-map icons from %s", file_desc); } + + if (add_flag & CLF_LensOverlays) + { + JUSTLOG("Loaded lens overlays from %s", file_desc); + } + else + { + SYNCDBG(0, "Unable to load lens overlays from %s", file_desc); + } + if (add_flag & CLF_LensMists) + { + JUSTLOG("Loaded lens mists from %s", file_desc); + } + else + { + SYNCDBG(0, "Unable to load lens mists from %s", file_desc); + } total_sprite_zip_count++; } @@ -256,9 +307,9 @@ static void load_dir_sprites(const char *dir_path, const char *dir_desc) do { sprintf(full_path, "%s/%s", dir_path, fe.Filename); int add_flag = load_file_sprites(full_path, NULL); - if (add_flag & 0x1) + if (add_flag & CLF_Sprites) cnt_sprite++; - if (add_flag & 0x2) + if (add_flag & CLF_Icons) cnt_icon++; cnt_zip++; } while (LbFileFindNext(ff, &fe) >= 0); @@ -1331,6 +1382,358 @@ add_custom_json(const char *path, const char *name, TbBool (*process)(const char return 0; } +// Forward declaration for internal PNG decoder +static unsigned char* decode_png_to_indexed_internal(unzFile zip, const char *file, const char *path, + int *out_width, int *out_height, + unz_file_info64 *zip_info, TbBool use_palette_conversion); + +// Helper function to decode PNG from ZIP file to indexed palette format +// Returns indexed data on success, NULL on failure +// Caller must free the returned data +static unsigned char* decode_png_to_indexed(unzFile zip, const char *file, const char *path, + int *out_width, int *out_height, + unz_file_info64 *zip_info) +{ + return decode_png_to_indexed_internal(zip, file, path, out_width, out_height, zip_info, true); +} + +static unsigned char* decode_png_to_indexed_no_palette(unzFile zip, const char *file, const char *path, + int *out_width, int *out_height, + unz_file_info64 *zip_info) +{ + return decode_png_to_indexed_internal(zip, file, path, out_width, out_height, zip_info, false); +} + +static unsigned char* decode_png_to_indexed_internal(unzFile zip, const char *file, const char *path, + int *out_width, int *out_height, + unz_file_info64 *zip_info, TbBool use_palette_conversion) +{ + // Only load RGB to palette conversion table if needed for color images + if (use_palette_conversion) { + load_rgb_to_pal_table(); + } + + if (zip_info->uncompressed_size > 1024 * 1024 * 4) + { + WARNLOG("PNG file too large: '%s' in '%s'", file, path); + return NULL; + } + + unsigned char *png_buffer = malloc(zip_info->uncompressed_size); + if (png_buffer == NULL) + { + ERRORLOG("Failed to allocate memory for PNG buffer"); + return NULL; + } + + if (unzReadCurrentFile(zip, png_buffer, zip_info->uncompressed_size) != zip_info->uncompressed_size) + { + WARNLOG("Failed to read '%s' from '%s'", file, path); + free(png_buffer); + return NULL; + } + + // Decode PNG using spng + spng_ctx *ctx = spng_ctx_new(0); + if (ctx == NULL) + { + ERRORLOG("Failed to create spng context"); + free(png_buffer); + return NULL; + } + + spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); + size_t limit = 1024 * 1024 * 4; + spng_set_chunk_limits(ctx, limit, limit); + + if (spng_set_png_buffer(ctx, png_buffer, zip_info->uncompressed_size)) + { + ERRORLOG("Failed to set PNG buffer for '%s'", file); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + struct spng_ihdr ihdr; + int r = spng_get_ihdr(ctx, &ihdr); + if (r) + { + ERRORLOG("spng_get_ihdr() error: %s for '%s'", spng_strerror(r), file); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + if (ihdr.width <= 0 || ihdr.height <= 0 || ihdr.width > 4096 || ihdr.height > 4096) + { + WARNLOG("Invalid image dimensions (%dx%d) in '%s'", ihdr.width, ihdr.height, file); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + // Decode to RGBA8 + size_t out_size; + int fmt = SPNG_FMT_RGBA8; + if (spng_decoded_image_size(ctx, fmt, &out_size)) + { + ERRORLOG("Failed to get decoded image size for '%s'", file); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + unsigned char *rgba_buffer = malloc(out_size); + if (rgba_buffer == NULL) + { + ERRORLOG("Failed to allocate memory for decoded image"); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + if (spng_decode_image(ctx, rgba_buffer, out_size, fmt, SPNG_DECODE_TRNS)) + { + ERRORLOG("Failed to decode PNG '%s'", file); + free(rgba_buffer); + spng_ctx_free(ctx); + free(png_buffer); + return NULL; + } + + spng_ctx_free(ctx); + free(png_buffer); + + // Convert RGBA to indexed palette format + size_t indexed_size = ihdr.width * ihdr.height; + unsigned char *indexed_data = malloc(indexed_size); + if (indexed_data == NULL) + { + ERRORLOG("Failed to allocate memory for indexed image data"); + free(rgba_buffer); + return NULL; + } + + + + // Convert RGBA to palette indices using rgb_to_pal_table + int transparent_count = 0, opaque_count = 0; + for (size_t i = 0; i < indexed_size; i++) + { + unsigned char red = rgba_buffer[i * 4 + 0]; + unsigned char green = rgba_buffer[i * 4 + 1]; + unsigned char blue = rgba_buffer[i * 4 + 2]; + unsigned char alpha = rgba_buffer[i * 4 + 3]; + + if (use_palette_conversion) + { + // Color image - convert RGB to palette index + if (alpha < 128) { + // Transparent pixel - use palette index 255 as transparency marker + indexed_data[i] = 255; + transparent_count++; + } else if (rgb_to_pal_table != NULL) { + // Use lookup table for color conversion + indexed_data[i] = rgb_to_pal_table[ + ((red >> 2) << 12) | ((green >> 2) << 6) | (blue >> 2) + ]; + opaque_count++; + } else { + // Fallback: simple grayscale conversion + indexed_data[i] = (red + green + blue) / 3; + opaque_count++; + } + } + else + { + // Data image (mist/displacement) - preserve grayscale values as-is + // Use the red channel as the index value (assuming grayscale PNG) + indexed_data[i] = red; + } + } + + free(rgba_buffer); + + *out_width = ihdr.width; + *out_height = ihdr.height; + return indexed_data; +} + +static int process_lens_overlay_from_list(const char *path, unzFile zip, int idx, VALUE *root) +{ + VALUE *val; + + val = value_dict_get(root, "name"); + if (val == NULL) + { + WARNLOG("Invalid lens overlay %s/lenses.json[%d]: no \"name\" key", path, idx); + return 0; + } + const char *name = value_string(val); + SYNCDBG(2, "found lens overlay: '%s/%s'", path, name); + + VALUE *file_value = value_dict_get(root, "file"); + if (file_value == NULL) + { + WARNLOG("Invalid lens overlay %s/lenses.json[%d]: no \"file\" key", path, idx); + return 0; + } + + const char *file = NULL; + if (value_type(file_value) == VALUE_STRING) + { + file = value_string(file_value); + } + else if (value_type(file_value) == VALUE_ARRAY && value_array_size(file_value) > 0) + { + file = value_string(value_array_get(file_value, 0)); + } + else + { + WARNLOG("Invalid lens overlay %s/lenses.json[%d]: invalid \"file\" value", path, idx); + return 0; + } + + if (fastUnzLocateFile(zip, file, 0)) + { + WARNLOG("File '%s' not found in '%s'", file, path); + return 0; + } + + unz_file_info64 zip_info = {0}; + if (UNZ_OK != unzGetCurrentFileInfo64(zip, &zip_info, NULL, 0, NULL, 0, NULL, 0)) + { + WARNLOG("Failed to get file info for '%s' in '%s'", file, path); + return 0; + } + + if (UNZ_OK != unzOpenCurrentFile(zip)) + { + return 0; + } + + // Check if this is a RAW file (256x256 = 65536 bytes) + const size_t raw_size = 256 * 256; + const char *ext = strrchr(file, '.'); + TbBool is_raw = (zip_info.uncompressed_size == raw_size) && ext && (strcasecmp(ext, ".raw") == 0); + + if (is_raw && zip_info.uncompressed_size == raw_size) + { + // Load RAW format directly (256x256 8-bit indexed palette data) + unsigned char *indexed_data = malloc(raw_size); + if (indexed_data == NULL) + { + ERRORLOG("Failed to allocate memory for RAW overlay"); + unzCloseCurrentFile(zip); + return 0; + } + + if (unzReadCurrentFile(zip, indexed_data, raw_size) != raw_size) + { + WARNLOG("Failed to read RAW file '%s' from '%s'", file, path); + free(indexed_data); + unzCloseCurrentFile(zip); + return 0; + } + + unzCloseCurrentFile(zip); + + // Check if overlay with this name already exists + struct LensOverlayData *existing = NULL; + for (int i = 0; i < num_added_lens_overlays; i++) + { + if (strcasecmp(added_lens_overlays[i].name, name) == 0) + { + existing = &added_lens_overlays[i]; + break; + } + } + + if (existing) + { + // Override existing overlay + free(existing->data); + existing->data = indexed_data; + existing->width = 256; + existing->height = 256; + } + else + { + // Add new overlay + if (num_added_lens_overlays >= MAX_LENS_OVERLAYS) + { + ERRORLOG("Too many lens overlays (max %d)", MAX_LENS_OVERLAYS); + free(indexed_data); + return 0; + } + + added_lens_overlays[num_added_lens_overlays].name = strdup(name); + added_lens_overlays[num_added_lens_overlays].data = indexed_data; + added_lens_overlays[num_added_lens_overlays].width = 256; + added_lens_overlays[num_added_lens_overlays].height = 256; + num_added_lens_overlays++; + SYNCDBG(8, "Added RAW lens overlay '%s' (256x256)", name); + } + + return 1; + } + + // PNG format handling - use shared helper + int width, height; + unsigned char *indexed_data = decode_png_to_indexed(zip, file, path, &width, &height, &zip_info); + if (indexed_data == NULL) + { + WARNLOG("Failed to decode PNG '%s' from '%s'", file, path); + return 0; + } + + if (width <= 0 || height <= 0 || width > 4096 || height > 4096) + { + WARNLOG("Invalid lens overlay dimensions (%dx%d) in '%s'", width, height, file); + free(indexed_data); + return 0; + } + + // Check if overlay with this name already exists + struct LensOverlayData *existing = NULL; + for (int i = 0; i < num_added_lens_overlays; i++) + { + if (strcasecmp(added_lens_overlays[i].name, name) == 0) + { + existing = &added_lens_overlays[i]; + break; + } + } + + if (existing) + { + // Override existing overlay + free(existing->data); + existing->data = indexed_data; + existing->width = width; + existing->height = height; + } + else + { + // Add new overlay + if (num_added_lens_overlays >= MAX_LENS_OVERLAYS) + { + ERRORLOG("Too many lens overlays (max %d)", MAX_LENS_OVERLAYS); + free(indexed_data); + return 0; + } + + added_lens_overlays[num_added_lens_overlays].name = strdup(name); + added_lens_overlays[num_added_lens_overlays].data = indexed_data; + added_lens_overlays[num_added_lens_overlays].width = width; + added_lens_overlays[num_added_lens_overlays].height = height; + num_added_lens_overlays++; + SYNCDBG(8, "Added PNG lens overlay '%s' (%dx%d)", name, width, height); + } + + return 1; +} + static int process_icon_from_list(const char *path, unzFile zip, int idx, VALUE *root) { VALUE *val; @@ -1424,6 +1827,176 @@ static int process_icon_from_list(const char *path, unzFile zip, int idx, VALUE return 1; } +static TbBool process_lens_overlay(const char *path, unzFile zip, VALUE *root) +{ + int array_size = value_array_size(root); + TbBool ret_ok = true; + for (int i = 0; i < array_size; i++) + { + VALUE *val = value_array_get(root, i); + if (!process_lens_overlay_from_list(path, zip, i, val)) + { + ret_ok = false; + continue; + } + } + return ret_ok; +} + +static int process_lens_mist_from_list(const char *path, unzFile zip, int idx, VALUE *root) +{ + VALUE *val; + + val = value_dict_get(root, "name"); + if (val == NULL) + { + WARNLOG("Invalid lens mist %s/mists.json[%d]: no \"name\" key", path, idx); + return 0; + } + const char *name = value_string(val); + SYNCDBG(2, "found lens mist: '%s/%s'", path, name); + + VALUE *file_value = value_dict_get(root, "file"); + if (file_value == NULL) + { + WARNLOG("Invalid lens mist %s/mists.json[%d]: no \"file\" key", path, idx); + return 0; + } + + const char *file = NULL; + if (value_type(file_value) == VALUE_STRING) + { + file = value_string(file_value); + } + else if (value_type(file_value) == VALUE_ARRAY && value_array_size(file_value) > 0) + { + file = value_string(value_array_get(file_value, 0)); + } + else + { + WARNLOG("Invalid lens mist %s/mists.json[%d]: invalid \"file\" value", path, idx); + return 0; + } + + if (fastUnzLocateFile(zip, file, 0)) + { + WARNLOG("File '%s' not found in '%s'", file, path); + return 0; + } + + unz_file_info64 zip_info = {0}; + if (UNZ_OK != unzGetCurrentFileInfo64(zip, &zip_info, NULL, 0, NULL, 0, NULL, 0)) + { + WARNLOG("Failed to get file info for '%s' in '%s'", file, path); + return 0; + } + + if (UNZ_OK != unzOpenCurrentFile(zip)) + { + return 0; + } + + unsigned char *mist_data = NULL; + const size_t mist_size = 256 * 256; + + // Try PNG format first + if (zip_info.uncompressed_size != mist_size) + { + // Not RAW format, try PNG + int width, height; + mist_data = decode_png_to_indexed_no_palette(zip, file, path, &width, &height, &zip_info); + if (mist_data == NULL) + { + // Already closed by decode function on failure + return 0; + } + + // Validate mist dimensions (must be 256x256) + if (width != 256 || height != 256) + { + WARNLOG("Invalid mist dimensions for '%s' in '%s': expected 256x256, got %dx%d", + file, path, width, height); + free(mist_data); + return 0; + } + + SYNCDBG(7, "Loaded PNG mist '%s' from '%s'", file, path); + } + else + { + // RAW format (256x256 = 65536 bytes) + mist_data = malloc(mist_size); + if (mist_data == NULL) + { + ERRORLOG("Failed to allocate memory for mist data"); + unzCloseCurrentFile(zip); + return 0; + } + + if (unzReadCurrentFile(zip, mist_data, mist_size) != mist_size) + { + WARNLOG("Failed to read mist file '%s' from '%s'", file, path); + free(mist_data); + unzCloseCurrentFile(zip); + return 0; + } + + unzCloseCurrentFile(zip); + SYNCDBG(7, "Loaded RAW mist '%s' from '%s'", file, path); + } + + // Check if mist with this name already exists + struct LensMistData *existing = NULL; + for (int i = 0; i < num_added_lens_mists; i++) + { + if (strcasecmp(added_lens_mists[i].name, name) == 0) + { + existing = &added_lens_mists[i]; + break; + } + } + + if (existing) + { + // Override existing mist + free(existing->data); + existing->data = mist_data; + JUSTLOG("Overriding lens mist '%s/%s'", path, name); + } + else + { + // Add new mist + if (num_added_lens_mists >= MAX_LENS_MISTS) + { + ERRORLOG("Too many lens mists (max %d)", MAX_LENS_MISTS); + free(mist_data); + return 0; + } + + added_lens_mists[num_added_lens_mists].name = strdup(name); + added_lens_mists[num_added_lens_mists].data = mist_data; + num_added_lens_mists++; + SYNCDBG(8, "Added lens mist '%s' (256x256)", name); + } + + return 1; +} + +static TbBool process_lens_mist(const char *path, unzFile zip, VALUE *root) +{ + TbBool ret_ok = true; + for (int i = 0; i < value_array_size(root); i++) + { + VALUE *val = value_array_get(root, i); + if (!process_lens_mist_from_list(path, zip, i, val)) + { + ret_ok = false; + continue; + } + } + return ret_ok; +} + static TbBool process_icon(const char *path, unzFile zip, VALUE *root) { TbBool ret_ok = true; @@ -1619,3 +2192,33 @@ int is_custom_icon(short icon_idx) icon_idx -= GUI_PANEL_SPRITES_COUNT; return (icon_idx >= 0) && (icon_idx < num_sprites(custom_sprites)); } + +const struct LensOverlayData* get_lens_overlay_data(const char *name) +{ + if (name == NULL || name[0] == '\0') + return NULL; + + for (int i = 0; i < num_added_lens_overlays; i++) + { + if (strcasecmp(added_lens_overlays[i].name, name) == 0) + { + return &added_lens_overlays[i]; + } + } + return NULL; +} + +const struct LensMistData* get_lens_mist_data(const char *name) +{ + if (name == NULL || name[0] == '\0') + return NULL; + + for (int i = 0; i < num_added_lens_mists; i++) + { + if (strcasecmp(added_lens_mists[i].name, name) == 0) + { + return &added_lens_mists[i]; + } + } + return NULL; +} diff --git a/src/custom_sprites.h b/src/custom_sprites.h index 8b3a6b624b..a89b0d85d6 100644 --- a/src/custom_sprites.h +++ b/src/custom_sprites.h @@ -42,6 +42,26 @@ const struct TbSprite *get_new_icon_sprite(short sprite_idx); const struct TbSprite *get_panel_sprite(short sprite_idx); int is_custom_icon(short icon_idx); +// Lens overlay data structure +struct LensOverlayData { + char *name; + unsigned char *data; + int width; + int height; +}; + +// Lens mist data structure +struct LensMistData { + char *name; + unsigned char *data; // 256x256 mist texture +}; + +// Get lens overlay data by name (returns NULL if not found) +const struct LensOverlayData* get_lens_overlay_data(const char *name); + +// Get lens mist data by name (returns NULL if not found) +const struct LensMistData* get_lens_mist_data(const char *name); + extern short bad_icon_id; #ifdef __cplusplus } diff --git a/src/engine_redraw.c b/src/engine_redraw.c index c43dc146fd..a495ef3c0a 100644 --- a/src/engine_redraw.c +++ b/src/engine_redraw.c @@ -629,7 +629,6 @@ void redraw_creature_view(void) ewnd.width, ewnd.height, lbDisplay.GraphicsScreenWidth); } remove_explored_flags_for_power_sight(player); - draw_swipe_graphic(); if ((game.operation_flags & GOF_ShowGui) != 0) { draw_whole_status_panel(); } diff --git a/src/lens_api.c b/src/lens_api.c index 75d6ed88c8..3348508531 100644 --- a/src/lens_api.c +++ b/src/lens_api.c @@ -20,6 +20,7 @@ #include "lens_api.h" #include +#include #include "globals.h" #include "bflib_basics.h" #include "bflib_fileio.h" @@ -31,6 +32,8 @@ #include "vidmode.h" #include "game_legacy.h" #include "config_keeperfx.h" +#include "custom_sprites.h" +#include "config_mods.h" #include "keeperfx.hpp" #include "post_inc.h" @@ -40,9 +43,92 @@ extern "C" { #endif /******************************************************************************/ +#define RAW_OVERLAY_SIZE 256 // RAW overlay files are 256x256 pixels (matching mist texture format) + +// Cache structure for lens overlay data (separate from save files) +struct LensOverlayCache { + unsigned char *data; + int width; + int height; +}; + +// Overlay cache for each lens (indexed by lens number) +static struct LensOverlayCache overlay_cache[LENS_ITEMS_MAX]; + uint32_t *eye_lens_memory; TbPixel *eye_lens_spare_screen_memory; /******************************************************************************/ +/** + * Try to load a file from mod data directories with fallback to base game. + * This is a helper function to reduce code duplication when loading files + * that support mod overrides. + * + * @param fname_base The base filename (without path) to load + * @param fgroup The file group (e.g. FGrp_StdData) + * @param buffer Pre-allocated buffer to load data into + * @param expected_size Expected file size in bytes + * @param loaded_from Optional output parameter to store which mod loaded the file (NULL if base game) + * @return true if file was loaded successfully, false otherwise + */ +static TbBool try_load_file_from_mods_with_fallback(const char* fname_base, short fgroup, + unsigned char* buffer, size_t expected_size, + const char** loaded_from) +{ + // Try loading from all loaded mods' data directories first (same order as mod loading) + for (int i = 0; i < mods_conf.after_base_cnt; i++) { + const struct ModConfigItem* mod_item = &mods_conf.after_base_item[i]; + // Only check mods that have a directory (mod_dir flag) + if (mod_item->state.mod_dir) { + char mod_dir[256]; + snprintf(mod_dir, sizeof(mod_dir), "%s/%s", MODS_DIR_NAME, mod_item->name); + char* fname_mod = prepare_file_path_mod(mod_dir, fgroup, fname_base); + + SYNCDBG(7, "CONFIG_DEBUG: Checking mod file: '%s'", fname_mod); + + // Check if file exists first to avoid error messages + if (LbFileExists(fname_mod)) { + // Check file size before loading to prevent buffer overflows + long file_size = LbFileLengthRnc(fname_mod); + SYNCDBG(7, "CONFIG_DEBUG: File exists (size: %ld bytes, expected: %lu bytes)", file_size, (unsigned long)expected_size); + + // Only load if file size matches expected size (exact match required for safety) + if (file_size == expected_size) { + long loaded = LbFileLoadAt(fname_mod, buffer); + SYNCDBG(7, "CONFIG_DEBUG: Loaded %ld bytes", loaded); + if (loaded == expected_size) { + if (loaded_from != NULL) { + *loaded_from = mod_item->name; + } + return true; + } + } else { + WARNLOG("File '%s' has wrong size: %ld bytes (expected %lu bytes)", fname_mod, file_size, (unsigned long)expected_size); + } + } + } + } + + // If not found in mods, try base game directory + char* fname_base_path = prepare_file_path(fgroup, fname_base); + if (LbFileExists(fname_base_path)) { + // Check file size before loading to prevent buffer overflows + long file_size = LbFileLengthRnc(fname_base_path); + + // Only load if file size matches expected size (exact match required for safety) + if (file_size == expected_size) { + long loaded = LbFileLoadAt(fname_base_path, buffer); + if (loaded == expected_size) { + if (loaded_from != NULL) { + *loaded_from = NULL; // NULL indicates base game + } + return true; + } + } + } + + return false; +} +/******************************************************************************/ void init_lens(uint32_t *lens_mem, int width, int height, int pitch, int nlens, int mag, int period); /******************************************************************************/ @@ -145,6 +231,131 @@ void init_lens(uint32_t *lens_mem, int width, int height, int pitch, int nlens, } } +static TbBool load_overlay_image(long lens_idx) +{ + if (lens_idx < 0 || lens_idx >= LENS_ITEMS_MAX) { + ERRORLOG("Invalid lens index %ld", lens_idx); + return false; + } + + struct LensOverlayCache* cache = &overlay_cache[lens_idx]; + if (cache->data != NULL) { + // Already loaded + return true; + } + + struct LensConfig* lenscfg = &lenses_conf.lenses[lens_idx]; + if (lenscfg->overlay_file[0] == '\0') { + WARNLOG("Empty overlay name"); + return false; + } + + // Check if this is a direct file path (contains .raw or other file extension) + const char* ext = strrchr(lenscfg->overlay_file, '.'); + if (ext != NULL && strcasecmp(ext, ".raw") == 0) { + // Load directly from file in /data directory + char* fname = prepare_file_path(FGrp_StdData, lenscfg->overlay_file); + + // RAW overlay files are 256x256 8-bit indexed (same as mist files) + cache->width = 256; + cache->height = 256; + size_t size = cache->width * cache->height; // 1 byte per pixel + + cache->data = (unsigned char*)malloc(size); + if (cache->data == NULL) { + ERRORLOG("Failed to allocate memory for overlay image"); + return false; + } + + if (LbFileLoadAt(fname, cache->data) != size) { + WARNLOG("Failed to load overlay file '%s' from /data directory", lenscfg->overlay_file); + free(cache->data); + cache->data = NULL; + cache->width = 0; + cache->height = 0; + return false; + } + + SYNCDBG(7, "Loaded overlay '%s' (%dx%d) from file", lenscfg->overlay_file, cache->width, cache->height); + return true; + } + + // Look up overlay by name in the custom sprites registry + const struct LensOverlayData* overlay = get_lens_overlay_data(lenscfg->overlay_file); + + // If not found in registry, try loading from /data directory as RAW file fallback + if (overlay == NULL) { + // Try loading RAW file from mod data directories, then base game /data + char fname_raw[256]; + snprintf(fname_raw, sizeof(fname_raw), "%s.raw", lenscfg->overlay_file); + + // RAW overlay files are 256x256 8-bit indexed (1 byte per pixel, same as mist files) + cache->width = 256; + cache->height = 256; + size_t size = cache->width * cache->height; // 65536 bytes + + cache->data = (unsigned char*)malloc(size); + if (cache->data == NULL) { + ERRORLOG("Failed to allocate memory for overlay image"); + return false; + } + + // Try loading from mods with fallback to base game + const char* loaded_from = NULL; + if (try_load_file_from_mods_with_fallback(fname_raw, FGrp_StdData, cache->data, size, &loaded_from)) { + if (loaded_from != NULL) { + SYNCDBG(7, "Loaded overlay '%s' (%dx%d) from mod '%s' data directory", fname_raw, cache->width, cache->height, loaded_from); + } else { + SYNCDBG(7, "Loaded overlay '%s' (%dx%d) from base game data directory", fname_raw, cache->width, cache->height); + } + return true; + } + + // Not found anywhere + WARNLOG("Lens overlay '%s' not found. Make sure it's defined in a lenses.json file in a .zip or provide a .raw file in /data", lenscfg->overlay_file); + free(cache->data); + cache->data = NULL; + cache->width = 0; + cache->height = 0; + return false; + } + + // Found in registry - load it + if (overlay->data == NULL || overlay->width <= 0 || overlay->height <= 0) { + WARNLOG("Invalid lens overlay data for '%s'", lenscfg->overlay_file); + return false; + } + + size_t size = overlay->width * overlay->height; + cache->data = (unsigned char*)malloc(size); + if (cache->data == NULL) { + ERRORLOG("Failed to allocate memory for overlay image"); + return false; + } + + memcpy(cache->data, overlay->data, size); + cache->width = overlay->width; + cache->height = overlay->height; + + SYNCDBG(7, "Loaded overlay '%s' (%dx%d) from registry", lenscfg->overlay_file, cache->width, cache->height); + return true; +} + +static void free_overlay_image(long lens_idx) +{ + if (lens_idx < 0 || lens_idx >= LENS_ITEMS_MAX) { + return; + } + + struct LensOverlayCache* cache = &overlay_cache[lens_idx]; + if (cache->data != NULL) { + free(cache->data); + cache->data = NULL; + cache->width = 0; + cache->height = 0; + } +} + TbBool clear_lens_palette(void) { SYNCDBG(7,"Staring"); @@ -166,6 +377,8 @@ TbBool clear_lens_palette(void) static void set_lens_palette(unsigned char *palette) { struct PlayerInfo* player = get_my_player(); + SYNCDBG(9,"CONFIG_DEBUG: set_lens_palette called, palette[0-4]: %02x %02x %02x %02x %02x", + palette[0], palette[1], palette[2], palette[3], palette[4]); player->main_palette = palette; player->lens_palette = palette; } @@ -175,6 +388,11 @@ void reset_eye_lenses(void) SYNCDBG(7,"Starting"); free_mist(); clear_lens_palette(); + // Free any loaded overlay images from cache + for (int i = 0; i < LENS_ITEMS_MAX; i++) + { + free_overlay_image(i); + } if (eye_lens_memory != NULL) { free(eye_lens_memory); @@ -245,10 +463,35 @@ void setup_eye_lens(long nlens) } struct LensConfig* lenscfg = get_lens_config(nlens); if ((lenscfg->flags & LCF_HasMist) != 0) - { - SYNCDBG(9,"Mist config entered"); - char* fname = prepare_file_path(FGrp_StdData, lenscfg->mist_file); - LbFileLoadAt(fname, eye_lens_memory); + { + // Try to load from registry first (ZIP files with mists.json) + const struct LensMistData* mist = get_lens_mist_data(lenscfg->mist_file); + + if (mist != NULL && mist->data != NULL) + { + // Load from registry + memcpy(eye_lens_memory, mist->data, 256 * 256); + SYNCDBG(7, "Loaded mist '%s' from registry", lenscfg->mist_file); + } + else + { + // Fall back to loading from mods with fallback to base game /data directory + const char* loaded_from = NULL; + if (try_load_file_from_mods_with_fallback(lenscfg->mist_file, FGrp_StdData, + (unsigned char*)eye_lens_memory, 256 * 256, &loaded_from)) + { + if (loaded_from != NULL) { + SYNCDBG(7, "Loaded mist '%s' from mod '%s' data directory", lenscfg->mist_file, loaded_from); + } else { + SYNCDBG(7, "Loaded mist '%s' from base game data directory", lenscfg->mist_file); + } + } + else + { + WARNLOG("Failed to load mist file '%s'", lenscfg->mist_file); + } + } + setup_mist((unsigned char *)eye_lens_memory, &pixmap.fade_tables[(lenscfg->mist_lightness)*256], &pixmap.ghost[(lenscfg->mist_ghost)*256]); @@ -274,8 +517,18 @@ void setup_eye_lens(long nlens) if ((lenscfg->flags & LCF_HasPalette) != 0) { SYNCDBG(9,"Palette config entered"); + WARNLOG("CONFIG_DEBUG: setup_eye_lens %ld calling set_lens_palette, flags=0x%02x", nlens, lenscfg->flags); set_lens_palette(lenscfg->palette); } + if ((lenscfg->flags & LCF_HasOverlay) != 0) + { + SYNCDBG(7, "Overlay config entered for lens %ld, name='%s'", nlens, lenscfg->overlay_file); + if (!load_overlay_image(nlens)) { + WARNLOG("Failed to load overlay for lens %ld", nlens); + } else { + SYNCDBG(7, "Successfully loaded overlay %dx%d", overlay_cache[nlens].width, overlay_cache[nlens].height); + } + } game.applied_lens_type = nlens; game.active_lens_type = nlens; } @@ -320,6 +573,83 @@ void draw_copy(unsigned char *dstbuf, long dstpitch, unsigned char *srcbuf, long } } +static void draw_overlay(unsigned char *dstbuf, long dstpitch, long width, long height, long lens_idx) +{ + if (lens_idx < 0 || lens_idx >= LENS_ITEMS_MAX) { + ERRORLOG("Invalid lens index %ld", lens_idx); + return; + } + + struct LensOverlayCache* cache = &overlay_cache[lens_idx]; + struct LensConfig* lenscfg = &lenses_conf.lenses[lens_idx]; + + if (cache->data == NULL) { + WARNLOG("Overlay data is NULL, cannot draw"); + return; + } + + // Validate dimensions + if (width <= 0 || height <= 0 || cache->width <= 0 || cache->height <= 0) { + WARNLOG("Invalid dimensions for overlay rendering: screen=%ldx%ld, overlay=%dx%d", width, height, cache->width, cache->height); + return; + } + + SYNCDBG(8, "Drawing overlay: screen=%ldx%ld, overlay=%dx%d, alpha=%d", width, height, cache->width, cache->height, lenscfg->overlay_alpha); + + // Calculate scale factors to stretch/fit overlay to fill entire viewport + float scale_x = (float)cache->width / width; + float scale_y = (float)cache->height / height; + + // Determine dithering threshold based on alpha (0-255) + // alpha=255 (opaque): draw all pixels + // alpha=128 (50%): draw checkerboard pattern + // alpha=0 (transparent): draw nothing + int alpha = lenscfg->overlay_alpha; + if (alpha < 0) alpha = 0; + if (alpha > 255) alpha = 255; + + // Draw the overlay with alpha transparency using dithering + unsigned char* dst = dstbuf; + for (long y = 0; y < height; y++) + { + int src_y = (int)(y * scale_y); + if (src_y >= cache->height) src_y = cache->height - 1; + + unsigned char* src_row = cache->data + (src_y * cache->width); + + for (long x = 0; x < width; x++) + { + int src_x = (int)(x * scale_x); + if (src_x >= cache->width) src_x = cache->width - 1; + + unsigned char overlay_pixel = src_row[src_x]; + + // Palette index 255 is used as transparency marker - skip it completely + if (overlay_pixel != 255) + { + // Apply alpha dithering using checkerboard pattern + if (alpha >= 255) + { + // Fully opaque - always draw + dst[x] = overlay_pixel; + } + else if (alpha > 0) + { + // Partial transparency - use checkerboard dithering + // Dither pattern alternates based on x+y coordinate + int dither = ((x + y) & 1) ? 255 : 0; + if (alpha > dither) + { + dst[x] = overlay_pixel; + } + } + // If alpha == 0, skip pixel (fully transparent) + } + } + dst += dstpitch; + } +} + void draw_lens_effect(unsigned char *dstbuf, long dstpitch, unsigned char *srcbuf, long srcpitch, long width, long height, long effect) { long copied = 0; @@ -355,6 +685,22 @@ void draw_lens_effect(unsigned char *dstbuf, long dstpitch, unsigned char *srcbu { // Nothing to do - palette is just set and don't have to be drawn } + // Draw overlay effect if present + if ((lenscfg->flags & LCF_HasOverlay) != 0) + { + // First, copy the source buffer to destination if not already done + if (!copied) + { + draw_copy(dstbuf, dstpitch, srcbuf, srcpitch, width, height); + } + // Load overlay if not already loaded + if (load_overlay_image(effect)) { + // Now draw the overlay on top of the game scene + SYNCDBG(8, "Calling draw_overlay (flags=%d, lens_idx=%ld)", lenscfg->flags, effect); + draw_overlay(dstbuf, dstpitch, width, height, effect); + } + copied = true; + } // If we haven't copied the buffer to screen yet, do so now if (!copied) { diff --git a/src/thing_creature.c b/src/thing_creature.c index ba2b9d40c7..1d82e3ca6b 100644 --- a/src/thing_creature.c +++ b/src/thing_creature.c @@ -286,6 +286,7 @@ TbBool control_creature_as_controller(struct PlayerInfo *player, struct Thing *t if (thing->class_id == TCls_Creature) { crconf = creature_stats_get_from_thing(thing); + SYNCDBG(7,"Controlling creature '%s', eye_effect=%d", crconf->name, crconf->eye_effect); setup_eye_lens(crconf->eye_effect); } return true; @@ -4253,6 +4254,8 @@ void draw_creature_view(struct Thing *thing) if (((game.mode_flags & MFlg_EyeLensReady) == 0) || (eye_lens_memory == NULL) || (game.applied_lens_type == 0)) { engine(player, render_cam); + // Still need to draw swipe even when no lens effect is active. + draw_swipe_graphic(); return; } // So there is an eye lens - we have to put a buffer in place of screen, @@ -4271,13 +4274,20 @@ void draw_creature_view(struct Thing *thing) // Draw on our buffer setup_engine_window(0, 0, MyScreenWidth, MyScreenHeight); engine(player, render_cam); + // Draw swipe into buffer BEFORE lens effects (so overlay renders on top of swipe) + draw_swipe_graphic(); + // Get the actual viewport dimensions (accounts for sidebar) + long view_width = player->engine_window_width / pixel_size; + long view_height = player->engine_window_height / pixel_size; + long view_x = player->engine_window_x / pixel_size; // Restore original graphics settings lbDisplay.WScreen = wscr_cp; LbScreenLoadGraphicsWindow(&grwnd); - // Draw the buffer on real screen + // Draw the buffer on real screen using actual viewport dimensions setup_engine_window(0, 0, MyScreenWidth, MyScreenHeight); - draw_lens_effect(lbDisplay.WScreen, lbDisplay.GraphicsScreenWidth, scrmem, eye_lens_width, - MyScreenWidth/pixel_size, MyScreenHeight/pixel_size, game.applied_lens_type); + // Apply lens effect to the viewport area only (not including sidebar) + draw_lens_effect(lbDisplay.WScreen + view_x, lbDisplay.GraphicsScreenWidth, + scrmem + view_x, eye_lens_width, view_width, view_height, game.applied_lens_type); } struct Thing *get_creature_near_for_controlling(PlayerNumber plyr_idx, MapCoord x, MapCoord y)