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() { 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/filefinder.cpp b/src/filefinder.cpp index 668c0d6fbf..081b10592e 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -432,23 +432,6 @@ std::string find_generic(const DirectoryTree::Args& args) { return FileFinder::Game().FindFile(args); } -std::string find_generic_with_fallback(DirectoryTree::Args& args) { - // Searches first in the Save directory (because the game could have written - // files there, then in the Game directory. - // Disable this behaviour when Game and Save are shared as this breaks the - // translation redirection. - if (Player::shared_game_and_save_directory) { - return find_generic(args); - } - - std::string found = FileFinder::Save().FindFile(args); - if (found.empty()) { - return find_generic(args); - } - - return found; -} - std::string FileFinder::FindImage(std::string_view dir, std::string_view name) { DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, 1, false }; return find_generic(args); @@ -490,16 +473,20 @@ Filesystem_Stream::InputStream open_generic(std::string_view dir, std::string_vi } Filesystem_Stream::InputStream open_generic_with_fallback(std::string_view dir, std::string_view name, DirectoryTree::Args& args) { - if (!Tr::GetCurrentTranslationId().empty()) { - auto tr_fs = Tr::GetCurrentTranslationFilesystem(); - auto is = tr_fs.OpenFile(args); - if (is) { - return is; - } + // Searches first in the Save directory (because the game could have written + // files there, then in the Game directory. + // Disable this behaviour when Game and Save are shared as this breaks the + // translation redirection. + if (Player::shared_game_and_save_directory) { + return open_generic(dir, name, args); } auto is = FileFinder::Save().OpenFile(args); - if (!is) { is = open_generic(dir, name, args); } + + if (!is) { + is = open_generic(dir, name, args); + } + if (!is) { Output::Debug("Unable to open in either Game or Save: {}/{}", dir, name); } @@ -509,7 +496,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 3e4842ba67..ebcc816665 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,124 @@ 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]; + + 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 apply_effects = (flags & 1) != 0; + bool is_opaque = (flags & 2) != 0; + + // Prepare Bitmap + BitmapRef bitmap; + + if (target_type == 0) { + // Target: Screen (.screen) + bitmap = DisplayUi->CaptureScreen(); + } else if (target_type == 1) { + // Target: Picture (.pic) + 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); + + if (picture.IsRequestPending()) { + picture.MakeRequestImportant(); + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + + const auto sprite = picture.sprite.get(); + + // 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); + } + + if (bitmap) { + // 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); + } + } + } + else { + Output::Warning("ManiacSaveImage: Unsupported target type {}", target_type); + return true; + } + + // Save logic + if (bitmap) { + // Save to disk + // Ensure 'filename' has a valid extension (.png). + if (!EndsWith(Utils::LowerCase(filename), ".png")) { + 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); + } 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); 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 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; }