From a5583fc8c8f53f05006f83e8721ec94b2ca0480d Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:52:30 -0300 Subject: [PATCH 1/7] Maniacs Patch - Save Image command Implements the CommandManiacSaveImage function, allowing saving of screen or picture images to disk --- src/game_interpreter.cpp | 144 +++++++++++++++++++++++++++++++++++++++ src/game_interpreter.h | 1 + 2 files changed, 145 insertions(+) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 3e4842ba67..10f37ff155 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -28,6 +28,7 @@ #include "async_handler.h" #include "game_dynrpg.h" #include "filefinder.h" +#include "cache.h" #include "game_destiny.h" #include "game_map.h" #include "game_event.h" @@ -806,6 +807,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter::CommandManiacSetGameOption, 4>(com); case Cmd::Maniac_ControlStrings: return CmdSetup<&Game_Interpreter::CommandManiacControlStrings, 8>(com); + case static_cast(3026): //Maniac_SaveImage + return CmdSetup<&Game_Interpreter::CommandManiacSaveImage, 5>(com); case Cmd::Maniac_CallCommand: return CmdSetup<&Game_Interpreter::CommandManiacCallCommand, 6>(com); case Cmd::Maniac_GetGameInfo: @@ -5288,6 +5291,147 @@ bool Game_Interpreter::CommandManiacControlStrings(lcf::rpg::EventCommand const& return true; } +bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) { + if (!Player::IsPatchManiac()) { + return true; + } + + /* + TPC Structure Reference: + @img.save .screen .dst "filename" + @img.save .pic ID .static/.dynamic .opaq .dst "filename" + + Parameters: + [0] Packing: + Bits 0-3: Picture ID Mode (0: Const, 1: Var, 2: Indirect) + Bits 4-7: Filename Mode (0: Literal, 1: String/Variable) + [1] Target Type: 0 = Screen, 1 = Picture + [2] Picture ID (Value) + [3] Filename ID (Value if not literal) + [4] Flags: + Bit 0: Dynamic (1) / Static (0) + Bit 1: Opaque (1) + */ + + int target_type = com.parameters[1]; + + // Decode Filename using the mode in bits 4-7 of parameter 0 + // val_idx 3 corresponds to the .dst argument + std::string filename = ToString(CommandStringOrVariableBitfield(com, 0, 1, 3)); + + if (filename.empty()) { + Output::Warning("ManiacSaveImage: Filename is empty"); + return true; + } + + // Decode Flags + int flags = com.parameters[4]; + bool is_dynamic = (flags & 1) != 0; + bool is_opaque = (flags & 2) != 0; + + // Prepare Bitmap + BitmapRef bitmap; + + if (target_type == 0) { + // Target: Screen (.screen) + // Capture the current screen buffer + bitmap = DisplayUi->CaptureScreen(); + } + else if (target_type == 1) { + // Target: Picture (.pic) + + // Decode Picture ID using the mode in bits 0-3 of parameter 0 + int pic_id = ValueOrVariableBitfield(com, 0, 0, 2); + + if (pic_id <= 0) { + Output::Warning("ManiacSaveImage: Invalid Picture ID {}", pic_id); + return true; + } + + auto& picture = Main_Data::game_pictures->GetPicture(pic_id); + + // Retrieve bitmap from picture + // If the picture is invalid or not showing, this might be null + if (picture.sprite) { + bitmap = picture.sprite->GetBitmap(); + } + + if (bitmap && is_dynamic) { + // .dynamic: Reflect color tone, flash, and other effects + + const auto& data = picture.data; + + // Tone + auto tone = Tone((int)(data.current_red * 128 / 100), + (int)(data.current_green * 128 / 100), + (int)(data.current_blue * 128 / 100), + (int)(data.current_sat * 128 / 100)); + + if (data.flags.affected_by_tint) { + auto screen_tone = Main_Data::game_screen->GetTone(); + tone = Blend(tone, screen_tone); + } + + // Flash + Color flash = Color(); + if (data.flags.affected_by_flash) { + flash = Main_Data::game_screen->GetFlashColor(); + } + + // Flip + bool flip_x = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_x) == lcf::rpg::SavePicture::EasyRpgFlip_x; + bool flip_y = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_y) == lcf::rpg::SavePicture::EasyRpgFlip_y; + + // Apply effects + // We use the full bitmap rect to save the whole image state, including spritesheets + bitmap = Cache::SpriteEffect(bitmap, bitmap->GetRect(), flip_x, flip_y, tone, flash); + } + } + else { + Output::Warning("ManiacSaveImage: Unsupported target type {}", target_type); + return true; + } + + // Save logic + if (bitmap) { + if (is_opaque) { + // .opaq: Make transparent/semitransparent pixels opaque + // Clone to avoid modifying the original cached/displayed bitmap + bitmap = Bitmap::Create(*bitmap, bitmap->GetRect()); + + int count = bitmap->GetWidth() * bitmap->GetHeight(); + auto* pixels = static_cast(bitmap->pixels()); + + uint8_t r, g, b, a; + for (int i = 0; i < count; ++i) { + Bitmap::pixel_format.uint32_to_rgba(pixels[i], r, g, b, a); + if (a != 255) { + pixels[i] = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, 255); + } + } + } + + // Save to disk + // Ensure 'filename' has a valid extension (.png). + if (!EndsWith(Utils::LowerCase(filename), ".png")) { + filename += ".png"; + } + + auto os = FileFinder::Save().OpenOutputStream(filename); + if (os) { + bitmap->WritePNG(os); + } + else { + Output::Warning("ManiacSaveImage: Failed to open file for writing: {}", filename); + } + } + else { + Output::Debug("ManiacSaveImage: Nothing to save (Target {})", target_type); + } + + return true; +} + bool Game_Interpreter::CommandManiacCallCommand(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; diff --git a/src/game_interpreter.h b/src/game_interpreter.h index e42c44462f..29281f1d68 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -305,6 +305,7 @@ class Game_Interpreter : public Game_BaseInterpreterContext bool CommandManiacChangePictureId(lcf::rpg::EventCommand const& com); bool CommandManiacSetGameOption(lcf::rpg::EventCommand const& com); bool CommandManiacControlStrings(lcf::rpg::EventCommand const& com); + bool CommandManiacSaveImage(lcf::rpg::EventCommand const& com); bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com); bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com); bool CommandEasyRpgProcessJson(lcf::rpg::EventCommand const& com); From 13ba3f4687b62be80a15f4ae18e41e3bfc930738 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:42:47 -0300 Subject: [PATCH 2/7] Maniac SaveImage: async support --- src/game_interpreter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 10f37ff155..f1e68b78c5 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5350,6 +5350,12 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) auto& picture = Main_Data::game_pictures->GetPicture(pic_id); + if (picture.IsRequestPending()) { + picture.MakeRequestImportant(); + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + // Retrieve bitmap from picture // If the picture is invalid or not showing, this might be null if (picture.sprite) { From 6aa0c49469428f98bb95dd2a946dd91e4912d64c Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:08:42 -0300 Subject: [PATCH 3/7] Maniacs Patch - Save Image command - Sprite Support, Proper Opaque and fallback on loading image Better handle opaque and effects flags Added support for cropping spritesheet frames Updated FileFinder to use open_generic_with_fallback for image loading. --- src/filefinder.cpp | 2 +- src/game_interpreter.cpp | 75 ++++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 668c0d6fbf..9d2f7dd6c7 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -509,7 +509,7 @@ Filesystem_Stream::InputStream open_generic_with_fallback(std::string_view dir, Filesystem_Stream::InputStream FileFinder::OpenImage(std::string_view dir, std::string_view name) { DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, 1, false }; - return open_generic(dir, name, args); + return open_generic_with_fallback(dir, name, args); } Filesystem_Stream::InputStream FileFinder::OpenMusic(std::string_view name) { diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index f1e68b78c5..792309065d 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5326,7 +5326,7 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) // Decode Flags int flags = com.parameters[4]; - bool is_dynamic = (flags & 1) != 0; + bool apply_effects = (flags & 1) != 0; bool is_opaque = (flags & 2) != 0; // Prepare Bitmap @@ -5339,7 +5339,6 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) } else if (target_type == 1) { // Target: Picture (.pic) - // Decode Picture ID using the mode in bits 0-3 of parameter 0 int pic_id = ValueOrVariableBitfield(com, 0, 0, 2); @@ -5356,16 +5355,49 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) return true; } - // Retrieve bitmap from picture - // If the picture is invalid or not showing, this might be null - if (picture.sprite) { + // Retrieve bitmap + // If Opaque flag is set, prefer loading the cached image without transparency + // to recover the original background color (key color). + bool use_cached_opaque = false; + if (is_opaque && !picture.data.name.empty()) { + bool is_canvas = false; + if (picture.sprite && picture.sprite->GetBitmap()) { + // Canvas bitmaps have IDs starting with "Canvas:" + is_canvas = StartsWith(picture.sprite->GetBitmap()->GetId(), "Canvas:"); + } + // Also if it's a Window (StringPic), we can't reload from file + bool is_window = picture.data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; + + if (!is_canvas && !is_window) { + use_cached_opaque = true; + } + } + + if (use_cached_opaque) { + // Load fresh from cache with transparency disabled + bitmap = Cache::Picture(picture.data.name, false); + } + else if (picture.sprite) { bitmap = picture.sprite->GetBitmap(); } - if (bitmap && is_dynamic) { - // .dynamic: Reflect color tone, flash, and other effects + const auto& data = picture.data; + Rect src_rect; - const auto& data = picture.data; + // Calculate Spritesheet frame + if (bitmap) { + src_rect = bitmap->GetRect(); + if (picture.NumSpriteSheetFrames() > 1) { + int frame_w = bitmap->GetWidth() / data.spritesheet_cols; + int frame_h = bitmap->GetHeight() / data.spritesheet_rows; + int sx = (data.spritesheet_frame % data.spritesheet_cols) * frame_w; + int sy = (data.spritesheet_frame / data.spritesheet_cols) * frame_h; + src_rect = Rect(sx, sy, frame_w, frame_h); + } + } + + if (bitmap && apply_effects) { + // .dynamic: Reflect color tone, flash, and other effects // Tone auto tone = Tone((int)(data.current_red * 128 / 100), @@ -5388,9 +5420,12 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) bool flip_x = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_x) == lcf::rpg::SavePicture::EasyRpgFlip_x; bool flip_y = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_y) == lcf::rpg::SavePicture::EasyRpgFlip_y; - // Apply effects - // We use the full bitmap rect to save the whole image state, including spritesheets - bitmap = Cache::SpriteEffect(bitmap, bitmap->GetRect(), flip_x, flip_y, tone, flash); + // Cache::SpriteEffect creates a new bitmap based on src_rect + bitmap = Cache::SpriteEffect(bitmap, src_rect, flip_x, flip_y, tone, flash); + } + else if (bitmap && src_rect != bitmap->GetRect()) { + // .static: Crop specific cell if it's a spritesheet + bitmap = Bitmap::Create(*bitmap, src_rect); } } else { @@ -5401,18 +5436,20 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) // Save logic if (bitmap) { if (is_opaque) { - // .opaq: Make transparent/semitransparent pixels opaque + // .opaq: Force Alpha to 255 // Clone to avoid modifying the original cached/displayed bitmap bitmap = Bitmap::Create(*bitmap, bitmap->GetRect()); - int count = bitmap->GetWidth() * bitmap->GetHeight(); - auto* pixels = static_cast(bitmap->pixels()); + if (bitmap->bpp() == 4) { + int count = bitmap->GetWidth() * bitmap->GetHeight(); + auto* pixels = static_cast(bitmap->pixels()); - uint8_t r, g, b, a; - for (int i = 0; i < count; ++i) { - Bitmap::pixel_format.uint32_to_rgba(pixels[i], r, g, b, a); - if (a != 255) { - pixels[i] = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, 255); + uint8_t r, g, b, a; + for (int i = 0; i < count; ++i) { + Bitmap::pixel_format.uint32_to_rgba(pixels[i], r, g, b, a); + if (a != 255) { + pixels[i] = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, 255); + } } } } From a742e3009d5e1568ebd08032017c75733d8d3b81 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:53:50 +0100 Subject: [PATCH 4/7] BaseUi: Shorten CaptureScreen code --- src/baseui.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/baseui.cpp b/src/baseui.cpp index 486a2d5fce..e5fef264ed 100644 --- a/src/baseui.cpp +++ b/src/baseui.cpp @@ -63,9 +63,7 @@ BaseUi::BaseUi(const Game_Config& cfg) } BitmapRef BaseUi::CaptureScreen() { - BitmapRef capture = Bitmap::Create(main_surface->width(), main_surface->height(), false); - capture->BlitFast(0, 0, *main_surface, main_surface->GetRect(), Opacity::Opaque()); - return capture; + return Bitmap::Create(*main_surface, main_surface->GetRect(), false); } void BaseUi::CleanDisplay() { From af490b81ca80db612e17ad19d3601128b2f78bec Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:54:39 +0100 Subject: [PATCH 5/7] FileFinder: Correct implement open_generic_with_fallback based on find_file_with_fallback Removed the find file function as was unused --- src/filefinder.cpp | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 9d2f7dd6c7..081b10592e 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -432,23 +432,6 @@ std::string find_generic(const DirectoryTree::Args& args) { return FileFinder::Game().FindFile(args); } -std::string find_generic_with_fallback(DirectoryTree::Args& args) { - // Searches first in the Save directory (because the game could have written - // files there, then in the Game directory. - // Disable this behaviour when Game and Save are shared as this breaks the - // translation redirection. - if (Player::shared_game_and_save_directory) { - return find_generic(args); - } - - std::string found = FileFinder::Save().FindFile(args); - if (found.empty()) { - return find_generic(args); - } - - return found; -} - std::string FileFinder::FindImage(std::string_view dir, std::string_view name) { DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, 1, false }; return find_generic(args); @@ -490,16 +473,20 @@ Filesystem_Stream::InputStream open_generic(std::string_view dir, std::string_vi } Filesystem_Stream::InputStream open_generic_with_fallback(std::string_view dir, std::string_view name, DirectoryTree::Args& args) { - if (!Tr::GetCurrentTranslationId().empty()) { - auto tr_fs = Tr::GetCurrentTranslationFilesystem(); - auto is = tr_fs.OpenFile(args); - if (is) { - return is; - } + // Searches first in the Save directory (because the game could have written + // files there, then in the Game directory. + // Disable this behaviour when Game and Save are shared as this breaks the + // translation redirection. + if (Player::shared_game_and_save_directory) { + return open_generic(dir, name, args); } auto is = FileFinder::Save().OpenFile(args); - if (!is) { is = open_generic(dir, name, args); } + + if (!is) { + is = open_generic(dir, name, args); + } + if (!is) { Output::Debug("Unable to open in either Game or Save: {}/{}", dir, name); } From 818aee8389fa10d6ab07233dd1ea570ae7c8275c Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:55:09 +0100 Subject: [PATCH 6/7] ManiacSaveImage: Simplify code --- src/game_interpreter.cpp | 124 +++++++++------------------------------ src/sprite.h | 6 ++ 2 files changed, 35 insertions(+), 95 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 792309065d..ebcc816665 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5315,8 +5315,6 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) int target_type = com.parameters[1]; - // Decode Filename using the mode in bits 4-7 of parameter 0 - // val_idx 3 corresponds to the .dst argument std::string filename = ToString(CommandStringOrVariableBitfield(com, 0, 1, 3)); if (filename.empty()) { @@ -5334,12 +5332,9 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) if (target_type == 0) { // Target: Screen (.screen) - // Capture the current screen buffer bitmap = DisplayUi->CaptureScreen(); - } - else if (target_type == 1) { + } else if (target_type == 1) { // Target: Picture (.pic) - // Decode Picture ID using the mode in bits 0-3 of parameter 0 int pic_id = ValueOrVariableBitfield(com, 0, 0, 2); if (pic_id <= 0) { @@ -5355,78 +5350,36 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) return true; } - // Retrieve bitmap - // If Opaque flag is set, prefer loading the cached image without transparency - // to recover the original background color (key color). - bool use_cached_opaque = false; - if (is_opaque && !picture.data.name.empty()) { - bool is_canvas = false; - if (picture.sprite && picture.sprite->GetBitmap()) { - // Canvas bitmaps have IDs starting with "Canvas:" - is_canvas = StartsWith(picture.sprite->GetBitmap()->GetId(), "Canvas:"); - } - // Also if it's a Window (StringPic), we can't reload from file - bool is_window = picture.data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; + const auto sprite = picture.sprite.get(); - if (!is_canvas && !is_window) { - use_cached_opaque = true; - } - } - - if (use_cached_opaque) { - // Load fresh from cache with transparency disabled - bitmap = Cache::Picture(picture.data.name, false); - } - else if (picture.sprite) { + // Retrieve bitmap + if (picture.IsWindowAttached()) { + // Maniac ignores the opaque setting for String Picture + bitmap = picture.sprite->GetBitmap(); + } else if (picture.data.name.empty()) { + // Not much we can do here (also shouldn't happen normally) bitmap = picture.sprite->GetBitmap(); + } else { + // Fetch picture with correct transparency + bitmap = Cache::Picture(picture.data.name, !is_opaque); } - const auto& data = picture.data; - Rect src_rect; - - // Calculate Spritesheet frame if (bitmap) { - src_rect = bitmap->GetRect(); - if (picture.NumSpriteSheetFrames() > 1) { - int frame_w = bitmap->GetWidth() / data.spritesheet_cols; - int frame_h = bitmap->GetHeight() / data.spritesheet_rows; - int sx = (data.spritesheet_frame % data.spritesheet_cols) * frame_w; - int sy = (data.spritesheet_frame / data.spritesheet_cols) * frame_h; - src_rect = Rect(sx, sy, frame_w, frame_h); + // Determine Spritesheet frame + Rect src_rect = picture.sprite->GetSrcRect(); + + if (apply_effects) { + // .dynamic: Reflect color tone, flash, and other effects + auto tone = sprite->GetTone(); + auto flash = sprite->GetFlashEffect(); + auto flip_x = sprite->GetFlipX(); + auto flip_y = sprite->GetFlipY(); + bitmap = Cache::SpriteEffect(bitmap, src_rect, flip_x, flip_y, tone, flash); + } else if (src_rect != bitmap->GetRect()) { + // .static: Crop specific cell if it's a spritesheet + bitmap = Bitmap::Create(*bitmap, src_rect); } } - - if (bitmap && apply_effects) { - // .dynamic: Reflect color tone, flash, and other effects - - // Tone - auto tone = Tone((int)(data.current_red * 128 / 100), - (int)(data.current_green * 128 / 100), - (int)(data.current_blue * 128 / 100), - (int)(data.current_sat * 128 / 100)); - - if (data.flags.affected_by_tint) { - auto screen_tone = Main_Data::game_screen->GetTone(); - tone = Blend(tone, screen_tone); - } - - // Flash - Color flash = Color(); - if (data.flags.affected_by_flash) { - flash = Main_Data::game_screen->GetFlashColor(); - } - - // Flip - bool flip_x = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_x) == lcf::rpg::SavePicture::EasyRpgFlip_x; - bool flip_y = (data.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_y) == lcf::rpg::SavePicture::EasyRpgFlip_y; - - // Cache::SpriteEffect creates a new bitmap based on src_rect - bitmap = Cache::SpriteEffect(bitmap, src_rect, flip_x, flip_y, tone, flash); - } - else if (bitmap && src_rect != bitmap->GetRect()) { - // .static: Crop specific cell if it's a spritesheet - bitmap = Bitmap::Create(*bitmap, src_rect); - } } else { Output::Warning("ManiacSaveImage: Unsupported target type {}", target_type); @@ -5435,40 +5388,21 @@ bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) // Save logic if (bitmap) { - if (is_opaque) { - // .opaq: Force Alpha to 255 - // Clone to avoid modifying the original cached/displayed bitmap - bitmap = Bitmap::Create(*bitmap, bitmap->GetRect()); - - if (bitmap->bpp() == 4) { - int count = bitmap->GetWidth() * bitmap->GetHeight(); - auto* pixels = static_cast(bitmap->pixels()); - - uint8_t r, g, b, a; - for (int i = 0; i < count; ++i) { - Bitmap::pixel_format.uint32_to_rgba(pixels[i], r, g, b, a); - if (a != 255) { - pixels[i] = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, 255); - } - } - } - } - // Save to disk // Ensure 'filename' has a valid extension (.png). if (!EndsWith(Utils::LowerCase(filename), ".png")) { filename += ".png"; } - auto os = FileFinder::Save().OpenOutputStream(filename); + auto found_file = FileFinder::Save().FindFile(filename); + + auto os = FileFinder::Save().OpenOutputStream(found_file.empty() ? filename : found_file); if (os) { bitmap->WritePNG(os); - } - else { + } else { Output::Warning("ManiacSaveImage: Failed to open file for writing: {}", filename); } - } - else { + } else { Output::Debug("ManiacSaveImage: Nothing to save (Target {})", target_type); } diff --git a/src/sprite.h b/src/sprite.h index 7d72676a5c..c2790acca6 100644 --- a/src/sprite.h +++ b/src/sprite.h @@ -98,6 +98,8 @@ class Sprite : public Drawable { */ void SetWaverPhase(double phase); + Color GetFlashEffect() const; + /** * Set the flash effect color */ @@ -296,6 +298,10 @@ inline void Sprite::SetBushDepth(int bush_depth) { bush_effect = bush_depth; } +inline Color Sprite::GetFlashEffect() const { + return flash_effect; +} + inline void Sprite::SetFlashEffect(const Color &color) { flash_effect = color; } From 035e141491a7557c545fd4ed98d668f291384127 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:55:46 +0100 Subject: [PATCH 7/7] WritePng: Support transparent PNG images Now matches exactly what Maniacs is saving --- src/bitmap.cpp | 10 +++++++++- src/image_png.cpp | 4 ++-- src/image_png.h | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 32aa4b6557..8a203f6a12 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -184,11 +184,19 @@ bool Bitmap::WritePNG(std::ostream& os) const { auto format = PIXMAN_b8g8r8; #endif + if (GetTransparent()) { +#ifdef WORDS_BIGENDIAN + format = PIXMAN_r8g8b8a8; +#else + format = PIXMAN_a8b8g8r8; +#endif + } + auto dst = PixmanImagePtr{pixman_image_create_bits(format, width, height, &data.front(), stride)}; pixman_image_composite32(PIXMAN_OP_SRC, bitmap.get(), NULL, dst.get(), 0, 0, 0, 0, 0, 0, width, height); - return ImagePNG::Write(os, width, height, &data.front()); + return ImagePNG::Write(os, width, height, &data.front(), GetTransparent()); } size_t Bitmap::GetSize() const { diff --git a/src/image_png.cpp b/src/image_png.cpp index 4eaddcb613..776ce650b7 100644 --- a/src/image_png.cpp +++ b/src/image_png.cpp @@ -253,7 +253,7 @@ static void flush_stream(png_structp out_ptr) { reinterpret_cast(png_get_io_ptr(out_ptr))->flush(); } -bool ImagePNG::Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data) { +bool ImagePNG::Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data, bool transparent) { png_structp write = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!write) { Output::Warning("Bitmap::WritePNG: error in png_create_write"); @@ -282,7 +282,7 @@ bool ImagePNG::Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t png_set_write_fn(write, &os, &write_data, &flush_stream); png_set_IHDR(write, info, width, height, 8, - PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, + transparent ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(write, info); png_write_image(write, ptrs); diff --git a/src/image_png.h b/src/image_png.h index d8ffd112f2..54f0cc23d7 100644 --- a/src/image_png.h +++ b/src/image_png.h @@ -25,7 +25,7 @@ namespace ImagePNG { bool Read(const void* buffer, bool transparent, ImageOut& output); bool Read(Filesystem_Stream::InputStream& is, bool transparent, ImageOut& output); - bool Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data); + bool Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data, bool transparent); } #endif