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..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) { @@ -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/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/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..e30c764f2d 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" @@ -46,7 +47,10 @@ #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" #include "spriteset_map.h" #include "sprite_character.h" #include "scene_gameover.h" @@ -69,6 +73,8 @@ #include "transition.h" #include "baseui.h" #include "algo.h" +#include "sprite_picture.h" +#include "bitmap.h" using namespace Game_Interpreter_Shared; @@ -804,12 +810,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_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 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: @@ -4203,9 +4213,27 @@ 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."); + { + // [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; + + // Creates a snapshot of the current frame + BitmapRef screen = DisplayUi->CaptureScreen(); + Rect frame_rect{pic_x, pic_y, pic_w, pic_h}; + + if (!ManiacPatch::WritePixelsToVariable(*screen, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) { + return true; + } + break; + } case 4: // Get command interpreter state { // Parameter "Nest" in the English version of Maniacs @@ -4678,7 +4706,8 @@ 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]); + auto& pic = Main_Data::game_pictures->GetPicture(pic_id); if (pic.IsRequestPending()) { @@ -4689,16 +4718,55 @@ 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) { + auto* sprite = pic.sprite.get(); + 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 (pic.IsWindowAttached()) { + const auto& window = Main_Data::game_windows->GetWindow(pic_id); + if (window.window) { + bitmap->Clear(); + window.window->Draw(*bitmap); + } + } + + // 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]); + + // Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB) + bool ignore_alpha = (com.parameters[2] & 2) != 0; + + 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)) { + return true; + } + + Game_Map::SetNeedRefresh(true); + return true; + } + // 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); @@ -4715,6 +4783,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); @@ -5288,6 +5359,202 @@ 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& picture = Main_Data::game_pictures->GetPicture(pic_id); + + if (picture.IsRequestPending()) { + picture.MakeRequestImportant(); + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + + auto* sprite = picture.sprite.get(); + auto bitmap = sprite->GetBitmap(); + + // Calculate Spritesheet Offset + // Maniacs operations are relative to the currently active cell. + + // Determine Spritesheet frame + Rect src_rect = sprite->GetSrcRect(); + + BitmapRef writable_bitmap = bitmap; + + 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()) { + // no-op + } else { + // Must be copied to avoid modifiying the original picture + 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 + 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]; + // 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 + + bool clear_dst = !flag_opaq && !flag_skip_trans; + bool ignore_alpha = flag_opaq; + + 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; +} + +bool Game_Interpreter::CommandManiacWritePicture(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 + // 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) + 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..60510aae7e 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -305,12 +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); void SetSubcommandIndex(int indent, int idx); uint8_t& ReserveSubcommandIndex(int indent); diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index 6f7187c2f7..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,10 +509,132 @@ 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; +} + 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..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,7 +126,12 @@ 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; + bool IsCanvas() const; }; Picture& GetPicture(int id); diff --git a/src/game_variables.cpp b/src/game_variables.cpp index 9b37fda8e0..ee5a262491 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,12 +210,14 @@ 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 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) { + 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) { diff --git a/src/game_variables.h b/src/game_variables.h index 1da0393a9d..d28e4b3cf3 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); + 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); Var_t Add(int variable_id, Var_t value); 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/maniac_patch.cpp b/src/maniac_patch.cpp index e672e97429..d87307eb5a 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,175 @@ 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; + 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->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); + 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..02c05992e4 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,31 @@ 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 channel in all pixel 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); 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 } 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; } 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());