From 3da6fd677d80e97ce166ffe656f851cd01cb336c 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 1/3] 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 3e4842ba67..1544e2da80 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4678,9 +4678,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(); @@ -4689,6 +4692,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 df2d93c26af3498322b59cacb9caea0e7b22732d 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 2/3] 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 1544e2da80..e94c91318a 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4203,9 +4203,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 1f6990fd6b83f455b0a10b04f5ed919d5e10ee25 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 3/3] 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 e94c91318a..8dae69cb27 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -69,6 +69,8 @@ #include "transition.h" #include "baseui.h" #include "algo.h" +#include "sprite_picture.h" +#include "bitmap.h" using namespace Game_Interpreter_Shared; @@ -810,6 +812,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: @@ -4426,6 +4430,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 e42c44462f..8baee17b84 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -311,6 +311,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);