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/cache.cpp b/src/cache.cpp index 2fe8f32e47..f3ab61de1d 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) { @@ -535,6 +536,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/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 ac1285ba67..baa68832c8 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -47,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" @@ -70,6 +73,8 @@ #include "transition.h" #include "baseui.h" #include "algo.h" +#include "sprite_picture.h" +#include "bitmap.h" using namespace Game_Interpreter_Shared; @@ -809,14 +814,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 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: @@ -4229,9 +4236,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 @@ -4704,7 +4729,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()) { @@ -4715,16 +4741,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); @@ -4741,6 +4806,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); @@ -5314,6 +5382,75 @@ 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; @@ -5376,8 +5513,17 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c 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()) { - // Maniac ignores the opaque setting for String Picture + // 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) @@ -5417,11 +5563,11 @@ 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); + // 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); } diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 8aba716292..b9775fc58b 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -305,13 +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 fd9df44ac5..9a12debdd5 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" @@ -103,7 +105,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 @@ -368,6 +370,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; } @@ -504,10 +510,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_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; diff --git a/src/game_variables.cpp b/src/game_variables.cpp index 4f77f700bb..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,13 +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; - vars.reserve(length); - 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 bd4d5d5211..28f0b82216 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/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 ac9280c1eb..d5dbef6d1b 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 1c5a28806c..d25ebd108e 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -64,7 +64,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_picture.cpp b/src/sprite_picture.cpp index 0329fe333d..ffcf3a462b 100644 --- a/src/sprite_picture.cpp +++ b/src/sprite_picture.cpp @@ -72,7 +72,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());