From a2863dd429ce67cc691198d808a9742574c84ac1 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 01/19] 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 1bbe7877e3..472e51fe16 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 e77563706cfa45fc87741fbc0f82d3f0887bf8de 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 02/19] 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 472e51fe16..9c249203dd 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 892e35cef9d72dbf915bf7e88b7278fdfb342226 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 03/19] 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 6f310c93c6..53afaed013 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 9c249203dd..7891fc913d 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 d6ccc8f583b1c5aafc88aebb16153e29d0ad308b Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:53:50 +0100 Subject: [PATCH 04/19] 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 e63832be00c9141876c3cc609b705cd04f5eee51 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:54:39 +0100 Subject: [PATCH 05/19] 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 53afaed013..97e51e5aaf 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 c92e28eb84affc96f12598e5d003bf91e636c031 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:55:09 +0100 Subject: [PATCH 06/19] 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 7891fc913d..31218d8be5 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 a0f399f330ed9f912a378c94abc597b00c12f6d5 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 11 Feb 2026 18:55:46 +0100 Subject: [PATCH 07/19] 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 From c69f6a9b659609697a7faa93a5d5894a88e4a136 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 20 Feb 2026 21:29:10 +0100 Subject: [PATCH 08/19] Cmd3026 already has a constant: WritePicture --- src/game_interpreter.cpp | 6 +++--- src/game_interpreter.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 31218d8be5..e92113a9c4 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -807,8 +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_WritePicture: + return CmdSetup<&Game_Interpreter::CommandManiacWritePicture, 5>(com); case Cmd::Maniac_CallCommand: return CmdSetup<&Game_Interpreter::CommandManiacCallCommand, 6>(com); case Cmd::Maniac_GetGameInfo: @@ -5291,7 +5291,7 @@ bool Game_Interpreter::CommandManiacControlStrings(lcf::rpg::EventCommand const& return true; } -bool Game_Interpreter::CommandManiacSaveImage(lcf::rpg::EventCommand const& com) { +bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; } diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 29281f1d68..3440aad8ca 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -305,7 +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 CommandManiacWritePicture(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 b0374682192cccbb0b82f62ab94c0f51995588a9 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:26:10 -0300 Subject: [PATCH 09/19] Maniacs Patch - GetPictureInfo (pixel data extraction) Adds support for extracting raw pixel data from pictures, including window-type pictures with forced refresh. Optimizes out-of-bounds handling to preserve variable values and ensures only 32-bit bitmaps are processed. Existing logic for info types 0, 1, and 2 remains unchanged. --- src/game_interpreter.cpp | 91 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index e92113a9c4..27a2f9dc46 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4681,9 +4681,12 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& return true; } - int pic_id = ValueOrVariable(com.parameters[0], com.parameters[3]); + int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]); + + // 1. Validate Picture existence auto& pic = Main_Data::game_pictures->GetPicture(pic_id); + // 2. Handle Async Loading if (pic.IsRequestPending()) { // Cannot do anything useful here without the dimensions pic.MakeRequestImportant(); @@ -4692,6 +4695,92 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& } const auto& data = pic.data; + int info_type = com.parameters[1]; + + // --- Type 3: Pixel Data Extraction --- + if (info_type == 3) { + // Verify Sprite existence + auto* sprite = pic.sprite.get(); + if (!sprite) return true; + + auto bitmap = sprite->GetBitmap(); + if (!bitmap) return true; + + // If this is a Window (String Picture), the visual content is generated by the Window class. + // We force a refresh/draw cycle here to ensure we read the actual text/window graphics. + if (data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window) { + const auto& window = Main_Data::game_windows->GetWindow(pic_id); + if (window.window) { + bitmap->Clear(); + window.window->Draw(*bitmap); + } + } + + // 3. Decode Parameters + int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); + int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); + int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); + int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[7]); + int dest_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[8]); + + int flags = com.parameters[2]; + bool ignore_alpha = (flags & 2) != 0; + + int bmp_w = bitmap->GetWidth(); + int bmp_h = bitmap->GetHeight(); + + // 4. Prepare for Raw Access + if (bitmap->bpp() != 4) { + Output::Debug("GetPictureInfo: Pixel read supported on 32-bit bitmaps only."); + return true; + } + + uint32_t* pixels = static_cast(bitmap->pixels()); + int pitch = bitmap->pitch() / 4; + + auto& variables = *Main_Data::game_variables; + + int var_idx = 0; + uint8_t r, g, b, a; + + // 5. Read Loop + for (int iy = 0; iy < h; ++iy) { + int by = y + iy; + + // If row out of bounds, skip variables for this row. + // Do NOT write 0. This preserves the existing values in the variables. + if (by < 0 || by >= bmp_h) { + var_idx += w; + continue; + } + + for (int ix = 0; ix < w; ++ix) { + int target_var = dest_var_id + var_idx++; + int bx = x + ix; + + // Only write if inside bounds. + if (bx >= 0 && bx < bmp_w) { + uint32_t raw_pixel = pixels[by * pitch + bx]; + + Bitmap::pixel_format.uint32_to_rgba(raw_pixel, r, g, b, a); + + if (ignore_alpha) { + a = 0; + } + + // Maniacs format: AARRGGBB + uint32_t packed = (a << 24) | (r << 16) | (g << 8) | b; + variables.Set(target_var, static_cast(packed)); + } + // Else: Out of bounds. Do nothing. + } + } + + Game_Map::SetNeedRefresh(true); + return true; + } + + // --- Logic for Info Types 0, 1, 2 --- int x = 0; int y = 0; From a87d9f24cbcec6d0d362e65b519d81109d3ca18e Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:42:14 -0300 Subject: [PATCH 10/19] Maniacs Patch - GetGameInfo (pixel data extraction Added support for the 'Pixel Info' option in the CommandManiacGetGameInfo function. This captures a screen region, extracts pixel data as packed AARRGGBB values, and stores them in game variables, matching Maniacs behavior. Out-of-bounds pixels are skipped, and alpha is forced to opaque. --- src/game_interpreter.cpp | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 27a2f9dc46..e8382d02d6 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4206,9 +4206,71 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co Main_Data::game_variables->Set(var + 1, Player::screen_height); break; case 3: // Get pixel info - // FIXME: figure out how 'Pixel info' works - Output::Warning("GetGameInfo: Option 'Pixel Info' not implemented."); + { + // 1. Decode Parameters + // [0] Packing: Bits 0-3: X Mode, 4-7: Y Mode, etc. + int p_x = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]); + int p_y = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); + int p_w = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); + int p_h = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); + int dest_var_id = com.parameters[7]; + + // Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB) + bool ignore_alpha = (com.parameters[2] & 1) != 0; + + // 2. Capture Screen + // Creates a snapshot of the current frame + BitmapRef screen = DisplayUi->CaptureScreen(); + if (!screen) return true; + + + int screen_w = screen->GetWidth(); + int screen_h = screen->GetHeight(); + + if (p_w <= 0 || p_h <= 0) return true; + + uint32_t* pixels = static_cast(screen->pixels()); + int pitch = screen->pitch() / 4; // stride in uint32 elements + + auto& variables = *Main_Data::game_variables; + + int var_idx = 0; + uint8_t r, g, b, a; + + // 4. Read Loop + for (int iy = 0; iy < p_h; ++iy) { + int sy = p_y + iy; + + // If row out of bounds, skip variables for this row + if (sy < 0 || sy >= screen_h) { + var_idx += p_w; + continue; + } + + for (int ix = 0; ix < p_w; ++ix) { + int target_var = dest_var_id + var_idx++; + int sx = p_x + ix; + + if (sx >= 0 && sx < screen_w) { + uint32_t raw_pixel = pixels[sy * pitch + sx]; + + Bitmap::opaque_pixel_format.uint32_to_rgba(raw_pixel, r, g, b, a); + + if (ignore_alpha) { + a = 0; + } + else { + a = 255; //In maniacs, bellow parallax layer can't be transparent is this the best approach? + } + + // Maniacs format: AARRGGBB (Signed 32-bit) + uint32_t packed = (a << 24) | (r << 16) | (g << 8) | b; + variables.Set(target_var, static_cast(packed)); + } + } + } break; + } case 4: // Get command interpreter state { // Parameter "Nest" in the English version of Maniacs From 95383063704e2f24fc669027deb25f1760448704 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:16:47 -0300 Subject: [PATCH 11/19] Maniacs Patch - SetPicturePixel Command Implements the CommandManiacSetPicturePixel function to allow direct pixel manipulation of game pictures via event commands. Handles bitmap uniqueness, format conversion, and window picture detachment to ensure safe editing. Updates command dispatch logic and header declaration. --- src/game_interpreter.cpp | 134 +++++++++++++++++++++++++++++++++++++++ src/game_interpreter.h | 1 + 2 files changed, 135 insertions(+) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index e8382d02d6..d95a86f46b 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -70,6 +70,8 @@ #include "transition.h" #include "baseui.h" #include "algo.h" +#include "sprite_picture.h" +#include "bitmap.h" using namespace Game_Interpreter_Shared; @@ -813,6 +815,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter::CommandManiacCallCommand, 6>(com); case Cmd::Maniac_GetGameInfo: return CmdSetup<&Game_Interpreter::CommandManiacGetGameInfo, 8>(com); + case static_cast(3025): + return CmdSetup<&Game_Interpreter::CommandManiacSetPicturePixel, 8>(com); case Cmd::EasyRpg_SetInterpreterFlag: return CmdSetup<&Game_Interpreter::CommandEasyRpgSetInterpreterFlag, 2>(com); case Cmd::EasyRpg_ProcessJson: @@ -4429,6 +4433,136 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co return true; } +bool Game_Interpreter::CommandManiacSetPicturePixel(lcf::rpg::EventCommand const& com) { + if (!Player::IsPatchManiac()) { + return true; + } + + int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[1]); + if (pic_id <= 0) { + Output::Warning("ManiacSetPicturePixel: Invalid picture ID {}", pic_id); + return true; + } + + auto& pic = Main_Data::game_pictures->GetPicture(pic_id); + + if (pic.IsRequestPending()) { + pic.MakeRequestImportant(); + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + + auto* sprite = pic.sprite.get(); + if (!sprite) return true; + + auto bitmap = sprite->GetBitmap(); + if (!bitmap) return true; + + // 1. Calculate Spritesheet Offset + // Maniacs operations are relative to the currently active cell. + int offset_x = 0; + int offset_y = 0; + + const auto& data = pic.data; + if (data.spritesheet_cols > 1 || data.spritesheet_rows > 1) { + int frame_width = bitmap->GetWidth() / data.spritesheet_cols; + int frame_height = bitmap->GetHeight() / data.spritesheet_rows; + + // Map current frame index to X/Y coords + offset_x = (data.spritesheet_frame % data.spritesheet_cols) * frame_width; + offset_y = (data.spritesheet_frame / data.spritesheet_cols) * frame_height; + } + + // 2. COW & Window Detach Logic + BitmapRef writeable_bitmap = bitmap; + + bool is_cached = !bitmap->GetId().empty() && !StartsWith(bitmap->GetId(), "Canvas:"); + bool wrong_format = bitmap->bpp() != 4; + bool is_window = data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; + + if (is_cached || wrong_format || is_window) { + writeable_bitmap = Bitmap::Create(bitmap->GetWidth(), bitmap->GetHeight()); + writeable_bitmap->BlitFast(0, 0, *bitmap, bitmap->GetRect(), 255); + + writeable_bitmap->SetId("Canvas:" + std::to_string(pic_id)); + + sprite->SetBitmap(writeable_bitmap); + + if (is_window) { + pic.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_default; + } + + // Force sprite to recalculate its clipping rectangle immediately. + // Otherwise, SetBitmap resets src_rect to full size, showing the whole sheet. + sprite->OnPictureShow(); + } + + // 3. Parameters + int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]); + int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]); + int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]); + int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]); + + int src_var_start = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]); + + int flags = com.parameters[7]; + bool flag_opaq = (flags & 1) != 0; + bool flag_skip_trans = (flags & 2) != 0; + + // 4. Drawing Loop + int bmp_w = writeable_bitmap->GetWidth(); + int bmp_h = writeable_bitmap->GetHeight(); + + if (w <= 0 || h <= 0) return true; + + uint32_t* pixels = static_cast(writeable_bitmap->pixels()); + int pitch = writeable_bitmap->pitch() / 4; + + int current_var = src_var_start; + auto& vars = *Main_Data::game_variables; + + for (int iy = 0; iy < h; ++iy) { + // Apply Y Offset here + int py = y + iy + offset_y; + + if (py < 0 || py >= bmp_h) { + current_var += w; + continue; + } + + for (int ix = 0; ix < w; ++ix) { + int32_t color_val = vars.Get(current_var++); + + // Apply X Offset here + int px = x + ix + offset_x; + + if (px < 0 || px >= bmp_w) continue; + + uint8_t a = (color_val >> 24) & 0xFF; + uint8_t r = (color_val >> 16) & 0xFF; + uint8_t g = (color_val >> 8) & 0xFF; + uint8_t b = (color_val) & 0xFF; + + if (flag_skip_trans && a == 0) continue; + + if (flag_opaq) { + a = 0xFF; + } + + if (a < 255) { + r = (r * a) / 255; + g = (g * a) / 255; + b = (b * a) / 255; + } + + uint32_t final_pixel = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, a); + pixels[py * pitch + px] = final_pixel; + } + } + + return true; +} + bool Game_Interpreter::CommandManiacGetSaveInfo(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 3440aad8ca..7dbfaba9d3 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -312,6 +312,7 @@ class Game_Interpreter : public Game_BaseInterpreterContext bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com); bool CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand const& com); bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com); + bool CommandManiacSetPicturePixel(lcf::rpg::EventCommand const& com); void SetSubcommandIndex(int indent, int idx); uint8_t& ReserveSubcommandIndex(int indent); From ddd4bb5a4033cb3aa46614dc988a3300fbc36348 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 14:23:25 +0100 Subject: [PATCH 12/19] Maniac Get Game Info: Improve performance with fill and copy --- src/game_interpreter.cpp | 127 ++++++++++++++++++++++++--------------- src/game_interpreter.h | 4 +- src/game_variables.cpp | 8 ++- src/game_variables.h | 4 +- 4 files changed, 92 insertions(+), 51 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index d95a86f46b..d9cba968c0 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -48,6 +48,8 @@ #include "game_windows.h" #include "json_helper.h" #include "maniac_patch.h" +#include "memory_management.h" +#include "pixel_format.h" #include "spriteset_map.h" #include "sprite_character.h" #include "scene_gameover.h" @@ -807,16 +809,16 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter::CommandManiacChangePictureId, 6>(com); case Cmd::Maniac_SetGameOption: return CmdSetup<&Game_Interpreter::CommandManiacSetGameOption, 4>(com); - case Cmd::Maniac_ControlStrings: - return CmdSetup<&Game_Interpreter::CommandManiacControlStrings, 8>(com); - case Cmd::Maniac_WritePicture: - return CmdSetup<&Game_Interpreter::CommandManiacWritePicture, 5>(com); case Cmd::Maniac_CallCommand: return CmdSetup<&Game_Interpreter::CommandManiacCallCommand, 6>(com); + case Cmd::Maniac_ControlStrings: + return CmdSetup<&Game_Interpreter::CommandManiacControlStrings, 8>(com); case Cmd::Maniac_GetGameInfo: return CmdSetup<&Game_Interpreter::CommandManiacGetGameInfo, 8>(com); - case static_cast(3025): - return CmdSetup<&Game_Interpreter::CommandManiacSetPicturePixel, 8>(com); + case Cmd::Maniac_EditPicture: + return CmdSetup<&Game_Interpreter::CommandManiacEditPicture, 8>(com); + case Cmd::Maniac_WritePicture: + return CmdSetup<&Game_Interpreter::CommandManiacWritePicture, 5>(com); case Cmd::EasyRpg_SetInterpreterFlag: return CmdSetup<&Game_Interpreter::CommandEasyRpgSetInterpreterFlag, 2>(com); case Cmd::EasyRpg_ProcessJson: @@ -4211,68 +4213,99 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co break; case 3: // Get pixel info { - // 1. Decode Parameters - // [0] Packing: Bits 0-3: X Mode, 4-7: Y Mode, etc. - int p_x = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]); - int p_y = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); - int p_w = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); - int p_h = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); - int dest_var_id = com.parameters[7]; + // [0] Packing: x pos, y pos, width, height + int pic_x = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]); + int pic_y = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); + int pic_w = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); + int pic_h = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); + int dst_var_id = com.parameters[7]; // Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB) bool ignore_alpha = (com.parameters[2] & 1) != 0; - // 2. Capture Screen // Creates a snapshot of the current frame BitmapRef screen = DisplayUi->CaptureScreen(); - if (!screen) return true; + if (pic_w <= 0 || pic_h <= 0) { + return true; + } - int screen_w = screen->GetWidth(); - int screen_h = screen->GetHeight(); + // Format expected by Maniacs + auto format = format_B8G8R8A8_a().format(); + if (ignore_alpha) { + format = format_B8G8R8A8_n().format(); + } - if (p_w <= 0 || p_h <= 0) return true; + // Allocate an image as large as the requested dimensions (ignoring out of bounds) + Rect screen_rect = screen->GetRect(); + Rect frame_rect = screen_rect.GetSubRect({pic_x, pic_y, pic_w, pic_h}); - uint32_t* pixels = static_cast(screen->pixels()); - int pitch = screen->pitch() / 4; // stride in uint32 elements + if (frame_rect.width <= 0 || frame_rect.height <= 0) { + return true; + } - auto& variables = *Main_Data::game_variables; + BitmapRef frame = Bitmap::Create(nullptr, frame_rect.width, frame_rect.height, frame_rect.width * format.bytes, format); - int var_idx = 0; - uint8_t r, g, b, a; + // Then blit the screen (converts to the correct format) + frame->BlitFast(0, 0, *screen, frame_rect, Opacity::Opaque()); - // 4. Read Loop - for (int iy = 0; iy < p_h; ++iy) { - int sy = p_y + iy; + uint32_t* pixels = static_cast(frame->pixels()); + int px_per_row = frame->pitch() / sizeof(uint32_t); + uint32_t* src_row = pixels; - // If row out of bounds, skip variables for this row - if (sy < 0 || sy >= screen_h) { - var_idx += p_w; - continue; + if (ignore_alpha) { + // Slow: Set all alpha values to 0 + const auto a_mask = format.a.mask; + for (int y = 0; y < frame_rect.height; ++y) { + for (int x = 0; x < frame_rect.width; ++x) { + src_row[x] &= ~a_mask; + } + src_row += px_per_row; } + } - for (int ix = 0; ix < p_w; ++ix) { - int target_var = dest_var_id + var_idx++; - int sx = p_x + ix; - - if (sx >= 0 && sx < screen_w) { - uint32_t raw_pixel = pixels[sy * pitch + sx]; - - Bitmap::opaque_pixel_format.uint32_to_rgba(raw_pixel, r, g, b, a); + // Rowwise memcpy + auto& variables = *Main_Data::game_variables; - if (ignore_alpha) { - a = 0; - } - else { - a = 255; //In maniacs, bellow parallax layer can't be transparent is this the best approach? - } + int x_l = std::min(0, pic_x); + int x_r = std::max(0, pic_x + pic_w - screen_rect.width) + frame_rect.width; + int y_t = std::min(0, pic_y); + int y_b = std::max(0, pic_y + pic_h - screen_rect.height) + frame_rect.height; + + src_row = pixels; + for (int y = y_t; y < y_b; ++y) { + // When row out of bounds write 0 in this row + if (y < 0 || y >= frame_rect.height) { + auto out_range = variables.GetWritableRange(dst_var_id, pic_w); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += pic_w; + continue; + } - // Maniacs format: AARRGGBB (Signed 32-bit) - uint32_t packed = (a << 24) | (r << 16) | (g << 8) | b; - variables.Set(target_var, static_cast(packed)); + for (int x = x_l; x < x_r;) { + if (x < 0) { + // OOB to the left (write 0 for remaining cols) + auto out_range = variables.GetWritableRange(dst_var_id, -x_l); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += -x; + x = 0; + } else if (x >= frame_rect.width) { + // OOB to the right (write 0 for remaining cols) + int len = x_r - frame_rect.width; + auto out_range = variables.GetWritableRange(dst_var_id, len); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += len; + break; + } else { + auto out_range = variables.GetWritableRange(dst_var_id, frame_rect.width); + std::copy(src_row, src_row + frame_rect.width, out_range.data()); + src_row += px_per_row; + dst_var_id += frame_rect.width; + x += frame_rect.width; } } } + break; } case 4: // Get command interpreter state diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 7dbfaba9d3..60510aae7e 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -305,14 +305,14 @@ 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 CommandManiacEditPicture(lcf::rpg::EventCommand const& com); bool CommandManiacWritePicture(lcf::rpg::EventCommand const& com); bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com); + bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com); bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com); bool CommandEasyRpgProcessJson(lcf::rpg::EventCommand const& com); bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com); bool CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand const& com); - bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com); - bool CommandManiacSetPicturePixel(lcf::rpg::EventCommand const& com); void SetSubcommandIndex(int indent, int idx); uint8_t& ReserveSubcommandIndex(int indent); diff --git a/src/game_variables.cpp b/src/game_variables.cpp index 9b37fda8e0..4af2bd3531 100644 --- a/src/game_variables.cpp +++ b/src/game_variables.cpp @@ -23,6 +23,7 @@ #include "utils.h" #include "rand.h" #include +#include namespace { using Var_t = Game_Variables::Var_t; @@ -209,7 +210,7 @@ void Game_Variables::WriteArray(const int first_id_a, const int last_id_a, const } } -std::vector Game_Variables::GetRange(int variable_id, int length) { +std::vector Game_Variables::GetRange(int variable_id, int length) const { std::vector vars; for (int i = 0; i < length; ++i) { vars.push_back(Get(variable_id + i)); @@ -217,6 +218,11 @@ std::vector Game_Variables::GetRange(int variable_id, int length) { return vars; } +lcf::Span Game_Variables::GetWritableRange(int variable_id, int length) { + PrepareRange(variable_id, variable_id + length, "Invalid write var[{},{}]!"); + return lcf::Span(&_variables[variable_id - 1], length); +} + Game_Variables::Var_t Game_Variables::Set(int variable_id, Var_t value) { return SetOp(variable_id, value, VarSet, "Invalid write var[{}] = {}!"); } diff --git a/src/game_variables.h b/src/game_variables.h index 1da0393a9d..d42e0ecd9c 100644 --- a/src/game_variables.h +++ b/src/game_variables.h @@ -23,6 +23,7 @@ #include "compiler.h" #include "string_view.h" #include +#include #include /** @@ -49,7 +50,8 @@ class Game_Variables { Var_t Get(int variable_id) const; Var_t GetIndirect(int variable_id) const; Var_t GetWithMode(int id, int mode) const; - std::vector GetRange(int variable_id, int length); + std::vector GetRange(int variable_id, int length) const; + lcf::Span GetWritableRange(int variable_id, int length); Var_t Set(int variable_id, Var_t value); Var_t Add(int variable_id, Var_t value); From c413fc49cb22f2a9fd9118da0dd990e28042ba4c Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 14:48:43 +0100 Subject: [PATCH 13/19] Move pixel extraction into a seperate function and implement pixel reading of screen and pictures with it --- src/bitmap.cpp | 2 +- src/bitmap.h | 3 +- src/game_interpreter.cpp | 164 ++++++--------------------------------- src/maniac_patch.cpp | 90 +++++++++++++++++++++ src/maniac_patch.h | 14 ++++ 5 files changed, 130 insertions(+), 143 deletions(-) diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 8a203f6a12..c8abcdb5cb 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -87,7 +87,7 @@ Bitmap::Bitmap(int width, int height, bool transparent) { Bitmap::Bitmap(void *pixels, int width, int height, int pitch, const DynamicFormat& _format) { format = _format; pixman_format = find_format(format); - Init(width, height, pixels, pitch, false); + Init(width, height, pixels, pitch, pixels == nullptr); } Bitmap::Bitmap(Filesystem_Stream::InputStream stream, bool transparent, uint32_t flags) { diff --git a/src/bitmap.h b/src/bitmap.h index 2060b274ab..10a4d73f7e 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -92,7 +92,8 @@ class Bitmap { static BitmapRef Create(int width, int height, bool transparent = true, int bpp = 0); /** - * Creates a surface wrapper around existing pixel data. + * Creates a surface wrapper around pixel data. + * When the pixel data is NULL the data is allocated and managed by the bitmap. * * @param pixels pointer to pixel data. * @param width surface width. diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index d9cba968c0..3f1c65baa2 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4225,87 +4225,12 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co // Creates a snapshot of the current frame BitmapRef screen = DisplayUi->CaptureScreen(); + Rect frame_rect{pic_x, pic_y, pic_w, pic_h}; - if (pic_w <= 0 || pic_h <= 0) { + if (!ManiacPatch::WritePixelsToVariable(*screen, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) { return true; } - // Format expected by Maniacs - auto format = format_B8G8R8A8_a().format(); - if (ignore_alpha) { - format = format_B8G8R8A8_n().format(); - } - - // Allocate an image as large as the requested dimensions (ignoring out of bounds) - Rect screen_rect = screen->GetRect(); - Rect frame_rect = screen_rect.GetSubRect({pic_x, pic_y, pic_w, pic_h}); - - if (frame_rect.width <= 0 || frame_rect.height <= 0) { - return true; - } - - BitmapRef frame = Bitmap::Create(nullptr, frame_rect.width, frame_rect.height, frame_rect.width * format.bytes, format); - - // Then blit the screen (converts to the correct format) - frame->BlitFast(0, 0, *screen, frame_rect, Opacity::Opaque()); - - uint32_t* pixels = static_cast(frame->pixels()); - int px_per_row = frame->pitch() / sizeof(uint32_t); - uint32_t* src_row = pixels; - - if (ignore_alpha) { - // Slow: Set all alpha values to 0 - const auto a_mask = format.a.mask; - for (int y = 0; y < frame_rect.height; ++y) { - for (int x = 0; x < frame_rect.width; ++x) { - src_row[x] &= ~a_mask; - } - src_row += px_per_row; - } - } - - // Rowwise memcpy - auto& variables = *Main_Data::game_variables; - - int x_l = std::min(0, pic_x); - int x_r = std::max(0, pic_x + pic_w - screen_rect.width) + frame_rect.width; - int y_t = std::min(0, pic_y); - int y_b = std::max(0, pic_y + pic_h - screen_rect.height) + frame_rect.height; - - src_row = pixels; - for (int y = y_t; y < y_b; ++y) { - // When row out of bounds write 0 in this row - if (y < 0 || y >= frame_rect.height) { - auto out_range = variables.GetWritableRange(dst_var_id, pic_w); - std::fill(out_range.begin(), out_range.end(), 0); - dst_var_id += pic_w; - continue; - } - - for (int x = x_l; x < x_r;) { - if (x < 0) { - // OOB to the left (write 0 for remaining cols) - auto out_range = variables.GetWritableRange(dst_var_id, -x_l); - std::fill(out_range.begin(), out_range.end(), 0); - dst_var_id += -x; - x = 0; - } else if (x >= frame_rect.width) { - // OOB to the right (write 0 for remaining cols) - int len = x_r - frame_rect.width; - auto out_range = variables.GetWritableRange(dst_var_id, len); - std::fill(out_range.begin(), out_range.end(), 0); - dst_var_id += len; - break; - } else { - auto out_range = variables.GetWritableRange(dst_var_id, frame_rect.width); - std::copy(src_row, src_row + frame_rect.width, out_range.data()); - src_row += px_per_row; - dst_var_id += frame_rect.width; - x += frame_rect.width; - } - } - } - break; } case 4: // Get command interpreter state @@ -4912,10 +4837,8 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]); - // 1. Validate Picture existence auto& pic = Main_Data::game_pictures->GetPicture(pic_id); - // 2. Handle Async Loading if (pic.IsRequestPending()) { // Cannot do anything useful here without the dimensions pic.MakeRequestImportant(); @@ -4926,14 +4849,12 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& const auto& data = pic.data; int info_type = com.parameters[1]; - // --- Type 3: Pixel Data Extraction --- + // Type 3: Pixel Data Extraction if (info_type == 3) { - // Verify Sprite existence auto* sprite = pic.sprite.get(); if (!sprite) return true; auto bitmap = sprite->GetBitmap(); - if (!bitmap) return true; // If this is a Window (String Picture), the visual content is generated by the Window class. // We force a refresh/draw cycle here to ensure we read the actual text/window graphics. @@ -4945,81 +4866,39 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& } } - // 3. Decode Parameters - int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); - int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); - int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); - int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[7]); - int dest_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[8]); + // Packing: x pos, y pos, width, height, var_id + int pic_x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]); + int pic_y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]); + int pic_w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]); + int pic_h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[7]); + int dst_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[8]); - int flags = com.parameters[2]; - bool ignore_alpha = (flags & 2) != 0; + // Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB) + bool ignore_alpha = (com.parameters[2] & 2) != 0; - int bmp_w = bitmap->GetWidth(); - int bmp_h = bitmap->GetHeight(); + // Creates a snapshot of the current frame + Rect frame_rect{pic_x, pic_y, pic_w, pic_h}; - // 4. Prepare for Raw Access - if (bitmap->bpp() != 4) { - Output::Debug("GetPictureInfo: Pixel read supported on 32-bit bitmaps only."); + if (!ManiacPatch::WritePixelsToVariable(*bitmap, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) { return true; } - uint32_t* pixels = static_cast(bitmap->pixels()); - int pitch = bitmap->pitch() / 4; - - auto& variables = *Main_Data::game_variables; - - int var_idx = 0; - uint8_t r, g, b, a; - - // 5. Read Loop - for (int iy = 0; iy < h; ++iy) { - int by = y + iy; - - // If row out of bounds, skip variables for this row. - // Do NOT write 0. This preserves the existing values in the variables. - if (by < 0 || by >= bmp_h) { - var_idx += w; - continue; - } - - for (int ix = 0; ix < w; ++ix) { - int target_var = dest_var_id + var_idx++; - int bx = x + ix; - - // Only write if inside bounds. - if (bx >= 0 && bx < bmp_w) { - uint32_t raw_pixel = pixels[by * pitch + bx]; - - Bitmap::pixel_format.uint32_to_rgba(raw_pixel, r, g, b, a); - - if (ignore_alpha) { - a = 0; - } - - // Maniacs format: AARRGGBB - uint32_t packed = (a << 24) | (r << 16) | (g << 8) | b; - variables.Set(target_var, static_cast(packed)); - } - // Else: Out of bounds. Do nothing. - } - } - Game_Map::SetNeedRefresh(true); return true; } - // --- Logic for Info Types 0, 1, 2 --- - + // Logic for Info Types 0, 1, 2 int x = 0; int y = 0; - int width = pic.sprite ? pic.sprite->GetWidth() : 0; - int height = pic.sprite ? pic.sprite->GetHeight() : 0; + int width = 0; + int height = 0; - switch (com.parameters[1]) { + switch (info_type) { case 0: x = Utils::RoundTo(data.current_x); y = Utils::RoundTo(data.current_y); + width = pic.sprite ? pic.sprite->GetWidth() : 0; + height = pic.sprite ? pic.sprite->GetHeight() : 0; break; case 1: x = Utils::RoundTo(data.current_x); @@ -5036,6 +4915,9 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& } switch (com.parameters[2]) { + case 0: + // X/Y is center + break; case 1: // X/Y is top-left corner x -= (width / 2); diff --git a/src/maniac_patch.cpp b/src/maniac_patch.cpp index e672e97429..bbe373e7e6 100644 --- a/src/maniac_patch.cpp +++ b/src/maniac_patch.cpp @@ -17,6 +17,7 @@ #include "maniac_patch.h" +#include "bitmap.h" #include "filesystem_stream.h" #include "input.h" #include "game_actors.h" @@ -28,6 +29,7 @@ #include "game_variables.h" #include "main_data.h" #include "output.h" +#include "pixel_format.h" #include "player.h" #include @@ -711,6 +713,94 @@ bool ManiacPatch::CheckString(std::string_view str_l, std::string_view str_r, in return check(str_l, str_r); } +bool ManiacPatch::WritePixelsToVariable(const Bitmap& src, Rect src_rect, int start_var_id, bool ignore_alpha, Game_Variables& variables) { + int pic_x = src_rect.x; + int pic_y = src_rect.y; + int pic_w = src_rect.width; + int pic_h = src_rect.height; + + if (pic_w <= 0 || pic_h <= 0) { + return false; + } + + // Format expected by Maniacs + auto format = format_B8G8R8A8_a().format(); + if (ignore_alpha) { + format = format_B8G8R8A8_n().format(); + } + + // Allocate an image as large as the requested dimensions (ignoring out of bounds) + Rect bmp_rect = src.GetRect(); + Rect frame_rect = bmp_rect.GetSubRect(src_rect); + + if (frame_rect.width <= 0 || frame_rect.height <= 0) { + return false; + } + + BitmapRef frame = Bitmap::Create(nullptr, frame_rect.width, frame_rect.height, frame_rect.width * format.bytes, format); + + // Then blit the screen (converts to the correct format) + frame->BlitFast(0, 0, src, frame_rect, Opacity::Opaque()); + + uint32_t* pixels = static_cast(frame->pixels()); + int px_per_row = frame->pitch() / sizeof(uint32_t); + uint32_t* src_row = pixels; + + if (ignore_alpha) { + // Slow: Set all alpha values to 0 + const auto a_mask = format.a.mask; + for (int y = 0; y < frame_rect.height; ++y) { + for (int x = 0; x < frame_rect.width; ++x) { + src_row[x] &= ~a_mask; + } + src_row += px_per_row; + } + } + + // Rowwise memcpy + int x_l = std::min(0, pic_x); + int x_r = std::max(0, pic_x + pic_w - bmp_rect.width) + frame_rect.width; + int y_t = std::min(0, pic_y); + int y_b = std::max(0, pic_y + pic_h - bmp_rect.height) + frame_rect.height; + + int dst_var_id = start_var_id; + src_row = pixels; + for (int y = y_t; y < y_b; ++y) { + // When row out of bounds write 0 in this row + if (y < 0 || y >= frame_rect.height) { + auto out_range = variables.GetWritableRange(dst_var_id, pic_w); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += pic_w; + continue; + } + + for (int x = x_l; x < x_r;) { + if (x < 0) { + // OOB to the left (write 0 for remaining cols) + auto out_range = variables.GetWritableRange(dst_var_id, -x_l); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += -x; + x = 0; + } else if (x >= frame_rect.width) { + // OOB to the right (write 0 for remaining cols) + int len = x_r - frame_rect.width; + auto out_range = variables.GetWritableRange(dst_var_id, len); + std::fill(out_range.begin(), out_range.end(), 0); + dst_var_id += len; + break; + } else { + auto out_range = variables.GetWritableRange(dst_var_id, frame_rect.width); + std::copy(src_row, src_row + frame_rect.width, out_range.data()); + src_row += px_per_row; + dst_var_id += frame_rect.width; + x += frame_rect.width; + } + } + } + + return true; +} + std::string_view ManiacPatch::GetLcfName(int data_type, int id, bool is_dynamic) { auto get_name = [&id](std::string_view type, const auto& vec) -> std::string_view { auto* data = lcf::ReaderUtil::GetElement(vec, id); diff --git a/src/maniac_patch.h b/src/maniac_patch.h index cf62d453ca..8977016b43 100644 --- a/src/maniac_patch.h +++ b/src/maniac_patch.h @@ -23,6 +23,8 @@ #include #include #include "filesystem_stream.h" +#include "game_variables.h" +#include "rect.h" #include "span.h" class Game_BaseInterpreterContext; @@ -35,6 +37,18 @@ namespace ManiacPatch { bool CheckString(std::string_view str_l, std::string_view str_r, int op, bool ignore_case); + /** + * Extracts pixel data out of a bitmap writing it into a range of variables in ARGB format + * + * @param src Bitmap + * @param src_rect Bitmap area to extract + * @param start_var_id Begin of variable range + * @param ignore_alpha Sets the alpha bits in all pixels to 0 (slow) + * @param variables Variable list + * @return true when any variables were modified, false otherwise + */ + bool WritePixelsToVariable(const Bitmap& src, Rect src_rect, int start_var_id, bool ignore_alpha, Game_Variables& variables); + std::string_view GetLcfName(int data_type, int id, bool is_dynamic); std::string_view GetLcfDescription(int data_type, int id, bool is_dynamic); From 6ea133a3695189b8c369909875b2266c4c38be26 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 14:51:34 +0100 Subject: [PATCH 14/19] Canvas handling, move ManiacEditPicture so it matches the command order --- src/game_interpreter.cpp | 265 ++++++++++++++++++++------------------- src/game_pictures.cpp | 4 + src/game_pictures.h | 2 + src/sprite_picture.cpp | 2 +- 4 files changed, 140 insertions(+), 133 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 3f1c65baa2..b1927339af 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4391,136 +4391,6 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co return true; } -bool Game_Interpreter::CommandManiacSetPicturePixel(lcf::rpg::EventCommand const& com) { - if (!Player::IsPatchManiac()) { - return true; - } - - int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[1]); - if (pic_id <= 0) { - Output::Warning("ManiacSetPicturePixel: Invalid picture ID {}", pic_id); - return true; - } - - auto& pic = Main_Data::game_pictures->GetPicture(pic_id); - - if (pic.IsRequestPending()) { - pic.MakeRequestImportant(); - _async_op = AsyncOp::MakeYieldRepeat(); - return true; - } - - auto* sprite = pic.sprite.get(); - if (!sprite) return true; - - auto bitmap = sprite->GetBitmap(); - if (!bitmap) return true; - - // 1. Calculate Spritesheet Offset - // Maniacs operations are relative to the currently active cell. - int offset_x = 0; - int offset_y = 0; - - const auto& data = pic.data; - if (data.spritesheet_cols > 1 || data.spritesheet_rows > 1) { - int frame_width = bitmap->GetWidth() / data.spritesheet_cols; - int frame_height = bitmap->GetHeight() / data.spritesheet_rows; - - // Map current frame index to X/Y coords - offset_x = (data.spritesheet_frame % data.spritesheet_cols) * frame_width; - offset_y = (data.spritesheet_frame / data.spritesheet_cols) * frame_height; - } - - // 2. COW & Window Detach Logic - BitmapRef writeable_bitmap = bitmap; - - bool is_cached = !bitmap->GetId().empty() && !StartsWith(bitmap->GetId(), "Canvas:"); - bool wrong_format = bitmap->bpp() != 4; - bool is_window = data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; - - if (is_cached || wrong_format || is_window) { - writeable_bitmap = Bitmap::Create(bitmap->GetWidth(), bitmap->GetHeight()); - writeable_bitmap->BlitFast(0, 0, *bitmap, bitmap->GetRect(), 255); - - writeable_bitmap->SetId("Canvas:" + std::to_string(pic_id)); - - sprite->SetBitmap(writeable_bitmap); - - if (is_window) { - pic.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_default; - } - - // Force sprite to recalculate its clipping rectangle immediately. - // Otherwise, SetBitmap resets src_rect to full size, showing the whole sheet. - sprite->OnPictureShow(); - } - - // 3. Parameters - int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]); - int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]); - int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]); - int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]); - - int src_var_start = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]); - - int flags = com.parameters[7]; - bool flag_opaq = (flags & 1) != 0; - bool flag_skip_trans = (flags & 2) != 0; - - // 4. Drawing Loop - int bmp_w = writeable_bitmap->GetWidth(); - int bmp_h = writeable_bitmap->GetHeight(); - - if (w <= 0 || h <= 0) return true; - - uint32_t* pixels = static_cast(writeable_bitmap->pixels()); - int pitch = writeable_bitmap->pitch() / 4; - - int current_var = src_var_start; - auto& vars = *Main_Data::game_variables; - - for (int iy = 0; iy < h; ++iy) { - // Apply Y Offset here - int py = y + iy + offset_y; - - if (py < 0 || py >= bmp_h) { - current_var += w; - continue; - } - - for (int ix = 0; ix < w; ++ix) { - int32_t color_val = vars.Get(current_var++); - - // Apply X Offset here - int px = x + ix + offset_x; - - if (px < 0 || px >= bmp_w) continue; - - uint8_t a = (color_val >> 24) & 0xFF; - uint8_t r = (color_val >> 16) & 0xFF; - uint8_t g = (color_val >> 8) & 0xFF; - uint8_t b = (color_val) & 0xFF; - - if (flag_skip_trans && a == 0) continue; - - if (flag_opaq) { - a = 0xFF; - } - - if (a < 255) { - r = (r * a) / 255; - g = (g * a) / 255; - b = (b * a) / 255; - } - - uint32_t final_pixel = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, a); - pixels[py * pitch + px] = final_pixel; - } - } - - return true; -} - bool Game_Interpreter::CommandManiacGetSaveInfo(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; @@ -5491,6 +5361,136 @@ bool Game_Interpreter::CommandManiacControlStrings(lcf::rpg::EventCommand const& return true; } +bool Game_Interpreter::CommandManiacEditPicture(lcf::rpg::EventCommand const& com) { + if (!Player::IsPatchManiac()) { + return true; + } + + int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[1]); + if (pic_id <= 0) { + Output::Warning("ManiacSetPicturePixel: Invalid picture ID {}", pic_id); + return true; + } + + auto& pic = Main_Data::game_pictures->GetPicture(pic_id); + + if (pic.IsRequestPending()) { + pic.MakeRequestImportant(); + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + + auto* sprite = pic.sprite.get(); + if (!sprite) return true; + + auto bitmap = sprite->GetBitmap(); + if (!bitmap) return true; + + // 1. Calculate Spritesheet Offset + // Maniacs operations are relative to the currently active cell. + int offset_x = 0; + int offset_y = 0; + + const auto& data = pic.data; + if (data.spritesheet_cols > 1 || data.spritesheet_rows > 1) { + int frame_width = bitmap->GetWidth() / data.spritesheet_cols; + int frame_height = bitmap->GetHeight() / data.spritesheet_rows; + + // Map current frame index to X/Y coords + offset_x = (data.spritesheet_frame % data.spritesheet_cols) * frame_width; + offset_y = (data.spritesheet_frame / data.spritesheet_cols) * frame_height; + } + + // 2. COW & Window Detach Logic + BitmapRef writeable_bitmap = bitmap; + + bool is_cached = !bitmap->GetId().empty() && !StartsWith(bitmap->GetId(), "Canvas:"); + bool wrong_format = bitmap->bpp() != 4; + bool is_window = data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; + + if (is_cached || wrong_format || is_window) { + writeable_bitmap = Bitmap::Create(bitmap->GetWidth(), bitmap->GetHeight()); + writeable_bitmap->BlitFast(0, 0, *bitmap, bitmap->GetRect(), 255); + + writeable_bitmap->SetId("Canvas:" + std::to_string(pic_id)); + + sprite->SetBitmap(writeable_bitmap); + + if (is_window) { + pic.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_default; + } + + // Force sprite to recalculate its clipping rectangle immediately. + // Otherwise, SetBitmap resets src_rect to full size, showing the whole sheet. + sprite->OnPictureShow(); + } + + // 3. Parameters + int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]); + int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]); + int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]); + int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]); + + int src_var_start = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]); + + int flags = com.parameters[7]; + bool flag_opaq = (flags & 1) != 0; + bool flag_skip_trans = (flags & 2) != 0; + + // 4. Drawing Loop + int bmp_w = writeable_bitmap->GetWidth(); + int bmp_h = writeable_bitmap->GetHeight(); + + if (w <= 0 || h <= 0) return true; + + uint32_t* pixels = static_cast(writeable_bitmap->pixels()); + int pitch = writeable_bitmap->pitch() / 4; + + int current_var = src_var_start; + auto& vars = *Main_Data::game_variables; + + for (int iy = 0; iy < h; ++iy) { + // Apply Y Offset here + int py = y + iy + offset_y; + + if (py < 0 || py >= bmp_h) { + current_var += w; + continue; + } + + for (int ix = 0; ix < w; ++ix) { + int32_t color_val = vars.Get(current_var++); + + // Apply X Offset here + int px = x + ix + offset_x; + + if (px < 0 || px >= bmp_w) continue; + + uint8_t a = (color_val >> 24) & 0xFF; + uint8_t r = (color_val >> 16) & 0xFF; + uint8_t g = (color_val >> 8) & 0xFF; + uint8_t b = (color_val) & 0xFF; + + if (flag_skip_trans && a == 0) continue; + + if (flag_opaq) { + a = 0xFF; + } + + if (a < 255) { + r = (r * a) / 255; + g = (g * a) / 255; + b = (b * a) / 255; + } + + uint32_t final_pixel = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, a); + pixels[py * pitch + px] = final_pixel; + } + } + + return true; +} + bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; @@ -5553,8 +5553,9 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c const auto sprite = picture.sprite.get(); // Retrieve bitmap - if (picture.IsWindowAttached()) { - // Maniac ignores the opaque setting for String Picture + if (picture.IsWindowAttached() || picture.IsCanvas()) { + // Cannot change transparency of images that are not reloadable from a file + // Appears to match Maniacs behaviour bitmap = picture.sprite->GetBitmap(); } else if (picture.data.name.empty()) { // Not much we can do here (also shouldn't happen normally) diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index 6f7187c2f7..81c0a9d8a0 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -507,6 +507,10 @@ bool Game_Pictures::Picture::IsWindowAttached() const { return data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; } +bool Game_Pictures::Picture::IsCanvas() const { + return data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_canvas; +} + void Game_Pictures::Picture::Update(bool is_battle) { if ((is_battle && !IsOnBattle()) || (!is_battle && !IsOnMap())) { return; diff --git a/src/game_pictures.h b/src/game_pictures.h index 30d0a0251d..334fd52b1b 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -128,6 +128,8 @@ class Game_Pictures { void AttachWindow(const Window_Base& window); bool IsWindowAttached() const; + + bool IsCanvas() const; }; Picture& GetPicture(int id); diff --git a/src/sprite_picture.cpp b/src/sprite_picture.cpp index 3fadea8568..956382eb76 100644 --- a/src/sprite_picture.cpp +++ b/src/sprite_picture.cpp @@ -71,7 +71,7 @@ void Sprite_Picture::Draw(Bitmap& dst) { return; } - if (data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window) { + if (pic.IsWindowAttached()) { // Paint the Window on the Picture const auto& window = Main_Data::game_windows->GetWindow(pic_id); window.window->Draw(*bitmap.get()); From 6234d34c92dd9489fc5957e9e0cf64f605f47d8f Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 17:01:16 +0100 Subject: [PATCH 15/19] EditPicture: Rework the code --- src/game_interpreter.cpp | 150 ++++++++++++--------------------------- src/game_pictures.cpp | 4 ++ src/game_pictures.h | 3 +- src/game_variables.cpp | 9 +-- src/game_variables.h | 2 +- src/maniac_patch.cpp | 83 +++++++++++++++++++++- src/maniac_patch.h | 15 +++- 7 files changed, 153 insertions(+), 113 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index b1927339af..237a735bc2 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -47,6 +47,7 @@ #include "game_interpreter_control_variables.h" #include "game_windows.h" #include "json_helper.h" +#include "lcf/rpg/savepicture.h" #include "maniac_patch.h" #include "memory_management.h" #include "pixel_format.h" @@ -4722,13 +4723,11 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& // Type 3: Pixel Data Extraction if (info_type == 3) { auto* sprite = pic.sprite.get(); - if (!sprite) return true; - auto bitmap = sprite->GetBitmap(); // If this is a Window (String Picture), the visual content is generated by the Window class. // We force a refresh/draw cycle here to ensure we read the actual text/window graphics. - if (data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window) { + if (pic.IsWindowAttached()) { const auto& window = Main_Data::game_windows->GetWindow(pic_id); if (window.window) { bitmap->Clear(); @@ -4746,7 +4745,6 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const& // Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB) bool ignore_alpha = (com.parameters[2] & 2) != 0; - // Creates a snapshot of the current frame Rect frame_rect{pic_x, pic_y, pic_w, pic_h}; if (!ManiacPatch::WritePixelsToVariable(*bitmap, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) { @@ -5372,121 +5370,59 @@ bool Game_Interpreter::CommandManiacEditPicture(lcf::rpg::EventCommand const& co return true; } - auto& pic = Main_Data::game_pictures->GetPicture(pic_id); + auto& picture = Main_Data::game_pictures->GetPicture(pic_id); - if (pic.IsRequestPending()) { - pic.MakeRequestImportant(); + if (picture.IsRequestPending()) { + picture.MakeRequestImportant(); _async_op = AsyncOp::MakeYieldRepeat(); return true; } - auto* sprite = pic.sprite.get(); - if (!sprite) return true; - + auto* sprite = picture.sprite.get(); auto bitmap = sprite->GetBitmap(); - if (!bitmap) return true; - // 1. Calculate Spritesheet Offset + // Calculate Spritesheet Offset // Maniacs operations are relative to the currently active cell. - int offset_x = 0; - int offset_y = 0; - - const auto& data = pic.data; - if (data.spritesheet_cols > 1 || data.spritesheet_rows > 1) { - int frame_width = bitmap->GetWidth() / data.spritesheet_cols; - int frame_height = bitmap->GetHeight() / data.spritesheet_rows; - - // Map current frame index to X/Y coords - offset_x = (data.spritesheet_frame % data.spritesheet_cols) * frame_width; - offset_y = (data.spritesheet_frame / data.spritesheet_cols) * frame_height; - } - - // 2. COW & Window Detach Logic - BitmapRef writeable_bitmap = bitmap; - bool is_cached = !bitmap->GetId().empty() && !StartsWith(bitmap->GetId(), "Canvas:"); - bool wrong_format = bitmap->bpp() != 4; - bool is_window = data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; + // Determine Spritesheet frame + Rect src_rect = sprite->GetSrcRect(); - if (is_cached || wrong_format || is_window) { - writeable_bitmap = Bitmap::Create(bitmap->GetWidth(), bitmap->GetHeight()); - writeable_bitmap->BlitFast(0, 0, *bitmap, bitmap->GetRect(), 255); + BitmapRef writable_bitmap = bitmap; - writeable_bitmap->SetId("Canvas:" + std::to_string(pic_id)); - - sprite->SetBitmap(writeable_bitmap); - - if (is_window) { - pic.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_default; + if (picture.IsWindowAttached()) { + // If this is a Window (String Picture), the visual content is generated by the Window class. + // We force a refresh/draw cycle here to ensure we read the actual text/window graphics. + const auto& window = Main_Data::game_windows->GetWindow(pic_id); + if (window.window) { + bitmap->Clear(); + window.window->Draw(*bitmap); } - - // Force sprite to recalculate its clipping rectangle immediately. - // Otherwise, SetBitmap resets src_rect to full size, showing the whole sheet. - sprite->OnPictureShow(); + } else if (picture.IsCanvas()) { + // no-op + } else { + // Must be copied to avoid modifiying the original picture + writable_bitmap = Bitmap::Create(*bitmap, src_rect, true); } - // 3. Parameters - int x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]); - int y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]); - int w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]); - int h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]); + picture.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_canvas; - int src_var_start = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]); + // Packing: x pos, y pos, width, height, var_id + int pic_x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]); + int pic_y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]); + int pic_w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]); + int pic_h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]); + int start_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]); int flags = com.parameters[7]; - bool flag_opaq = (flags & 1) != 0; - bool flag_skip_trans = (flags & 2) != 0; - - // 4. Drawing Loop - int bmp_w = writeable_bitmap->GetWidth(); - int bmp_h = writeable_bitmap->GetHeight(); - - if (w <= 0 || h <= 0) return true; + // When no flag is set the area is cleared and a OP_OVER blit occurs + bool flag_opaq = (flags & 1) != 0; // Blit with OP_SRC + bool flag_skip_trans = (flags & 2) != 0; // Blit with OP_OVER - uint32_t* pixels = static_cast(writeable_bitmap->pixels()); - int pitch = writeable_bitmap->pitch() / 4; + bool clear_dst = !flag_opaq && !flag_skip_trans; + bool ignore_alpha = flag_opaq; - int current_var = src_var_start; - auto& vars = *Main_Data::game_variables; - - for (int iy = 0; iy < h; ++iy) { - // Apply Y Offset here - int py = y + iy + offset_y; - - if (py < 0 || py >= bmp_h) { - current_var += w; - continue; - } - - for (int ix = 0; ix < w; ++ix) { - int32_t color_val = vars.Get(current_var++); - - // Apply X Offset here - int px = x + ix + offset_x; - - if (px < 0 || px >= bmp_w) continue; - - uint8_t a = (color_val >> 24) & 0xFF; - uint8_t r = (color_val >> 16) & 0xFF; - uint8_t g = (color_val >> 8) & 0xFF; - uint8_t b = (color_val) & 0xFF; - - if (flag_skip_trans && a == 0) continue; - - if (flag_opaq) { - a = 0xFF; - } - - if (a < 255) { - r = (r * a) / 255; - g = (g * a) / 255; - b = (b * a) / 255; - } - - uint32_t final_pixel = Bitmap::pixel_format.rgba_to_uint32_t(r, g, b, a); - pixels[py * pitch + px] = final_pixel; - } - } + Rect frame_rect{pic_x, pic_y, pic_w, pic_h}; + ManiacPatch::ReadPixelsFromVariable(*writable_bitmap, frame_rect, start_var_id, clear_dst, ignore_alpha, *Main_Data::game_variables); return true; } @@ -5553,9 +5489,17 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c const auto sprite = picture.sprite.get(); // Retrieve bitmap - if (picture.IsWindowAttached() || picture.IsCanvas()) { - // Cannot change transparency of images that are not reloadable from a file - // Appears to match Maniacs behaviour + // Cannot change transparency of images that are not reloadable from a file (window and canvas) + // Appears to match Maniacs behaviour + if (picture.IsWindowAttached()) { + // If this is a Window (String Picture), the visual content is generated by the Window class. + // We force a refresh/draw cycle here to ensure we read the actual text/window graphics. + const auto& window = Main_Data::game_windows->GetWindow(pic_id); + if (window.window) { + bitmap->Clear(); + window.window->Draw(*bitmap); + } + } else if (picture.IsCanvas()) { bitmap = picture.sprite->GetBitmap(); } else if (picture.data.name.empty()) { // Not much we can do here (also shouldn't happen normally) diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index 81c0a9d8a0..e17a34b0f5 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -503,6 +503,10 @@ void Game_Pictures::Picture::AttachWindow(const Window_Base& window) { ApplyOrigin(false); } +bool Game_Pictures::Picture::IsNormalPicture() const { + return data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_default; +} + bool Game_Pictures::Picture::IsWindowAttached() const { return data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_window; } diff --git a/src/game_pictures.h b/src/game_pictures.h index 334fd52b1b..db1963e59e 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -127,8 +127,9 @@ class Game_Pictures { void OnMapScrolled(int dx, int dy); void AttachWindow(const Window_Base& window); - bool IsWindowAttached() const; + bool IsNormalPicture() const; + bool IsWindowAttached() const; bool IsCanvas() const; }; diff --git a/src/game_variables.cpp b/src/game_variables.cpp index 4af2bd3531..ee5a262491 100644 --- a/src/game_variables.cpp +++ b/src/game_variables.cpp @@ -210,12 +210,9 @@ void Game_Variables::WriteArray(const int first_id_a, const int last_id_a, const } } -std::vector Game_Variables::GetRange(int variable_id, int length) const { - std::vector vars; - for (int i = 0; i < length; ++i) { - vars.push_back(Get(variable_id + i)); - } - return vars; +lcf::Span Game_Variables::GetRange(int variable_id, int length) { + PrepareRange(variable_id, variable_id + length, "Invalid write var[{},{}]!"); + return lcf::Span(&_variables[variable_id - 1], length); } lcf::Span Game_Variables::GetWritableRange(int variable_id, int length) { diff --git a/src/game_variables.h b/src/game_variables.h index d42e0ecd9c..d28e4b3cf3 100644 --- a/src/game_variables.h +++ b/src/game_variables.h @@ -50,7 +50,7 @@ class Game_Variables { Var_t Get(int variable_id) const; Var_t GetIndirect(int variable_id) const; Var_t GetWithMode(int id, int mode) const; - std::vector GetRange(int variable_id, int length) const; + lcf::Span GetRange(int variable_id, int length); lcf::Span GetWritableRange(int variable_id, int length); Var_t Set(int variable_id, Var_t value); diff --git a/src/maniac_patch.cpp b/src/maniac_patch.cpp index bbe373e7e6..d87307eb5a 100644 --- a/src/maniac_patch.cpp +++ b/src/maniac_patch.cpp @@ -713,7 +713,87 @@ bool ManiacPatch::CheckString(std::string_view str_l, std::string_view str_r, in return check(str_l, str_r); } +bool ManiacPatch::ReadPixelsFromVariable(Bitmap& dst, Rect dst_rect, int start_var_id, bool clear_dst, bool ignore_alpha, Game_Variables& variables) { + int pic_x = dst_rect.x; + int pic_y = dst_rect.y; + int pic_w = dst_rect.width; + int pic_h = dst_rect.height; + + if (pic_w <= 0 || pic_h <= 0) { + return false; + } + + // Format expected by Maniacs + auto format = format_B8G8R8A8_a().format(); + if (ignore_alpha) { + format = format_B8G8R8A8_n().format(); + } + + // Allocate an image as large as the requested dimensions (ignoring out of bounds) + Rect bmp_rect = dst.GetRect(); + Rect frame_rect = bmp_rect.GetSubRect(dst_rect); + + if (frame_rect.width <= 0 || frame_rect.height <= 0) { + return false; + } + + BitmapRef frame = Bitmap::Create(nullptr, frame_rect.width, frame_rect.height, frame_rect.width * format.bytes, format); + + uint32_t* pixels = static_cast(frame->pixels()); + int px_per_row = frame->pitch() / sizeof(uint32_t); + uint32_t* dst_row = pixels; + + // Rowwise memcpy + int x_l = std::min(0, pic_x); + int x_r = std::max(0, pic_x + pic_w - bmp_rect.width) + frame_rect.width; + int y_t = std::min(0, pic_y); + int y_b = std::max(0, pic_y + pic_h - bmp_rect.height) + frame_rect.height; + + int src_var_id = start_var_id; + dst_row = pixels; + for (int y = y_t; y < y_b; ++y) { + // When row out of bounds skip + if (y < 0 || y >= frame_rect.height) { + src_var_id += pic_w; + continue; + } + + for (int x = x_l; x < x_r;) { + if (x < 0) { + // OOB to the left (skip) + src_var_id += -x; + x = 0; + } else if (x >= frame_rect.width) { + // OOB to the right (skip) + int len = x_r - frame_rect.width; + src_var_id += len; + break; + } else { + auto in_range = variables.GetRange(src_var_id, frame_rect.width); + std::copy(in_range.begin(), in_range.end(), dst_row); + dst_row += px_per_row; + src_var_id += frame_rect.width; + x += frame_rect.width; + } + } + } + + if (clear_dst) { + dst.ClearRect({pic_x, pic_y, frame_rect.width, frame_rect.height}); + } + + dst.Blit(pic_x, pic_y, *frame, frame->GetRect(), Opacity::Opaque(), + ignore_alpha ? Bitmap::BlendMode::NormalWithoutAlpha : Bitmap::BlendMode::Normal); + + return true; +} + bool ManiacPatch::WritePixelsToVariable(const Bitmap& src, Rect src_rect, int start_var_id, bool ignore_alpha, Game_Variables& variables) { + // FIXME: Because we use premultiplied alpha the colors of transparent pixels are lost (always 0) + // Maniacs appears to preserve them + // E.g. when reading a transparent pixel from Chara1 (which was green) then Maniac will read green and we read 0 + // This is noticable e.g. when using the EditPicture command with the opaque flag when reading from a transparent image + int pic_x = src_rect.x; int pic_y = src_rect.y; int pic_w = src_rect.width; @@ -740,7 +820,8 @@ bool ManiacPatch::WritePixelsToVariable(const Bitmap& src, Rect src_rect, int st BitmapRef frame = Bitmap::Create(nullptr, frame_rect.width, frame_rect.height, frame_rect.width * format.bytes, format); // Then blit the screen (converts to the correct format) - frame->BlitFast(0, 0, src, frame_rect, Opacity::Opaque()); + frame->Blit(0, 0, src, frame_rect, Opacity::Opaque(), + ignore_alpha ? Bitmap::BlendMode::NormalWithoutAlpha : Bitmap::BlendMode::Default); uint32_t* pixels = static_cast(frame->pixels()); int px_per_row = frame->pitch() / sizeof(uint32_t); diff --git a/src/maniac_patch.h b/src/maniac_patch.h index 8977016b43..02c05992e4 100644 --- a/src/maniac_patch.h +++ b/src/maniac_patch.h @@ -37,13 +37,26 @@ namespace ManiacPatch { bool CheckString(std::string_view str_l, std::string_view str_r, int op, bool ignore_case); + /** + * Reads pixel data in ARGB format out of variables and writes them into a bitmap + * + * @param dst Bitmap to write + * @param dst_rect Bitmap area to write to + * @param start_var_id Begin of variable range + * @param clear_dst Clears the target area before blitting + * @param ignore_alpha Blit as if no alpha channel exists (faster) + * @param variables Variable list + * @return true when any pixels were modified, false otherwise + */ + bool ReadPixelsFromVariable(Bitmap& dst, Rect dst_rect, int start_var_id, bool clear_dst, bool ignore_alpha, Game_Variables& variables); + /** * Extracts pixel data out of a bitmap writing it into a range of variables in ARGB format * * @param src Bitmap * @param src_rect Bitmap area to extract * @param start_var_id Begin of variable range - * @param ignore_alpha Sets the alpha bits in all pixels to 0 (slow) + * @param ignore_alpha Sets the alpha channel in all pixel to 0 (slow) * @param variables Variable list * @return true when any variables were modified, false otherwise */ From a98c609ff69959d02465578285b665d4972ad980 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 17:01:23 +0100 Subject: [PATCH 16/19] Change default pixel format on little endian to ARGB. Not touching big endian as this would require further testing. Has the same base performance as ABGR so is okay to use. Makes Masked Blit much faster (see #3497), Pixman fast path probably? Also is the format used by Maniacs so the pixel operations are faster --- src/platform/sdl/sdl2_ui.cpp | 4 ++-- src/platform/sdl/sdl3_ui.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index 1ada3e6967..edda201fb7 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -63,7 +63,7 @@ static uint32_t GetDefaultFormat() { #ifdef WORDS_BIGENDIAN return SDL_PIXELFORMAT_ABGR32; #else - return SDL_PIXELFORMAT_RGBA32; + return SDL_PIXELFORMAT_BGRA32; #endif } @@ -88,7 +88,7 @@ static int GetFormatRank(uint32_t fmt) { case SDL_PIXELFORMAT_RGBA32: return 2; case SDL_PIXELFORMAT_BGRA32: - return 2; + return 3; case SDL_PIXELFORMAT_ARGB32: return 1; case SDL_PIXELFORMAT_ABGR32: diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index c31a7bb684..5d113ba0fa 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -65,7 +65,7 @@ static SDL_PixelFormat GetDefaultFormat() { #ifdef WORDS_BIGENDIAN return SDL_PIXELFORMAT_ABGR32; #else - return SDL_PIXELFORMAT_RGBA32; + return SDL_PIXELFORMAT_BGRA32; #endif } From a730d8bc91d5a2006826e19697eaa77e19b53fb6 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 21 Feb 2026 19:02:18 +0100 Subject: [PATCH 17/19] Implement saving of Canvas bitmaps Most compatible with what Maniacs is doing At least we can load Maniacs images but Maniacs shows nothing --- src/game_interpreter.cpp | 1 + src/game_pictures.cpp | 122 ++++++++++++++++++++++++++++++++++++++- src/game_pictures.h | 3 +- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 237a735bc2..f8f3be00ca 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5404,6 +5404,7 @@ bool Game_Interpreter::CommandManiacEditPicture(lcf::rpg::EventCommand const& co writable_bitmap = Bitmap::Create(*bitmap, src_rect, true); } + picture.data.name = {}; picture.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_canvas; // Packing: x pos, y pos, width, height, var_id diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index e17a34b0f5..eaad9c5cdb 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -16,7 +16,9 @@ */ #include +#include #include "bitmap.h" +#include "lcf/rpg/savepicture.h" #include "options.h" #include "cache.h" #include "output.h" @@ -102,7 +104,7 @@ std::vector Game_Pictures::GetSaveData() const { save.reserve(data_size); for (auto& pic: pictures) { - save.push_back(pic.data); + save.push_back(pic.OnSave()); } // RPG_RT Save game data always has a constant number of pictures @@ -367,6 +369,10 @@ void Game_Pictures::Picture::MakeRequestImportant() const { void Game_Pictures::RequestPictureSprite(Picture& pic) { const auto& name = pic.data.name; if (name.empty()) { + if (Player::IsPatchManiac()) { + pic.LoadCanvas(); + } + return; } @@ -503,6 +509,120 @@ void Game_Pictures::Picture::AttachWindow(const Window_Base& window) { ApplyOrigin(false); } +void Game_Pictures::Picture::LoadCanvas() { + if (data.maniac_image_data.empty()) { + return; + } + + // Size is stored in the (unused in later 2k3) current_bot_trans, wtf? + std::array dim; + std::memcpy(dim.data(), &data.current_bot_trans, 8); + + // Image data is a compressed buffer (deflate) + auto& compressed = data.maniac_image_data; + z_stream strm{}; + strm.next_in = const_cast(compressed.data()); + strm.avail_in = static_cast(compressed.size()); + + if (inflateInit(&strm) != Z_OK) { + Output::Warning("LoadCanvas (Pic {}}: inflateInit failed", data.ID); + return; + } + + std::vector output; + const size_t CHUNK_SIZE = 16384; + unsigned char temp[CHUNK_SIZE]; + + int ret; + do { + strm.next_out = temp; + strm.avail_out = CHUNK_SIZE; + + ret = inflate(&strm, Z_NO_FLUSH); + + if (ret != Z_OK && ret != Z_STREAM_END) { + inflateEnd(&strm); + Output::Warning("LoadCanvas (Pic {}}: inflate failed (err={})", data.ID, ret); + return; + } + + size_t bytes_produced = CHUNK_SIZE - strm.avail_out; + output.insert(output.end(), temp, temp + bytes_produced); + } while (ret != Z_STREAM_END); + + inflateEnd(&strm); + + if (output.size() != dim[0] * dim[1] * 4) { + Output::Warning("LoadCanvas (Pic {}): Wrong buffer size", data.ID); + return; + } + + // Convert from Maniac Patch format to our screen format + auto format = format_B8G8R8A8_a().format(); + auto bmp = Bitmap::Create(output.data(), dim[0], dim[1], dim[0] * 4, format); + CreateSprite(); + sprite->SetBitmap(Bitmap::Create(*bmp, bmp->GetRect())); + data.maniac_image_data = {}; // Save memory + data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_canvas; +} + +lcf::rpg::SavePicture Game_Pictures::Picture::OnSave() const { + auto save_data = data; + + if (IsCanvas()) { + // Write compressed image data into the savefile + auto& bitmap = *sprite->GetBitmap(); + + // Convert from our screen format to Maniac Patch format + auto format = format_B8G8R8A8_a().format(); + auto bmp_out = Bitmap::Create(nullptr, bitmap.width(), bitmap.height(), bitmap.width() * 4, format); + bmp_out->Blit(0, 0, bitmap, bitmap.GetRect(), Opacity::Opaque()); + + // Compress + z_stream strm{}; + strm.next_in = reinterpret_cast(bmp_out->pixels()); + strm.avail_in = static_cast(bitmap.pitch() * bitmap.height()); + + if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) { + Output::Warning("LoadCanvas (Pic {}}: deflateInit failed", data.ID); + return {}; + } + + std::vector output; + const size_t CHUNK_SIZE = 16384; + unsigned char temp[CHUNK_SIZE]; + + int ret; + do { + strm.next_out = temp; + strm.avail_out = CHUNK_SIZE; + + ret = deflate(&strm, Z_FINISH); + + if (ret != Z_OK && ret != Z_STREAM_END) { + deflateEnd(&strm); + Output::Warning("LoadCanvas (Pic {}}: deflate failed", data.ID); + return {}; + } + + size_t bytes_produced = CHUNK_SIZE - strm.avail_out; + output.insert(output.end(), temp, temp + bytes_produced); + } while (ret != Z_STREAM_END); + + deflateEnd(&strm); + + // Save the data + save_data.maniac_image_data = output; + + std::array dim; + dim[0] = bitmap.width(); + dim[1] = bitmap.height(); + std::memcpy(&save_data.current_bot_trans, dim.data(), 8); + } + + return save_data; +} + bool Game_Pictures::Picture::IsNormalPicture() const { return data.easyrpg_type == lcf::rpg::SavePicture::EasyRpgType_default; } diff --git a/src/game_pictures.h b/src/game_pictures.h index db1963e59e..f9d884d406 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -20,7 +20,6 @@ // Headers #include -#include #include "async_handler.h" #include #include "sprite_picture.h" @@ -127,6 +126,8 @@ class Game_Pictures { void OnMapScrolled(int dx, int dy); void AttachWindow(const Window_Base& window); + void LoadCanvas(); + lcf::rpg::SavePicture OnSave() const; bool IsNormalPicture() const; bool IsWindowAttached() const; From 9e7fe478d0951eaa209653674f7a8c02e3c8d70e Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 23 Feb 2026 11:57:54 +0100 Subject: [PATCH 18/19] Add a helper function to create the path when writing to a file in the save directory --- src/filefinder.cpp | 19 +++++++++++++++++++ src/filefinder.h | 11 +++++++++++ src/game_interpreter.cpp | 8 +++----- src/game_strings.cpp | 19 +------------------ 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 97e51e5aaf..756bec5f61 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -519,6 +519,25 @@ Filesystem_Stream::InputStream FileFinder::OpenText(std::string_view name) { return open_generic_with_fallback("Text", name, args); } +Filesystem_Stream::OutputStream FileFinder::OpenWrite(std::string_view name) { + std::string orig_name = FileFinder::MakeCanonical(name, 1); + + std::string filename = FileFinder::Save().FindFile(name); + + if (filename.empty()) { + // File not found: Create directory hierarchy to ensure file creation succeeds + auto dir = FileFinder::GetPathAndFilename(orig_name).first; + + if (!dir.empty() && !FileFinder::Save().MakeDirectory(dir, false)) { + return Filesystem_Stream::OutputStream(); + } + + filename = orig_name; + } + + return FileFinder::Save().OpenOutputStream(filename); +} + bool FileFinder::IsMajorUpdatedTree() { auto fs = Game(); assert(fs); diff --git a/src/filefinder.h b/src/filefinder.h index effa98df64..906ec57af2 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -229,6 +230,16 @@ namespace FileFinder { */ Filesystem_Stream::InputStream OpenText(std::string_view name); + /** + * Opens a given file for writing in the save directory. + * Sanitizes the path and creates the directory hierarchy to the file when + * necessary. + * + * @param name + * @return Filesystem_Stream::OutputStream + */ + Filesystem_Stream::OutputStream OpenWrite(std::string_view name); + /** * Appends name to directory. * diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index f8f3be00ca..8d1ae3dabb 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5540,11 +5540,9 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c filename += ".png"; } - auto found_file = FileFinder::Save().FindFile(filename); - - auto os = FileFinder::Save().OpenOutputStream(found_file.empty() ? filename : found_file); - if (os) { - bitmap->WritePNG(os); + auto img_out = FileFinder::OpenWrite(filename); + if (img_out) { + bitmap->WritePNG(img_out); } else { Output::Warning("ManiacSaveImage: Failed to open file for writing: {}", filename); } diff --git a/src/game_strings.cpp b/src/game_strings.cpp index 63eb5d66c3..79c027a354 100644 --- a/src/game_strings.cpp +++ b/src/game_strings.cpp @@ -239,24 +239,7 @@ bool Game_Strings::ToFile(Str_Params params, std::string filename, int encoding) filename += ".txt"; } - filename = FileFinder::MakeCanonical(filename, 1); - - auto txt_file = FileFinder::Save().FindFile(filename); - Filesystem_Stream::OutputStream txt_out; - - if (txt_file.empty()) { - // File not found: Create directory hierarchy to ensure file creation succeeds - auto txt_dir = FileFinder::GetPathAndFilename(filename).first; - - if (!txt_dir.empty() && !FileFinder::Save().MakeDirectory(txt_dir, false)) { - Output::Warning("Maniac String Op ToFile failed. Cannot create directory {}", txt_dir); - return false; - } - - txt_file = filename; - } - - txt_out = FileFinder::Save().OpenOutputStream(txt_file); + auto txt_out = FileFinder::OpenWrite(filename); if (!txt_out) { Output::Warning("Maniac String Op ToFile failed. Cannot write to {}", filename); return false; From 54f7ce3ad93bd9324d5ec9301bd83a7507c34fcb Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 23 Feb 2026 12:24:37 +0100 Subject: [PATCH 19/19] Invalidate the Picture cache when a picture is written to the disk Otherwise an old version of the Picture will be loaded next time ShowPicture is used --- src/cache.cpp | 11 +++++++++++ src/cache.h | 6 ++++++ src/game_interpreter.cpp | 2 ++ 3 files changed, 19 insertions(+) diff --git a/src/cache.cpp b/src/cache.cpp index 6852bb30ac..16969f6598 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -248,6 +248,7 @@ namespace { BitmapRef bmp; const auto key = MakeHashKey(s.directory, filename, transparent, extra_flags); + auto it = cache.find(key); if (it == cache.end()) { if (filename == CACHE_DEFAULT_BITMAP) { @@ -529,6 +530,16 @@ BitmapRef Cache::SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, boo } else { return it->second.lock(); } } +void Cache::Invalidate(std::string_view section) { + for (auto it = cache.begin(); it != cache.end(); ) { + if (StartsWith(it->first, section)) { + it = cache.erase(it); + } else { + ++it; + } + } +} + void Cache::Clear() { cache_effects.clear(); cache.clear(); diff --git a/src/cache.h b/src/cache.h index 4b8882243e..56a752d96c 100644 --- a/src/cache.h +++ b/src/cache.h @@ -58,6 +58,12 @@ namespace Cache { BitmapRef Tile(std::string_view filename, int tile_id); BitmapRef SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, bool flip_x, bool flip_y, const Tone& tone, const Color& blend); + /** + * Removes all cached entries of the given section + * + * @param section Cache section to remove + */ + void Invalidate(std::string_view section); void Clear(); void ClearAll(); diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 8d1ae3dabb..1bfde4f4ad 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5543,6 +5543,8 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c auto img_out = FileFinder::OpenWrite(filename); if (img_out) { bitmap->WritePNG(img_out); + // Not ideal but figuring out the exact cache entry is complicated + Cache::Invalidate("Picture"); } else { Output::Warning("ManiacSaveImage: Failed to open file for writing: {}", filename); }