From 038f53b596477d1eb8c64b7d6452a6a8960506f9 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:33:23 -0300 Subject: [PATCH 1/7] Maniacs Patch - ChangePictureId command Added support for the Maniac Patch ChangePictureId event command, enabling move, swap, and slide operations on picture IDs with error handling and sprite refresh logic. This replaces the previous stub implementation and ensures proper picture management for Maniac Patch compatibility. --- src/game_interpreter.cpp | 236 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 234 insertions(+), 2 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index c2ac849c37..8ee1f0c40a 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5025,12 +5025,244 @@ bool Game_Interpreter::CommandManiacControlGlobalSave(lcf::rpg::EventCommand con return true; } -bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const&) { +bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const& com) { + /* + TPC Structure Reference: + @pic[target1].setId .move(target2, size) .ignoreError + @pic[target1].setId .swap(target2, size) .ignoreError + @pic[target1].setId .slide(distance, size) .ignoreError + + Parameters: + [0] Operation: 0 = Move, 1 = Swap, 2 = Slide + [1] Packing: + Bits 0-3: Target 1 Mode (0: Const, 1: Var, 2: Indirect) + Bits 4-7: Size Mode (0: Const, 1: Var, 2: Indirect) + Bits 8-11: Object 2 / Distance Mode (0: Const, 1: Var, 2: Indirect) + [2] Target 1 Value + [3] Size Value + [4] Object 2 / Distance Value + [5] Ignore Error (1 = Ignore) + */ + if (!Player::IsPatchManiac()) { return true; } - Output::Warning("Maniac Patch: Command ChangePictureId not supported"); + int operation = com.parameters[0]; + int target1 = ValueOrVariableBitfield(com, 1, 0, 2); + int size = ValueOrVariableBitfield(com, 1, 1, 3); + int arg3 = ValueOrVariableBitfield(com, 1, 2, 4); // Target 2 or Distance + + bool ignore_error = com.parameters.size() > 5 && com.parameters[5] != 0; + + if (size <= 0) { + return true; + } + + auto& pictures = *Main_Data::game_pictures; + auto& windows = *Main_Data::game_windows; + + auto isValidId = [](int id) { + return id > 0; + }; + + // Helper to move a single picture from src to dst + auto move_picture = [&](int src, int dst) { + // Ensure existence in vectors to avoid reference invalidation during assignments + int max_id = std::max(src, dst); + pictures.GetPicture(max_id); + windows.GetWindow(max_id); + + auto& src_pic = pictures.GetPicture(src); + auto& dst_pic = pictures.GetPicture(dst); + + // If source is empty, erase destination + if (!src_pic.Exists() && !src_pic.IsWindowAttached()) { + dst_pic.Erase(); + return; + } + + // 1. Handle Window Data (String Pictures) + if (src_pic.IsWindowAttached()) { + auto& src_win = windows.GetWindow(src); + auto& dst_win = windows.GetWindow(dst); + dst_win.data = src_win.data; + dst_win.data.ID = dst; + src_win.Erase(); + } + else { + // If overwriting a window picture with a normal one, clear the old window data + // (Safe to call even if dst wasn't a window before) + windows.GetWindow(dst).Erase(); + } + + // 2. Handle Picture Data + BitmapRef src_bmp = src_pic.sprite ? src_pic.sprite->GetBitmap() : nullptr; + auto request_id = src_pic.request_id; + src_pic.request_id = nullptr; // Prevent cancellation on Erase + + dst_pic.data = src_pic.data; + dst_pic.data.ID = dst; + dst_pic.request_id = request_id; + + src_pic.Erase(); + + // 3. Refresh Sprite + if (dst_pic.IsWindowAttached()) { + // Re-attach window to generate sprite + bool async; + windows.GetWindow(dst).Refresh(async); + } + else if (!dst_pic.data.name.empty()) { + if (!dst_pic.sprite) dst_pic.CreateSprite(); + if (src_bmp) { + dst_pic.sprite->SetBitmap(src_bmp); + dst_pic.sprite->OnPictureShow(); + dst_pic.sprite->SetVisible(true); + } + } + else { + dst_pic.sprite.reset(); + } + }; + + // Helper to swap two pictures + auto swap_picture = [&](int id1, int id2) { + // Ensure existence in vectors to avoid reference invalidation during assignments + int max_id = std::max(id1, id2); + pictures.GetPicture(max_id); + windows.GetWindow(max_id); + + auto& p1 = pictures.GetPicture(id1); + auto& p2 = pictures.GetPicture(id2); + + // Swap Window Data + auto& w1 = windows.GetWindow(id1); + auto& w2 = windows.GetWindow(id2); + std::swap(w1.data, w2.data); + w1.data.ID = id1; + w2.data.ID = id2; + + // Swap Picture Data + BitmapRef b1 = p1.sprite ? p1.sprite->GetBitmap() : nullptr; + BitmapRef b2 = p2.sprite ? p2.sprite->GetBitmap() : nullptr; + + using std::swap; + swap(p1.data, p2.data); + swap(p1.request_id, p2.request_id); + + p1.data.ID = id1; + p2.data.ID = id2; + + // Refresh Sprites Helper + auto refresh = [&](Game_Pictures::Picture& p, BitmapRef bmp) { + if (p.IsWindowAttached()) { + bool async; + windows.GetWindow(p.data.ID).Refresh(async); + } + else if (!p.data.name.empty()) { + if (!p.sprite) p.CreateSprite(); + if (bmp) { + p.sprite->SetBitmap(bmp); + p.sprite->OnPictureShow(); + p.sprite->SetVisible(true); + } + } + else { + p.sprite.reset(); + } + }; + + refresh(p1, b2); + refresh(p2, b1); + }; + + if (operation == 0 || operation == 2) { + // Move (0) or Slide (2) + int target2 = (operation == 0) ? arg3 : (target1 + arg3); + + int start = 0; + int end = size; + int step = 1; + + // Handle overlapping ranges + if (target2 > target1) { + start = size - 1; + end = -1; + step = -1; + } + + for (int i = start; i != end; i += step) { + int src_id = target1 + i; + int dst_id = target2 + i; + + if (!isValidId(src_id)) { + if (!ignore_error) { + Output::Warning("Maniac ChangePictureId {}: Invalid Picture ID {}", (operation == 0 ? "Move (Source)" : "Slide (Source)"), src_id); + return true; + } + continue; + } + + if (!isValidId(dst_id)) { + if (!ignore_error) { + Output::Warning("Maniac ChangePictureId {}: Invalid Picture ID {}", (operation == 0 ? "Move (Dest)" : "Slide (Dest)"), dst_id); + return true; + } + + // "If you use the "Ignore out-of-range errors" setting, moving from/to an out-of-range ID will be replaced by a simple delete operation." + // If destination is invalid, delete source. + auto& src_pic = pictures.GetPicture(src_id); + if (src_pic.Exists() || src_pic.IsWindowAttached()) { + pictures.Erase(src_id); + } + continue; + } + + if (src_id != dst_id) { + move_picture(src_id, dst_id); + } + } + } + else if (operation == 1) { + // Swap + int target2 = arg3; + + for (int i = 0; i < size; ++i) { + int id1 = target1 + i; + int id2 = target2 + i; + + bool valid1 = isValidId(id1); + bool valid2 = isValidId(id2); + + if (!valid1 && !ignore_error) { + Output::Warning("Maniac ChangePictureId Swap: Invalid Picture ID {}", id1); + return true; + } + if (!valid2 && !ignore_error) { + Output::Warning("Maniac ChangePictureId Swap: Invalid Picture ID {}", id2); + return true; + } + + if (valid1 && valid2) { + swap_picture(id1, id2); + } + else if (valid1 && !valid2) { + // Valid swap with invalid -> erase valid + pictures.Erase(id1); + } + else if (!valid1 && valid2) { + // Invalid swap with valid -> erase valid + pictures.Erase(id2); + } + } + } + else { + Output::Warning("Maniac ChangePictureId: Unknown operation {}", operation); + } + + Game_Map::SetNeedRefresh(true); + return true; } From 0a487c41e66349adfa9ba4acd18e96edff9e8449 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:44:02 -0300 Subject: [PATCH 2/7] New Feature - Picture Inspector Introduces a new debug scene and windows for viewing and inspecting active pictures and string windows. Updates scene and debug menu logic to support the new 'DebugPicture' scene, adds menu option, and implements detailed property display for both image and string pictures. --- CMakeLists.txt | 4 + src/scene.cpp | 7 +- src/scene.h | 3 +- src/scene_debug.cpp | 6 + src/scene_debug.h | 1 + src/scene_debug_picture.cpp | 68 ++++++++ src/scene_debug_picture.h | 36 ++++ src/window_debug_picture.cpp | 320 +++++++++++++++++++++++++++++++++++ src/window_debug_picture.h | 70 ++++++++ 9 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 src/scene_debug_picture.cpp create mode 100644 src/scene_debug_picture.h create mode 100644 src/window_debug_picture.cpp create mode 100644 src/window_debug_picture.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9707ab6d9e..7bb93f8a83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,6 +308,8 @@ add_library(${PROJECT_NAME} OBJECT src/scene.cpp src/scene_debug.cpp src/scene_debug.h + src/scene_debug_picture.cpp + src/scene_debug_picture.h src/scene_end.cpp src/scene_end.h src/scene_equip.cpp @@ -423,6 +425,8 @@ add_library(${PROJECT_NAME} OBJECT src/window_command.h src/window_command_horizontal.cpp src/window_command_horizontal.h + src/window_debug_picture.cpp + src/window_debug_picture.h src/window.cpp src/window_equip.cpp src/window_equip.h diff --git a/src/scene.cpp b/src/scene.cpp index bc95a3d3f4..e5977e1ade 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -44,7 +44,7 @@ std::shared_ptr Scene::instance; std::vector > Scene::old_instances; std::vector > Scene::instances; -const char Scene::scene_names[SceneMax][12] = +const char Scene::scene_names[SceneMax][13] = { "Null", "Title", @@ -69,7 +69,8 @@ const char Scene::scene_names[SceneMax][12] = "GameBrowser", "Teleport", "Settings", - "Language" + "Language", + "DebugPicture" }; enum PushPopOperation { @@ -116,6 +117,8 @@ lcf::rpg::SaveSystem::Scene Scene::rpgRtSceneFromSceneType(SceneType t) { return lcf::rpg::SaveSystem::Scene_game_over; case Debug: return lcf::rpg::SaveSystem::Scene_debug; + case DebugPicture: + return lcf::rpg::SaveSystem::Scene_debug; } return lcf::rpg::SaveSystem::Scene(-1); } diff --git a/src/scene.h b/src/scene.h index 51d077b43c..d00af5e624 100644 --- a/src/scene.h +++ b/src/scene.h @@ -60,6 +60,7 @@ class Scene { Teleport, Settings, LanguageMenu, + DebugPicture, SceneMax }; @@ -203,7 +204,7 @@ class Scene { static std::vector > old_instances; /** Contains name of the Scenes. For debug purposes. */ - static const char scene_names[SceneMax][12]; + static const char scene_names[SceneMax][13]; /** * Called by the graphic system to request drawing of a background, usually a system color background diff --git a/src/scene_debug.cpp b/src/scene_debug.cpp index 33924ac77b..2c37585ac2 100644 --- a/src/scene_debug.cpp +++ b/src/scene_debug.cpp @@ -34,6 +34,7 @@ #include "scene_menu.h" #include "scene_save.h" #include "scene_map.h" +#include "scene_debug_picture.h" #include "scene_battle.h" #include "player.h" #include "window_command.h" @@ -621,6 +622,10 @@ void Scene_Debug::vUpdate() { PushUiRangeList(); } break; + case ePictureTool: + Scene::Push(std::make_shared()); + mode = eMain; + return; case eInterpreter: if (sz == 3) { auto action = interpreter_window->GetSelectedAction(); @@ -776,6 +781,7 @@ void Scene_Debug::UpdateRangeListWindow() { addItem("Call MapEvent", Scene::Find(Scene::Map) != nullptr); addItem("Call BtlEvent", is_battle); addItem("Strings", Player::IsPatchManiac()); + addItem("Pictures"); addItem("Interpreter"); addItem("Open Menu", !is_battle); } diff --git a/src/scene_debug.h b/src/scene_debug.h index a3efc68e6e..2c04ed97e5 100644 --- a/src/scene_debug.h +++ b/src/scene_debug.h @@ -72,6 +72,7 @@ class Scene_Debug : public Scene { eCallMapEvent, eCallBattleEvent, eString, + ePictureTool, eInterpreter, eOpenMenu, eLastMainMenuOption, diff --git a/src/scene_debug_picture.cpp b/src/scene_debug_picture.cpp new file mode 100644 index 0000000000..ca3b221d7a --- /dev/null +++ b/src/scene_debug_picture.cpp @@ -0,0 +1,68 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "scene_debug_picture.h" +#include "input.h" +#include "player.h" +#include "game_system.h" +#include "main_data.h" + +Scene_DebugPicture::Scene_DebugPicture() { + type = Scene::DebugPicture; +} + +void Scene_DebugPicture::Start() { + // Make list window narrow (just IDs) to maximize info space + int list_w = 64; + + list_window = std::make_unique( + Player::menu_offset_x, + Player::menu_offset_y, + list_w, + MENU_HEIGHT + ); + + info_window = std::make_unique( + Player::menu_offset_x + list_w, + Player::menu_offset_y, + MENU_WIDTH - list_w, + MENU_HEIGHT + ); + + list_window->SetActive(true); + list_window->SetIndex(0); + + // Initialize info window with first item + info_window->SetPictureId(list_window->GetPictureId()); +} + +void Scene_DebugPicture::vUpdate() { + list_window->Update(); + info_window->Update(); + + // Update info window based on selection + if (list_window->GetActive()) { + // Always refresh info window to see real-time coordinate updates + info_window->SetPictureId(list_window->GetPictureId()); + info_window->Refresh(); + } + + if (Input::IsTriggered(Input::CANCEL)) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel)); + Scene::Pop(); + } +} diff --git a/src/scene_debug_picture.h b/src/scene_debug_picture.h new file mode 100644 index 0000000000..dcc5d920dc --- /dev/null +++ b/src/scene_debug_picture.h @@ -0,0 +1,36 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_SCENE_DEBUG_PICTURE_H +#define EP_SCENE_DEBUG_PICTURE_H + +#include "scene.h" +#include "window_debug_picture.h" +#include + +class Scene_DebugPicture : public Scene { +public: + Scene_DebugPicture(); + void Start() override; + void vUpdate() override; + +private: + std::unique_ptr list_window; + std::unique_ptr info_window; +}; + +#endif diff --git a/src/window_debug_picture.cpp b/src/window_debug_picture.cpp new file mode 100644 index 0000000000..09a057566c --- /dev/null +++ b/src/window_debug_picture.cpp @@ -0,0 +1,320 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "window_debug_picture.h" +#include "game_pictures.h" +#include "game_windows.h" +#include "main_data.h" +#include "bitmap.h" +#include "font.h" +#include "utils.h" +#include +#include +#include +#include +#include + +Window_DebugPictureList::Window_DebugPictureList(int x, int y, int w, int h) : + Window_Selectable(x, y, w, h) +{ + SetMenuItemHeight(16); + SetColumnMax(1); + SetContents(Bitmap::Create(width, height)); + Refresh(); +} + +void Window_DebugPictureList::Refresh() { + picture_ids.clear(); + + // Scan all allocated pictures + for (int i = 1; ; ++i) { + auto* pic = Main_Data::game_pictures->GetPicturePtr(i); + if (!pic) { + break; + } + + if (pic->Exists() || pic->IsWindowAttached()) { + picture_ids.push_back(i); + } + } + + item_max = picture_ids.size(); + + int required_height = std::max(height - 32, (int)(item_max * menu_item_height)); + if (contents->GetHeight() != required_height) { + SetContents(Bitmap::Create(contents->GetWidth(), required_height)); + } + + contents->Clear(); + + if (item_max == 0) { + contents->TextDraw(0, 0, Font::ColorDisabled, "No Pic"); + } + else { + for (int i = 0; i < item_max; ++i) { + Rect rect = GetItemRect(i); + int id = picture_ids[i]; + auto* pic = Main_Data::game_pictures->GetPicturePtr(id); + + std::string suffix = ""; + if (pic->IsWindowAttached()) suffix = " T"; // Text/String + + std::string text = fmt::format("{:04d}{}", id, suffix); + contents->TextDraw(rect.x, rect.y, Font::ColorDefault, text); + } + } +} + +int Window_DebugPictureList::GetPictureId() const { + if (index >= 0 && index < static_cast(picture_ids.size())) { + return picture_ids[index]; + } + return 0; +} + +// --------------------------------------------------------------------------- + +Window_DebugPictureInfo::Window_DebugPictureInfo(int x, int y, int w, int h) : + Window_Base(x, y, w, h) +{ + SetContents(Bitmap::Create(width, height)); +} + +void Window_DebugPictureInfo::SetPictureId(int id) { + if (picture_id != id) { + picture_id = id; + Refresh(); + } +} + +int Window_DebugPictureInfo::DrawLine(int y, std::string_view label, std::string_view value) { + contents->TextDraw(0, y, Font::ColorDefault, label); + int val_x = 40; + contents->TextDraw(val_x, y, Font::ColorHeal, value); + return y + 16; +} + +int Window_DebugPictureInfo::DrawDualLine(int y, std::string_view l1, std::string_view v1, std::string_view l2, std::string_view v2) { + contents->TextDraw(0, y, Font::ColorDefault, l1); + contents->TextDraw(40, y, Font::ColorHeal, v1); + + contents->TextDraw(110, y, Font::ColorDefault, l2); + contents->TextDraw(150, y, Font::ColorHeal, v2); + return y + 16; +} + +int Window_DebugPictureInfo::DrawSeparator(int y) { + // Draw a dim line + Color col; + col.alpha = 128; + contents->FillRect(Rect(0, y + 7, contents->GetWidth(), 1), col); + return y + 16; +} + +int Window_DebugPictureInfo::DrawFlags(int y, const std::vector& flags) { + int x = 0; + int row_start_y = y; + + for (const auto& flag : flags) { + int w = Text::GetSize(*Font::Default(), flag.name).width + 4; + if (x + w > contents->GetWidth()) { + x = 0; + y += 16; + } + + // Draw bracketed flag like [FlipX] + std::string text = fmt::format("[{}]", flag.name); + contents->TextDraw(x, y, flag.active ? Font::ColorHeal : Font::ColorDisabled, text); + + x += w + 12; + } + + return y + 16; +} + +void Window_DebugPictureInfo::Refresh() { + contents->Clear(); + + if (picture_id <= 0) { + contents->TextDraw(0, 0, Font::ColorDisabled, "No Selection"); + return; + } + + auto* pic = Main_Data::game_pictures->GetPicturePtr(picture_id); + if (!pic) { + contents->TextDraw(0, 0, Font::ColorCritical, "Invalid ID"); + return; + } + + bool is_str = pic->IsWindowAttached(); + if (!pic->Exists() && !is_str) { + contents->TextDraw(0, 0, Font::ColorDisabled, "Empty"); + return; + } + + const auto& d = pic->data; + int y = 0; + + // === COMMON PROPERTIES === + + // ID & Type + std::string type_str = is_str ? "String" : "Image"; + DrawDualLine(y, "ID", fmt::format("{}", picture_id), "Type", type_str); + y += 16; + + // Position & Movement + std::string pos_str = fmt::format("{:.0f},{:.0f}", d.current_x, d.current_y); + y = DrawLine(y, "Pos", pos_str); + + if (d.time_left > 0 || d.current_x != d.finish_x || d.current_y != d.finish_y) { + std::string goal_str = fmt::format("{:.0f},{:.0f} ({}f)", d.finish_x, d.finish_y, d.time_left); + y = DrawLine(y, "Goal", goal_str); + } + + // Scale + std::string scale_str = fmt::format("{:.0f}", d.current_magnify); + if (d.maniac_current_magnify_height != d.current_magnify) { + scale_str += fmt::format(" / {:.0f}", d.maniac_current_magnify_height); + } + + + // Transparency + std::string trans_str; + if (d.current_top_trans == d.current_bot_trans) { + trans_str = fmt::format("{:.0f}", d.current_top_trans); + } + else { + trans_str = fmt::format("{:.0f}/{:.0f}", d.current_top_trans, d.current_bot_trans); + } + + + y = DrawDualLine(y, "Scale", scale_str + "%", "Trans", trans_str + "%"); + + // Blend & Layer + std::string blend = "None"; + if (d.easyrpg_blend_mode == 1) blend = "Multiply"; + if (d.easyrpg_blend_mode == 2) blend = "Addition"; + if (d.easyrpg_blend_mode == 3) blend = "Overlay"; + + std::string layer = fmt::format("M:{} B:{}", d.map_layer, d.battle_layer); + y = DrawDualLine(y, "Blend", blend, "Layer", layer); + + // Tone (R,G,B,S) + std::string tone_str = fmt::format("{:.0f},{:.0f},{:.0f},{:.0f}", d.current_red, d.current_green, d.current_blue, d.current_sat); + y = DrawLine(y, "Tone", tone_str); + + // Effects + if (d.effect_mode != lcf::rpg::SavePicture::Effect_none) { + std::string effect; + switch (d.effect_mode) { + case lcf::rpg::SavePicture::Effect_rotation: effect = "Rot"; break; + case lcf::rpg::SavePicture::Effect_wave: effect = "Wave"; break; + case lcf::rpg::SavePicture::Effect_maniac_fixed_angle: effect = "Ang"; break; + } + effect += fmt::format(" {:.1f}", d.current_effect_power); + if (d.effect_mode == lcf::rpg::SavePicture::Effect_rotation || d.effect_mode == lcf::rpg::SavePicture::Effect_maniac_fixed_angle) { + effect += fmt::format(" ({:.1f})", d.current_rotation); + } + y = DrawLine(y, "FX", effect); + } + + y = DrawSeparator(y); + // Common Flags + std::vector flags = { + { "Fixed", d.fixed_to_map }, + { "Chroma", d.use_transparent_color }, + { "Tint", d.flags.affected_by_tint }, + { "Flash", d.flags.affected_by_flash }, + { "Shake", d.flags.affected_by_shake }, + { "EraseOnMapChange", d.flags.erase_on_map_change }, + { "EraseAfterBattle", d.flags.erase_on_battle_end }, + { "FlipX", (bool)(d.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_x) }, + { "FlipY", (bool)(d.easyrpg_flip & lcf::rpg::SavePicture::EasyRpgFlip_y) } + }; + y = DrawFlags(y, flags); + + y = DrawSeparator(y); + + // === SPECIFIC PROPERTIES === + + if (!is_str) { + // FILE PICTURE + std::string name_str = std::string(d.name); + if (name_str.length() > 18) name_str = "..." + name_str.substr(name_str.length() - 15); + y = DrawLine(y, "File", name_str); + + if (d.spritesheet_cols > 1 || d.spritesheet_rows > 1) { + std::string cell_str = fmt::format("#{} ({}x{})", d.spritesheet_frame, d.spritesheet_cols, d.spritesheet_rows); + y = DrawLine(y, "Cell", cell_str); + + if (d.spritesheet_speed > 0) { + y = DrawLine(y, "Anim", fmt::format("Spd: {} {}", d.spritesheet_speed, d.spritesheet_play_once ? "[Once]" : "[Loop]")); + } + } + } + else { + // STRING PICTURE + auto& win = Main_Data::game_windows->GetWindow(picture_id); + const auto& wd = win.data; + + std::string dims = fmt::format("{}x{}", wd.width, wd.height); + y = DrawLine(y, "Size", dims); + + std::string skin = std::string(wd.system_name); + if (skin.empty()) skin = "Default"; + y = DrawLine(y, "Skin", skin); + + if (!wd.texts.empty()) { + const auto& txt = wd.texts[0]; // Usually only one text chunk for string pics + + std::string font_info = std::string(txt.font_name); + if (font_info.empty()) font_info = "Sys"; + font_info += fmt::format(" {}pt", txt.font_size); + y = DrawLine(y, "Font", font_info); + + y = DrawDualLine(y, "LSpc", std::to_string(txt.letter_spacing), "HSpc", std::to_string(txt.line_spacing)); + + // String Flags + std::vector str_flags = { + { "Frame", wd.flags.draw_frame }, + { "Grad", txt.flags.draw_gradient }, + { "Shdw", txt.flags.draw_shadow }, + { "Bold", txt.flags.bold }, + { "Ital", txt.flags.italic }, + { "Marg", wd.flags.border_margin } + }; + + // Background type (Stretch/Tile/None) + std::string bg_type = "Stretch"; + if (wd.message_stretch == 0) bg_type = "Tile"; + if (wd.message_stretch == 2) bg_type = "None"; // easyrpg_none + + y = DrawLine(y, "BG", bg_type); + y = DrawFlags(y, str_flags); + + // Content preview + y = DrawSeparator(y); + std::string content = ToString(txt.text); + // Simple replace newlines for preview + content = Utils::ReplaceAll(content, "\n", "\\n"); + if (content.length() > 22) content = content.substr(0, 20) + "..."; + + contents->TextDraw(0, y, Font::ColorDefault, "Text:"); + contents->TextDraw(40, y, Font::ColorDefault, content); + } + } +} diff --git a/src/window_debug_picture.h b/src/window_debug_picture.h new file mode 100644 index 0000000000..54e10cf074 --- /dev/null +++ b/src/window_debug_picture.h @@ -0,0 +1,70 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_WINDOW_DEBUG_PICTURE_H +#define EP_WINDOW_DEBUG_PICTURE_H + +#include "window_selectable.h" +#include "window_base.h" +#include +#include + + /** + * Debug window showing the list of active pictures. + */ +class Window_DebugPictureList : public Window_Selectable { +public: + Window_DebugPictureList(int x, int y, int w, int h); + + void Refresh(); + int GetPictureId() const; + +private: + std::vector picture_ids; +}; + +/** + * Debug window showing details of a specific picture. + */ +class Window_DebugPictureInfo : public Window_Base { +public: + Window_DebugPictureInfo(int x, int y, int w, int h); + + void SetPictureId(int id); + void Refresh(); + +private: + int picture_id = 0; + + // Draw label and value. Returns next Y. + int DrawLine(int y, std::string_view label, std::string_view value); + + // Draw two label/value pairs on one line. Returns next Y. + int DrawDualLine(int y, std::string_view l1, std::string_view v1, std::string_view l2, std::string_view v2); + + // Draw a separator line. + int DrawSeparator(int y); + + // Helper for drawing boolean flags compactly + struct FlagInfo { + const char* name; + bool active; + }; + int DrawFlags(int y, const std::vector& flags); +}; + +#endif From 13e66aeef146f48450dd3cbbd56f9c9c97ae5224 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 02:18:55 +0200 Subject: [PATCH 3/7] Picture Inspector: Refactor to integrate into the Debug Scene directly Get rid of the additional Debug Picture Scene. Rest remains unchanged. --- CMakeLists.txt | 6 +-- src/game_pictures.cpp | 4 ++ src/game_pictures.h | 1 + src/scene.cpp | 7 +--- src/scene.h | 3 +- src/scene_debug.cpp | 63 ++++++++++++++++++++++++++++-- src/scene_debug.h | 13 +++++- src/scene_debug_picture.cpp | 68 -------------------------------- src/scene_debug_picture.h | 36 ----------------- src/window_debug_picture.cpp | 76 +++--------------------------------- src/window_debug_picture.h | 16 -------- src/window_varlist.cpp | 33 ++++++++++++++++ src/window_varlist.h | 8 +++- 13 files changed, 125 insertions(+), 209 deletions(-) delete mode 100644 src/scene_debug_picture.cpp delete mode 100644 src/scene_debug_picture.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb93f8a83..c80302d5ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,8 +308,6 @@ add_library(${PROJECT_NAME} OBJECT src/scene.cpp src/scene_debug.cpp src/scene_debug.h - src/scene_debug_picture.cpp - src/scene_debug_picture.h src/scene_end.cpp src/scene_end.h src/scene_equip.cpp @@ -405,6 +403,8 @@ add_library(${PROJECT_NAME} OBJECT src/version.h src/weather.cpp src/weather.h + src/window.cpp + src/window.h src/window_about.cpp src/window_about.h src/window_actorinfo.cpp @@ -427,7 +427,6 @@ add_library(${PROJECT_NAME} OBJECT src/window_command_horizontal.h src/window_debug_picture.cpp src/window_debug_picture.h - src/window.cpp src/window_equip.cpp src/window_equip.h src/window_equipitem.cpp @@ -440,7 +439,6 @@ add_library(${PROJECT_NAME} OBJECT src/window_gamelist.h src/window_gold.cpp src/window_gold.h - src/window.h src/window_help.cpp src/window_help.h src/window_import_progress.cpp diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index fd9df44ac5..7b6971b56d 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -146,6 +146,10 @@ int Game_Pictures::GetDefaultNumberOfPictures() { return 0; } +int Game_Pictures::GetPictureCount() const { + return static_cast(pictures.size()); +} + Game_Pictures::Picture& Game_Pictures::GetPicture(int id) { if (EP_UNLIKELY(id > static_cast(pictures.size()))) { pictures.reserve(id); diff --git a/src/game_pictures.h b/src/game_pictures.h index 30d0a0251d..a2b07a58ef 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -42,6 +42,7 @@ class Game_Pictures { void InitGraphics(); static int GetDefaultNumberOfPictures(); + int GetPictureCount() const; struct Params { int position_x = 0; diff --git a/src/scene.cpp b/src/scene.cpp index e5977e1ade..bc95a3d3f4 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -44,7 +44,7 @@ std::shared_ptr Scene::instance; std::vector > Scene::old_instances; std::vector > Scene::instances; -const char Scene::scene_names[SceneMax][13] = +const char Scene::scene_names[SceneMax][12] = { "Null", "Title", @@ -69,8 +69,7 @@ const char Scene::scene_names[SceneMax][13] = "GameBrowser", "Teleport", "Settings", - "Language", - "DebugPicture" + "Language" }; enum PushPopOperation { @@ -117,8 +116,6 @@ lcf::rpg::SaveSystem::Scene Scene::rpgRtSceneFromSceneType(SceneType t) { return lcf::rpg::SaveSystem::Scene_game_over; case Debug: return lcf::rpg::SaveSystem::Scene_debug; - case DebugPicture: - return lcf::rpg::SaveSystem::Scene_debug; } return lcf::rpg::SaveSystem::Scene(-1); } diff --git a/src/scene.h b/src/scene.h index d00af5e624..51d077b43c 100644 --- a/src/scene.h +++ b/src/scene.h @@ -60,7 +60,6 @@ class Scene { Teleport, Settings, LanguageMenu, - DebugPicture, SceneMax }; @@ -204,7 +203,7 @@ class Scene { static std::vector > old_instances; /** Contains name of the Scenes. For debug purposes. */ - static const char scene_names[SceneMax][13]; + static const char scene_names[SceneMax][12]; /** * Called by the graphic system to request drawing of a background, usually a system color background diff --git a/src/scene_debug.cpp b/src/scene_debug.cpp index 2c37585ac2..53e3babdf2 100644 --- a/src/scene_debug.cpp +++ b/src/scene_debug.cpp @@ -34,7 +34,7 @@ #include "scene_menu.h" #include "scene_save.h" #include "scene_map.h" -#include "scene_debug_picture.h" +#include "window_debug_picture.h" #include "scene_battle.h" #include "player.h" #include "window_command.h" @@ -42,6 +42,7 @@ #include "window_numberinput.h" #include "bitmap.h" #include "game_party.h" +#include "game_pictures.h" #include "game_player.h" #include #include "output.h" @@ -75,6 +76,7 @@ void Scene_Debug::Start() { CreateChoicesWindow(); CreateStringViewWindow(); CreateInterpreterWindow(); + CreatePictureInfoWindow(); SetupUiRangeList(); @@ -140,6 +142,9 @@ void Scene_Debug::UpdateFrameValueFromUi() { frame.value = GetSelectedIndexFromRange() + interpreter_window->GetIndex(); state_interpreter.selected_frame = interpreter_window->GetSelectedStackFrameLine(); break; + case eUiPictureView: + // Window is not interactive + break; } } @@ -309,6 +314,31 @@ void Scene_Debug::PushUiStringView() { stringview_window->Refresh(); } +void Scene_Debug::PushUiPictureView() { + const auto pic_id = GetFrame().value; + + if (pic_id <= 0) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); + return; + } + + auto* pic = Main_Data::game_pictures->GetPicturePtr(pic_id); + if (!pic || (!pic->Exists() && !pic->IsWindowAttached())) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); + return; + } + + Push(eUiPictureView); + + var_window->SetActive(false); + picture_info_window->SetVisible(true); + + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); + + picture_info_window->SetPictureId(pic_id); + picture_info_window->Refresh(); +} + void Scene_Debug::PushUiInterpreterView() { const bool was_range_list = (GetFrame().uimode == eUiRangeList); @@ -344,6 +374,7 @@ void Scene_Debug::Pop() { stringview_window->SetActive(false); stringview_window->SetVisible(false); interpreter_window->SetActive(false); + picture_info_window->SetVisible(false); if (mode == eInterpreter) { interpreter_window->SetIndex(-1); @@ -402,6 +433,11 @@ void Scene_Debug::Pop() { var_window->SetVisible(false); interpreter_window->SetVisible(true); break; + case eUiPictureView: + picture_info_window->SetVisible(true); + picture_info_window->SetPictureId(frame.value); + picture_info_window->Refresh(); + break; } if (stack_index == 0) { @@ -623,9 +659,14 @@ void Scene_Debug::vUpdate() { } break; case ePictureTool: - Scene::Push(std::make_shared()); - mode = eMain; - return; + if (sz > 2) { + PushUiPictureView(); + } else if (sz > 1) { + PushUiVarList(); + } else { + PushUiRangeList(); + } + break; case eInterpreter: if (sz == 3) { auto action = interpreter_window->GetSelectedAction(); @@ -791,6 +832,7 @@ void Scene_Debug::UpdateRangeListWindow() { case eItem: case eBattle: case eString: + case ePictureTool: fillRange(GetWindowMode()); break; case eMap: @@ -953,6 +995,16 @@ void Scene_Debug::CreateInterpreterWindow() { interpreter_window->SetIndex(-1); } +void Scene_Debug::CreatePictureInfoWindow() { + picture_info_window = std::make_unique( + Player::menu_offset_x + 20, + Player::menu_offset_y + 16, + MENU_WIDTH - 40, + MENU_HEIGHT - 32 + ); + picture_info_window->SetVisible(false); +} + int Scene_Debug::GetNumMainMenuItems() const { return static_cast(eLastMainMenuOption) - 1; } @@ -992,6 +1044,9 @@ int Scene_Debug::GetLastPage() const { case eString: num_elements = Main_Data::game_strings->GetSizeWithLimit(); break; + case ePictureTool: + num_elements = Main_Data::game_pictures->GetPictureCount(); + break; case eInterpreter: num_elements = 1 + state_interpreter.background_states.Count(); return (static_cast(num_elements) - 1) / 10; diff --git a/src/scene_debug.h b/src/scene_debug.h index 2c04ed97e5..ba23cd41f1 100644 --- a/src/scene_debug.h +++ b/src/scene_debug.h @@ -27,6 +27,8 @@ #include "window_varlist.h" #include "window_stringview.h" #include "window_interpreter.h" +#include "window_debug_picture.h" + /** * Scene Equip class. @@ -85,7 +87,8 @@ class Scene_Debug : public Scene { eUiNumberInput, eUiStringView, eUiChoices, - eUiInterpreterView + eUiInterpreterView, + eUiPictureView }; private: Mode mode = eMain; @@ -113,6 +116,9 @@ class Scene_Debug : public Scene { /** Creates interpreter window. */ void CreateInterpreterWindow(); + /** Creates picture info window. */ + void CreatePictureInfoWindow(); + /** Get the last page for the current mode */ int GetLastPage() const; @@ -153,6 +159,8 @@ class Scene_Debug : public Scene { std::unique_ptr stringview_window; /** Displays the currently running inteprreters. */ std::unique_ptr interpreter_window; + /** Displays picture debug info. */ + std::unique_ptr picture_info_window; struct StackFrame { UiMode uimode = eUiMain; @@ -176,6 +184,7 @@ class Scene_Debug : public Scene { void PushUiChoices(std::vector choices, std::vector choices_enabled); void PushUiStringView(); void PushUiInterpreterView(); + void PushUiPictureView(); Window_VarList::Mode GetWindowMode() const; static constexpr Window_VarList::Mode GetWindowMode(Mode mode); @@ -226,6 +235,8 @@ constexpr Window_VarList::Mode Scene_Debug::GetWindowMode(Mode mode) { return Window_VarList::eMapEvent; case eString: return Window_VarList::eString; + case ePictureTool: + return Window_VarList::ePicture; default: return Window_VarList::eNone; } diff --git a/src/scene_debug_picture.cpp b/src/scene_debug_picture.cpp deleted file mode 100644 index ca3b221d7a..0000000000 --- a/src/scene_debug_picture.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of EasyRPG Player. - * - * EasyRPG Player is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EasyRPG Player is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EasyRPG Player. If not, see . - */ - -#include "scene_debug_picture.h" -#include "input.h" -#include "player.h" -#include "game_system.h" -#include "main_data.h" - -Scene_DebugPicture::Scene_DebugPicture() { - type = Scene::DebugPicture; -} - -void Scene_DebugPicture::Start() { - // Make list window narrow (just IDs) to maximize info space - int list_w = 64; - - list_window = std::make_unique( - Player::menu_offset_x, - Player::menu_offset_y, - list_w, - MENU_HEIGHT - ); - - info_window = std::make_unique( - Player::menu_offset_x + list_w, - Player::menu_offset_y, - MENU_WIDTH - list_w, - MENU_HEIGHT - ); - - list_window->SetActive(true); - list_window->SetIndex(0); - - // Initialize info window with first item - info_window->SetPictureId(list_window->GetPictureId()); -} - -void Scene_DebugPicture::vUpdate() { - list_window->Update(); - info_window->Update(); - - // Update info window based on selection - if (list_window->GetActive()) { - // Always refresh info window to see real-time coordinate updates - info_window->SetPictureId(list_window->GetPictureId()); - info_window->Refresh(); - } - - if (Input::IsTriggered(Input::CANCEL)) { - Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel)); - Scene::Pop(); - } -} diff --git a/src/scene_debug_picture.h b/src/scene_debug_picture.h deleted file mode 100644 index dcc5d920dc..0000000000 --- a/src/scene_debug_picture.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of EasyRPG Player. - * - * EasyRPG Player is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EasyRPG Player is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EasyRPG Player. If not, see . - */ - -#ifndef EP_SCENE_DEBUG_PICTURE_H -#define EP_SCENE_DEBUG_PICTURE_H - -#include "scene.h" -#include "window_debug_picture.h" -#include - -class Scene_DebugPicture : public Scene { -public: - Scene_DebugPicture(); - void Start() override; - void vUpdate() override; - -private: - std::unique_ptr list_window; - std::unique_ptr info_window; -}; - -#endif diff --git a/src/window_debug_picture.cpp b/src/window_debug_picture.cpp index 09a057566c..b68ab38104 100644 --- a/src/window_debug_picture.cpp +++ b/src/window_debug_picture.cpp @@ -22,72 +22,11 @@ #include "bitmap.h" #include "font.h" #include "utils.h" -#include -#include +#include #include #include #include -Window_DebugPictureList::Window_DebugPictureList(int x, int y, int w, int h) : - Window_Selectable(x, y, w, h) -{ - SetMenuItemHeight(16); - SetColumnMax(1); - SetContents(Bitmap::Create(width, height)); - Refresh(); -} - -void Window_DebugPictureList::Refresh() { - picture_ids.clear(); - - // Scan all allocated pictures - for (int i = 1; ; ++i) { - auto* pic = Main_Data::game_pictures->GetPicturePtr(i); - if (!pic) { - break; - } - - if (pic->Exists() || pic->IsWindowAttached()) { - picture_ids.push_back(i); - } - } - - item_max = picture_ids.size(); - - int required_height = std::max(height - 32, (int)(item_max * menu_item_height)); - if (contents->GetHeight() != required_height) { - SetContents(Bitmap::Create(contents->GetWidth(), required_height)); - } - - contents->Clear(); - - if (item_max == 0) { - contents->TextDraw(0, 0, Font::ColorDisabled, "No Pic"); - } - else { - for (int i = 0; i < item_max; ++i) { - Rect rect = GetItemRect(i); - int id = picture_ids[i]; - auto* pic = Main_Data::game_pictures->GetPicturePtr(id); - - std::string suffix = ""; - if (pic->IsWindowAttached()) suffix = " T"; // Text/String - - std::string text = fmt::format("{:04d}{}", id, suffix); - contents->TextDraw(rect.x, rect.y, Font::ColorDefault, text); - } - } -} - -int Window_DebugPictureList::GetPictureId() const { - if (index >= 0 && index < static_cast(picture_ids.size())) { - return picture_ids[index]; - } - return 0; -} - -// --------------------------------------------------------------------------- - Window_DebugPictureInfo::Window_DebugPictureInfo(int x, int y, int w, int h) : Window_Base(x, y, w, h) { @@ -127,7 +66,6 @@ int Window_DebugPictureInfo::DrawSeparator(int y) { int Window_DebugPictureInfo::DrawFlags(int y, const std::vector& flags) { int x = 0; - int row_start_y = y; for (const auto& flag : flags) { int w = Text::GetSize(*Font::Default(), flag.name).width + 4; @@ -169,8 +107,6 @@ void Window_DebugPictureInfo::Refresh() { const auto& d = pic->data; int y = 0; - // === COMMON PROPERTIES === - // ID & Type std::string type_str = is_str ? "String" : "Image"; DrawDualLine(y, "ID", fmt::format("{}", picture_id), "Type", type_str); @@ -190,7 +126,7 @@ void Window_DebugPictureInfo::Refresh() { if (d.maniac_current_magnify_height != d.current_magnify) { scale_str += fmt::format(" / {:.0f}", d.maniac_current_magnify_height); } - + // Transparency std::string trans_str; @@ -200,7 +136,7 @@ void Window_DebugPictureInfo::Refresh() { else { trans_str = fmt::format("{:.0f}/{:.0f}", d.current_top_trans, d.current_bot_trans); } - + y = DrawDualLine(y, "Scale", scale_str + "%", "Trans", trans_str + "%"); @@ -249,10 +185,8 @@ void Window_DebugPictureInfo::Refresh() { y = DrawSeparator(y); - // === SPECIFIC PROPERTIES === - if (!is_str) { - // FILE PICTURE + // File Picture std::string name_str = std::string(d.name); if (name_str.length() > 18) name_str = "..." + name_str.substr(name_str.length() - 15); y = DrawLine(y, "File", name_str); @@ -267,7 +201,7 @@ void Window_DebugPictureInfo::Refresh() { } } else { - // STRING PICTURE + // String Picture auto& win = Main_Data::game_windows->GetWindow(picture_id); const auto& wd = win.data; diff --git a/src/window_debug_picture.h b/src/window_debug_picture.h index 54e10cf074..58655a6f25 100644 --- a/src/window_debug_picture.h +++ b/src/window_debug_picture.h @@ -18,24 +18,8 @@ #ifndef EP_WINDOW_DEBUG_PICTURE_H #define EP_WINDOW_DEBUG_PICTURE_H -#include "window_selectable.h" #include "window_base.h" #include -#include - - /** - * Debug window showing the list of active pictures. - */ -class Window_DebugPictureList : public Window_Selectable { -public: - Window_DebugPictureList(int x, int y, int w, int h); - - void Refresh(); - int GetPictureId() const; - -private: - std::vector picture_ids; -}; /** * Debug window showing details of a specific picture. diff --git a/src/window_varlist.cpp b/src/window_varlist.cpp index 93fce1efb2..169727b39a 100644 --- a/src/window_varlist.cpp +++ b/src/window_varlist.cpp @@ -22,14 +22,17 @@ #include "game_switches.h" #include "game_variables.h" #include "game_strings.h" +#include "game_pictures.h" #include "bitmap.h" #include #include #include "input.h" +#include "main_data.h" #include "output.h" #include "game_party.h" #include "game_map.h" #include "game_system.h" +#include constexpr int LINE_COUNT = 10; @@ -120,6 +123,18 @@ void Window_VarList::DrawItemValue(int index){ case eString: DrawStringVarItem(index, y); break; + case ePicture: { + auto* pic = Main_Data::game_pictures->GetPicturePtr(first_var + index); + if (pic && (pic->Exists() || pic->IsWindowAttached())) { + auto pos_str = fmt::format("{:.0f},{:.0f}", pic->data.current_x, pic->data.current_y); + contents->TextDraw(GetWidth() - 16, y, Font::ColorHeal, pos_str, Text::AlignRight); + } else { + const int space_reserved = (GetDigitCount() + 2); + int x = space_reserved * 6; + contents->TextDraw(x, y, Font::ColorDisabled, "undefined"); + } + break; + } case eNone: break; } @@ -197,6 +212,19 @@ void Window_VarList::UpdateList(int first_value){ ss << strvar_name; } break; + case ePicture: { + auto* pic = Main_Data::game_pictures->GetPicturePtr(first_value + i); + if (pic->IsWindowAttached()) { + ss << "[String]"; + } else { + std::string name = ToString(pic->data.name); + if (name.length() > 14) { + name = name.substr(0, 11) + "..."; + } + ss << name; + } + break; + } default: break; } @@ -239,6 +267,9 @@ bool Window_VarList::DataIsValid(int range_index) { return Game_Map::GetEvent(range_index) != nullptr; case eString: return range_index > 0 && range_index <= Main_Data::game_strings->GetSizeWithLimit(); + case ePicture: { + return range_index > 0 && range_index <= Main_Data::game_pictures->GetPictureCount(); + } default: break; } @@ -263,6 +294,8 @@ int Window_VarList::GetNumElements(Mode mode) { return Game_Map::GetHighestEventId(); case eString: return Main_Data::game_strings->GetSizeWithLimit(); + case ePicture: + return Main_Data::game_pictures->GetPictureCount(); default: return -1; } diff --git a/src/window_varlist.h b/src/window_varlist.h index 0113e65a5f..51e104350b 100644 --- a/src/window_varlist.h +++ b/src/window_varlist.h @@ -35,7 +35,8 @@ class Window_VarList : public Window_Selectable eLevel, eCommonEvent, eMapEvent, - eString + eString, + ePicture }; /** @@ -50,7 +51,7 @@ class Window_VarList : public Window_Selectable /** * UpdateList. - * + * * @param first_value starting value. */ void UpdateList(int first_value); @@ -132,6 +133,8 @@ constexpr std::string_view Window_VarList::GetPrefix(Mode mode) { return "Me"; case eString: return "St"; + case ePicture: + return "Pi"; default: assert(false); return {}; @@ -141,6 +144,7 @@ constexpr std::string_view Window_VarList::GetPrefix(Mode mode) { constexpr int Window_VarList::GetItemCount(Mode mode, bool show_detail) { switch (mode) { case eString: + if (show_detail) { return 5; } From 0b11f6e0057faf17b7ce9832fee6228a8f8b67ae Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 13 Jul 2026 02:22:21 +0200 Subject: [PATCH 4/7] Command ChangePictureId: Extract Move and SwapPictureId into Functions in Game Pictures --- src/game_interpreter.cpp | 116 +-------------------------------------- src/game_pictures.cpp | 113 ++++++++++++++++++++++++++++++++++++++ src/game_pictures.h | 16 ++++++ 3 files changed, 131 insertions(+), 114 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 8ee1f0c40a..2c579eada4 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5060,123 +5060,11 @@ bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const } auto& pictures = *Main_Data::game_pictures; - auto& windows = *Main_Data::game_windows; auto isValidId = [](int id) { return id > 0; }; - // Helper to move a single picture from src to dst - auto move_picture = [&](int src, int dst) { - // Ensure existence in vectors to avoid reference invalidation during assignments - int max_id = std::max(src, dst); - pictures.GetPicture(max_id); - windows.GetWindow(max_id); - - auto& src_pic = pictures.GetPicture(src); - auto& dst_pic = pictures.GetPicture(dst); - - // If source is empty, erase destination - if (!src_pic.Exists() && !src_pic.IsWindowAttached()) { - dst_pic.Erase(); - return; - } - - // 1. Handle Window Data (String Pictures) - if (src_pic.IsWindowAttached()) { - auto& src_win = windows.GetWindow(src); - auto& dst_win = windows.GetWindow(dst); - dst_win.data = src_win.data; - dst_win.data.ID = dst; - src_win.Erase(); - } - else { - // If overwriting a window picture with a normal one, clear the old window data - // (Safe to call even if dst wasn't a window before) - windows.GetWindow(dst).Erase(); - } - - // 2. Handle Picture Data - BitmapRef src_bmp = src_pic.sprite ? src_pic.sprite->GetBitmap() : nullptr; - auto request_id = src_pic.request_id; - src_pic.request_id = nullptr; // Prevent cancellation on Erase - - dst_pic.data = src_pic.data; - dst_pic.data.ID = dst; - dst_pic.request_id = request_id; - - src_pic.Erase(); - - // 3. Refresh Sprite - if (dst_pic.IsWindowAttached()) { - // Re-attach window to generate sprite - bool async; - windows.GetWindow(dst).Refresh(async); - } - else if (!dst_pic.data.name.empty()) { - if (!dst_pic.sprite) dst_pic.CreateSprite(); - if (src_bmp) { - dst_pic.sprite->SetBitmap(src_bmp); - dst_pic.sprite->OnPictureShow(); - dst_pic.sprite->SetVisible(true); - } - } - else { - dst_pic.sprite.reset(); - } - }; - - // Helper to swap two pictures - auto swap_picture = [&](int id1, int id2) { - // Ensure existence in vectors to avoid reference invalidation during assignments - int max_id = std::max(id1, id2); - pictures.GetPicture(max_id); - windows.GetWindow(max_id); - - auto& p1 = pictures.GetPicture(id1); - auto& p2 = pictures.GetPicture(id2); - - // Swap Window Data - auto& w1 = windows.GetWindow(id1); - auto& w2 = windows.GetWindow(id2); - std::swap(w1.data, w2.data); - w1.data.ID = id1; - w2.data.ID = id2; - - // Swap Picture Data - BitmapRef b1 = p1.sprite ? p1.sprite->GetBitmap() : nullptr; - BitmapRef b2 = p2.sprite ? p2.sprite->GetBitmap() : nullptr; - - using std::swap; - swap(p1.data, p2.data); - swap(p1.request_id, p2.request_id); - - p1.data.ID = id1; - p2.data.ID = id2; - - // Refresh Sprites Helper - auto refresh = [&](Game_Pictures::Picture& p, BitmapRef bmp) { - if (p.IsWindowAttached()) { - bool async; - windows.GetWindow(p.data.ID).Refresh(async); - } - else if (!p.data.name.empty()) { - if (!p.sprite) p.CreateSprite(); - if (bmp) { - p.sprite->SetBitmap(bmp); - p.sprite->OnPictureShow(); - p.sprite->SetVisible(true); - } - } - else { - p.sprite.reset(); - } - }; - - refresh(p1, b2); - refresh(p2, b1); - }; - if (operation == 0 || operation == 2) { // Move (0) or Slide (2) int target2 = (operation == 0) ? arg3 : (target1 + arg3); @@ -5220,7 +5108,7 @@ bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const } if (src_id != dst_id) { - move_picture(src_id, dst_id); + pictures.MovePictureId(src_id, dst_id); } } } @@ -5245,7 +5133,7 @@ bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const } if (valid1 && valid2) { - swap_picture(id1, id2); + pictures.SwapPictureId(id1, id2); } else if (valid1 && !valid2) { // Valid swap with invalid -> erase valid diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index 7b6971b56d..af71e0a336 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -658,3 +658,116 @@ void Game_Pictures::Picture::SetNonEffectParams(const Params& params, bool set_p int Game_Pictures::Picture::NumSpriteSheetFrames() const { return data.spritesheet_cols * data.spritesheet_rows; } + +void Game_Pictures::MovePictureId(int src_id, int dst_id) { + auto& pictures = *this; + auto& windows = *Main_Data::game_windows; + + // Ensure existence in vectors to avoid reference invalidation during assignments + int max_id = std::max(src_id, dst_id); + pictures.GetPicture(max_id); + windows.GetWindow(max_id); + + auto& src_pic = pictures.GetPicture(src_id); + auto& dst_pic = pictures.GetPicture(dst_id); + + // If source is empty, erase destination + if (!src_pic.Exists() && !src_pic.IsWindowAttached()) { + dst_pic.Erase(); + return; + } + + // Handle Window Data (String Pictures) + if (src_pic.IsWindowAttached()) { + auto& src_win = windows.GetWindow(src_id); + auto& dst_win = windows.GetWindow(dst_id); + dst_win.data = src_win.data; + dst_win.data.ID = dst_id; + src_win.Erase(); + } + else { + // If overwriting a window picture with a normal one, clear the old window data + windows.GetWindow(dst_id).Erase(); + } + + // Handle Picture Data + BitmapRef src_bmp = src_pic.sprite ? src_pic.sprite->GetBitmap() : nullptr; + auto request_id = src_pic.request_id; + src_pic.request_id = nullptr; // Prevent cancellation on Erase + + dst_pic.data = src_pic.data; + dst_pic.data.ID = dst_id; + dst_pic.request_id = request_id; + + src_pic.Erase(); + + // Refresh Sprite + if (dst_pic.IsWindowAttached()) { + bool async; + windows.GetWindow(dst_id).Refresh(async); + } + else if (!dst_pic.data.name.empty()) { + if (!dst_pic.sprite) dst_pic.CreateSprite(); + if (src_bmp) { + dst_pic.sprite->SetBitmap(src_bmp); + dst_pic.sprite->OnPictureShow(); + dst_pic.sprite->SetVisible(true); + } + } + else { + dst_pic.sprite.reset(); + } +} + +void Game_Pictures::SwapPictureId(int id1, int id2) { + auto& pictures = *this; + auto& windows = *Main_Data::game_windows; + + // Ensure existence in vectors to avoid reference invalidation during assignments + int max_id = std::max(id1, id2); + pictures.GetPicture(max_id); + windows.GetWindow(max_id); + + auto& p1 = pictures.GetPicture(id1); + auto& p2 = pictures.GetPicture(id2); + + // Swap Window Data + auto& w1 = windows.GetWindow(id1); + auto& w2 = windows.GetWindow(id2); + std::swap(w1.data, w2.data); + w1.data.ID = id1; + w2.data.ID = id2; + + // Swap Picture Data + BitmapRef b1 = p1.sprite ? p1.sprite->GetBitmap() : nullptr; + BitmapRef b2 = p2.sprite ? p2.sprite->GetBitmap() : nullptr; + + using std::swap; + swap(p1.data, p2.data); + swap(p1.request_id, p2.request_id); + + p1.data.ID = id1; + p2.data.ID = id2; + + // Rebuild each picture's visual with the other's bitmap + auto refresh = [&](Picture& p, BitmapRef bmp) { + if (p.IsWindowAttached()) { + bool async; + windows.GetWindow(p.data.ID).Refresh(async); + } + else if (!p.data.name.empty()) { + if (!p.sprite) p.CreateSprite(); + if (bmp) { + p.sprite->SetBitmap(bmp); + p.sprite->OnPictureShow(); + p.sprite->SetVisible(true); + } + } + else { + p.sprite.reset(); + } + }; + + refresh(p1, b2); + refresh(p2, b1); +} diff --git a/src/game_pictures.h b/src/game_pictures.h index a2b07a58ef..dcf9fbe902 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -134,6 +134,22 @@ class Game_Pictures { Picture& GetPicture(int id); Picture* GetPicturePtr(int id); + /** + * Moves picture data to a different ID + * + * @param src_id Source ID to move from + * @param dst_id Destination ID to move to + */ + void MovePictureId(int src_id, int dst_id); + + /** + * Swaps picture data between two IDs + * + * @param id1 First ID to swap with + * @param id2 Second ID to swap with + */ + void SwapPictureId(int id1, int id2); + private: void RequestPictureSprite(Picture& pic); void OnPictureSpriteReady(FileRequestResult*, int id); From 6c63f24199d4f0b0821a9d3b62f1d99d4605627a Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 31 Jul 2026 16:34:52 +0200 Subject: [PATCH 5/7] Also consider picture as Existing when a Window is attached They were considered not existing as they have no filename. The window is deattached after erase --- src/game_pictures.cpp | 2 +- src/scene_debug.cpp | 2 +- src/window_debug_picture.cpp | 2 +- src/window_varlist.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index af71e0a336..54625d4325 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -351,7 +351,7 @@ void Game_Pictures::EraseAll() { bool Game_Pictures::Picture::Exists() const { // Incompatible with the Yume2kki edge-case that uses empty filenames - return !data.name.empty(); + return !data.name.empty() || IsWindowAttached(); } void Game_Pictures::Picture::CreateSprite() { diff --git a/src/scene_debug.cpp b/src/scene_debug.cpp index 53e3babdf2..69d1f8d4b1 100644 --- a/src/scene_debug.cpp +++ b/src/scene_debug.cpp @@ -323,7 +323,7 @@ void Scene_Debug::PushUiPictureView() { } auto* pic = Main_Data::game_pictures->GetPicturePtr(pic_id); - if (!pic || (!pic->Exists() && !pic->IsWindowAttached())) { + if (!pic || (!pic->Exists())) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); return; } diff --git a/src/window_debug_picture.cpp b/src/window_debug_picture.cpp index b68ab38104..91330657f4 100644 --- a/src/window_debug_picture.cpp +++ b/src/window_debug_picture.cpp @@ -99,7 +99,7 @@ void Window_DebugPictureInfo::Refresh() { } bool is_str = pic->IsWindowAttached(); - if (!pic->Exists() && !is_str) { + if (!pic->Exists()) { contents->TextDraw(0, 0, Font::ColorDisabled, "Empty"); return; } diff --git a/src/window_varlist.cpp b/src/window_varlist.cpp index 169727b39a..d7e6b4d553 100644 --- a/src/window_varlist.cpp +++ b/src/window_varlist.cpp @@ -125,7 +125,7 @@ void Window_VarList::DrawItemValue(int index){ break; case ePicture: { auto* pic = Main_Data::game_pictures->GetPicturePtr(first_var + index); - if (pic && (pic->Exists() || pic->IsWindowAttached())) { + if (pic && (pic->Exists())) { auto pos_str = fmt::format("{:.0f},{:.0f}", pic->data.current_x, pic->data.current_y); contents->TextDraw(GetWidth() - 16, y, Font::ColorHeal, pos_str, Text::AlignRight); } else { From eff8b8e5d54944b84dfa30a7e23083c79fe1d5b5 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 31 Jul 2026 16:37:36 +0200 Subject: [PATCH 6/7] Maniac ChangePictureId: Proper implementation Was able to delete a huge amount of the AI boilerplate My favourite optimisation is noticing that a Move can be implemented in terms of a Swap+Delete --- src/game_interpreter.cpp | 130 +++++++++++++------------------- src/game_pictures.cpp | 156 +++++++++++++++------------------------ src/game_pictures.h | 13 +++- src/game_windows.cpp | 45 +++++++++++ src/game_windows.h | 18 +++++ src/sprite_picture.h | 15 +++- 6 files changed, 200 insertions(+), 177 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 2c579eada4..ebf3612b21 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -5048,10 +5048,16 @@ bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const return true; } - int operation = com.parameters[0]; - int target1 = ValueOrVariableBitfield(com, 1, 0, 2); + enum class Op { + Move, + Swap, + Slide + }; + + int op = com.parameters[0]; + int from_id = ValueOrVariableBitfield(com, 1, 0, 2); int size = ValueOrVariableBitfield(com, 1, 1, 3); - int arg3 = ValueOrVariableBitfield(com, 1, 2, 4); // Target 2 or Distance + int arg = ValueOrVariableBitfield(com, 1, 2, 4); // Target 2 or Distance bool ignore_error = com.parameters.size() > 5 && com.parameters[5] != 0; @@ -5061,95 +5067,63 @@ bool Game_Interpreter::CommandManiacChangePictureId(lcf::rpg::EventCommand const auto& pictures = *Main_Data::game_pictures; - auto isValidId = [](int id) { - return id > 0; - }; + auto checkValidId = [&](int id, const char* msg) { + bool valid = id > 0; - if (operation == 0 || operation == 2) { - // Move (0) or Slide (2) - int target2 = (operation == 0) ? arg3 : (target1 + arg3); + if (!valid) { + auto outmsg = fmt::format("Maniac ChangePictureId {}: Invalid Picture ID {}", msg, id); + if (ignore_error) { + Output::DebugStr(outmsg); + } else { + Output::WarningStr(outmsg); + } + } - int start = 0; - int end = size; - int step = 1; + return valid; + }; - // Handle overlapping ranges - if (target2 > target1) { - start = size - 1; - end = -1; - step = -1; - } + if (op < 0 || op > 2) { + Output::Warning("Maniac ChangePictureId: Unknown operation {}", op); + return true; + } - for (int i = start; i != end; i += step) { - int src_id = target1 + i; - int dst_id = target2 + i; + auto operation = static_cast(op); - if (!isValidId(src_id)) { - if (!ignore_error) { - Output::Warning("Maniac ChangePictureId {}: Invalid Picture ID {}", (operation == 0 ? "Move (Source)" : "Slide (Source)"), src_id); - return true; - } - continue; - } + if (operation == Op::Slide) { + arg = from_id + arg; + } - if (!isValidId(dst_id)) { - if (!ignore_error) { - Output::Warning("Maniac ChangePictureId {}: Invalid Picture ID {}", (operation == 0 ? "Move (Dest)" : "Slide (Dest)"), dst_id); - return true; - } + auto func = (operation == Op::Swap) + ? &Game_Pictures::SwapPicture + : &Game_Pictures::MovePicture; - // "If you use the "Ignore out-of-range errors" setting, moving from/to an out-of-range ID will be replaced by a simple delete operation." - // If destination is invalid, delete source. - auto& src_pic = pictures.GetPicture(src_id); - if (src_pic.Exists() || src_pic.IsWindowAttached()) { - pictures.Erase(src_id); - } - continue; - } + for (int i = 0; i < size; ++i) { + int idx = i; - if (src_id != dst_id) { - pictures.MovePictureId(src_id, dst_id); - } + // Handle overlapping ranges + if (arg > from_id) { + idx = size - 1 - i; } - } - else if (operation == 1) { - // Swap - int target2 = arg3; - - for (int i = 0; i < size; ++i) { - int id1 = target1 + i; - int id2 = target2 + i; - bool valid1 = isValidId(id1); - bool valid2 = isValidId(id2); + int src_id = from_id + idx; + int dst_id = arg + idx; - if (!valid1 && !ignore_error) { - Output::Warning("Maniac ChangePictureId Swap: Invalid Picture ID {}", id1); - return true; - } - if (!valid2 && !ignore_error) { - Output::Warning("Maniac ChangePictureId Swap: Invalid Picture ID {}", id2); - return true; - } + if (!checkValidId(src_id, ( + operation == Op::Move ? "Move (Source)" : + operation == Op::Swap ? "Swap (Source)" : "Slide (Source)"))) { + if (operation == Op::Swap) { + // Based on tests swapping with an invalid src is a no-op + continue; + } + } - if (valid1 && valid2) { - pictures.SwapPictureId(id1, id2); - } - else if (valid1 && !valid2) { - // Valid swap with invalid -> erase valid - pictures.Erase(id1); - } - else if (!valid1 && valid2) { - // Invalid swap with valid -> erase valid - pictures.Erase(id2); - } + if (!checkValidId(dst_id, ( + operation == Op::Move ? "Move (Dest)" : + operation == Op::Swap ? "Swap (Dest)" : "Slide (Dest)"))) { } - } - else { - Output::Warning("Maniac ChangePictureId: Unknown operation {}", operation); - } - Game_Map::SetNeedRefresh(true); + (pictures.*func)(src_id, dst_id); + } return true; } diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index 54625d4325..2b16c99160 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -151,6 +151,7 @@ int Game_Pictures::GetPictureCount() const { } Game_Pictures::Picture& Game_Pictures::GetPicture(int id) { + assert(id > 0); if (EP_UNLIKELY(id > static_cast(pictures.size()))) { pictures.reserve(id); while (static_cast(pictures.size()) < id) { @@ -161,7 +162,7 @@ Game_Pictures::Picture& Game_Pictures::GetPicture(int id) { } Game_Pictures::Picture* Game_Pictures::GetPicturePtr(int id) { - return id <= static_cast(pictures.size()) + return id > 0 && id <= static_cast(pictures.size()) ? &pictures[id - 1] : nullptr; } @@ -466,6 +467,9 @@ void Game_Pictures::Picture::ApplyOrigin(bool is_move) { } data.finish_x = x; data.finish_y = y; + + // Origin was applied, prevent applying again in later calls + origin = 0; } void Game_Pictures::Picture::OnMapScrolled(int dx16, int dy16) { @@ -659,115 +663,75 @@ int Game_Pictures::Picture::NumSpriteSheetFrames() const { return data.spritesheet_cols * data.spritesheet_rows; } -void Game_Pictures::MovePictureId(int src_id, int dst_id) { - auto& pictures = *this; - auto& windows = *Main_Data::game_windows; - - // Ensure existence in vectors to avoid reference invalidation during assignments - int max_id = std::max(src_id, dst_id); - pictures.GetPicture(max_id); - windows.GetWindow(max_id); - - auto& src_pic = pictures.GetPicture(src_id); - auto& dst_pic = pictures.GetPicture(dst_id); - - // If source is empty, erase destination - if (!src_pic.Exists() && !src_pic.IsWindowAttached()) { - dst_pic.Erase(); +void Game_Pictures::MovePicture(int src_id, int dst_id) { + if (src_id == dst_id) { return; } - // Handle Window Data (String Pictures) - if (src_pic.IsWindowAttached()) { - auto& src_win = windows.GetWindow(src_id); - auto& dst_win = windows.GetWindow(dst_id); - dst_win.data = src_win.data; - dst_win.data.ID = dst_id; - src_win.Erase(); - } - else { - // If overwriting a window picture with a normal one, clear the old window data - windows.GetWindow(dst_id).Erase(); - } - - // Handle Picture Data - BitmapRef src_bmp = src_pic.sprite ? src_pic.sprite->GetBitmap() : nullptr; - auto request_id = src_pic.request_id; - src_pic.request_id = nullptr; // Prevent cancellation on Erase - - dst_pic.data = src_pic.data; - dst_pic.data.ID = dst_id; - dst_pic.request_id = request_id; - - src_pic.Erase(); - - // Refresh Sprite - if (dst_pic.IsWindowAttached()) { - bool async; - windows.GetWindow(dst_id).Refresh(async); - } - else if (!dst_pic.data.name.empty()) { - if (!dst_pic.sprite) dst_pic.CreateSprite(); - if (src_bmp) { - dst_pic.sprite->SetBitmap(src_bmp); - dst_pic.sprite->OnPictureShow(); - dst_pic.sprite->SetVisible(true); - } - } - else { - dst_pic.sprite.reset(); + // Delete the destination, then swap + if (dst_id > 0) { + auto& dst_pic = GetPicture(dst_id); + dst_pic.Erase(); } + + SwapPicture(src_id, dst_id); } -void Game_Pictures::SwapPictureId(int id1, int id2) { - auto& pictures = *this; - auto& windows = *Main_Data::game_windows; +void Game_Pictures::SwapPicture(int id1, int id2) { + if (id1 == id2 || (id1 <= 0 && id2 <= 0)) { + return; + } - // Ensure existence in vectors to avoid reference invalidation during assignments - int max_id = std::max(id1, id2); - pictures.GetPicture(max_id); - windows.GetWindow(max_id); + auto max_id = std::max(id1, id2); + GetPicture(max_id); // Preallocate to ensure references are stable - auto& p1 = pictures.GetPicture(id1); - auto& p2 = pictures.GetPicture(id2); + Picture bad_pic{0}; // Sentinel when one of the pictures is invalid - // Swap Window Data - auto& w1 = windows.GetWindow(id1); - auto& w2 = windows.GetWindow(id2); - std::swap(w1.data, w2.data); - w1.data.ID = id1; - w2.data.ID = id2; + auto* src_pic = &bad_pic; + auto* dst_pic = &bad_pic; - // Swap Picture Data - BitmapRef b1 = p1.sprite ? p1.sprite->GetBitmap() : nullptr; - BitmapRef b2 = p2.sprite ? p2.sprite->GetBitmap() : nullptr; + if (id1 > 0) { + src_pic = &GetPicture(id1); + } else { + bad_pic = Picture(id1); + } - using std::swap; - swap(p1.data, p2.data); - swap(p1.request_id, p2.request_id); + if (id2 > 0) { + dst_pic = &GetPicture(id2); + } else { + bad_pic = Picture(id2); + } - p1.data.ID = id1; - p2.data.ID = id2; + // Handle Window Data (String Pictures) + if (src_pic->IsWindowAttached() || dst_pic->IsWindowAttached()) { + Main_Data::game_windows->SwapWindow(id1, id2); + } - // Rebuild each picture's visual with the other's bitmap - auto refresh = [&](Picture& p, BitmapRef bmp) { - if (p.IsWindowAttached()) { - bool async; - windows.GetWindow(p.data.ID).Refresh(async); + std::swap(src_pic->data.ID, dst_pic->data.ID); + if (src_pic->sprite) { + if (id2 <= 0) { + src_pic->sprite.reset(); + } else { + src_pic->sprite->SetPictureId(id2); } - else if (!p.data.name.empty()) { - if (!p.sprite) p.CreateSprite(); - if (bmp) { - p.sprite->SetBitmap(bmp); - p.sprite->OnPictureShow(); - p.sprite->SetVisible(true); - } - } - else { - p.sprite.reset(); + } + if (dst_pic->sprite) { + if (id1 <= 0) { + dst_pic->sprite.reset(); + } else { + dst_pic->sprite->SetPictureId(id1); } - }; + } + + // Cancel pending requests and restart them + if (src_pic->IsRequestPending()) { + RequestPictureSprite(*src_pic); + } + + if (dst_pic->IsRequestPending()) { + RequestPictureSprite(*dst_pic); + } - refresh(p1, b2); - refresh(p2, b1); + // Must be last (invalidates references) + std::swap(*src_pic, *dst_pic); } diff --git a/src/game_pictures.h b/src/game_pictures.h index dcf9fbe902..f82fb31d2d 100644 --- a/src/game_pictures.h +++ b/src/game_pictures.h @@ -131,7 +131,16 @@ class Game_Pictures { bool IsWindowAttached() const; }; + /** + * @param id Picture ID + * @return Reference to a picture (allocates when necessary). Passing an invalid ID will abort! + */ Picture& GetPicture(int id); + + /** + * @param id Picture ID + * @return Pointer to an existing picture or nullptr if its an unused picture slot + */ Picture* GetPicturePtr(int id); /** @@ -140,7 +149,7 @@ class Game_Pictures { * @param src_id Source ID to move from * @param dst_id Destination ID to move to */ - void MovePictureId(int src_id, int dst_id); + void MovePicture(int src_id, int dst_id); /** * Swaps picture data between two IDs @@ -148,7 +157,7 @@ class Game_Pictures { * @param id1 First ID to swap with * @param id2 Second ID to swap with */ - void SwapPictureId(int id1, int id2); + void SwapPicture(int id1, int id2); private: void RequestPictureSprite(Picture& pic); diff --git a/src/game_windows.cpp b/src/game_windows.cpp index 849a2b7694..5161ed095a 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -103,6 +103,51 @@ Game_Windows::Window_User* Game_Windows::GetWindowPtr(int id) { ? &windows[id - 1] : nullptr; } +void Game_Windows::MoveWindow(int src_id, int dst_id) { + if (src_id == dst_id) { + return; + } + + // Delete the destination, then swap + if (dst_id > 0) { + auto& dst_win = GetWindow(dst_id); + dst_win.Erase(); + } + + SwapWindow(src_id, dst_id); +} + +void Game_Windows::SwapWindow(int id1, int id2) { + if (id1 == id2 || (id1 <= 0 && id2 <= 0)) { + return; + } + + auto max_id = std::max(id1, id2); + GetWindow(max_id); // Preallocate to ensure references are stable + + Window_User bad_win{0}; + + auto* src_win = &bad_win; + auto* dst_win = &bad_win; + + if (id1 > 0) { + src_win = &GetWindow(id1); + } else { + bad_win = Window_User(id1); + } + + if (id2 > 0) { + dst_win = &GetWindow(id2); + } else { + bad_win = Window_User(id2); + } + + std::swap(src_win->data.ID, dst_win->data.ID); + + // Must be last (invalidates references) + std::swap(*src_win, *dst_win); +} + bool Game_Windows::Window_User::Create(const WindowParams& params) { Erase(); diff --git a/src/game_windows.h b/src/game_windows.h index 1d4e6f9039..e29c93d19e 100644 --- a/src/game_windows.h +++ b/src/game_windows.h @@ -96,6 +96,24 @@ class Game_Windows { Window_User& GetWindow(int id); Window_User* GetWindowPtr(int id); + /** + * Moves window data to a different ID. + * Do not call this function. Always use Game_Pictures::MovePicture. + * + * @param src_id Source ID to move from + * @param dst_id Destination ID to move to + */ + void MoveWindow(int src_id, int dst_id); + + /** + * Swaps window data between two IDs. + * Do not call this function. Always use Game_Pictures::SwapPicture. + * + * @param id1 First ID to swap with + * @param id2 Second ID to swap with + */ + void SwapWindow(int id1, int id2); + private: std::vector windows; }; diff --git a/src/sprite_picture.h b/src/sprite_picture.h index 06e044a347..386053335f 100644 --- a/src/sprite_picture.h +++ b/src/sprite_picture.h @@ -44,12 +44,25 @@ class Sprite_Picture : public Sprite { /** @return Height of a single spritesheet frame or the entire width if the picture has no spritesheet */ int GetFrameHeight() const; + int GetPictureId() const; + + void SetPictureId(int pic_id); + private: int last_spritesheet_frame = -1; - const int pic_id = 0; + int pic_id = 0; const bool feature_spritesheet = false; const bool feature_priority_layers = false; const bool feature_bottom_trans = false; }; +inline int Sprite_Picture::GetPictureId() const { + return pic_id; +} + +inline void Sprite_Picture::SetPictureId(int pic_id) { + this->pic_id = pic_id; + OnPictureShow(); +} + #endif From 8b9fd9159d0687ded6deabf8cc941cee79506fce Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 3 Aug 2026 11:01:48 +0200 Subject: [PATCH 7/7] Picture Debug: Replace "Chroma" with "Transparent" as this is the name the editor uses Also Chroma is not really correct as this is about background removal and not about alpha transparency. Cell index is now 1-based to match what the editor displays --- src/window_debug_picture.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/window_debug_picture.cpp b/src/window_debug_picture.cpp index 91330657f4..1dd8cd9db2 100644 --- a/src/window_debug_picture.cpp +++ b/src/window_debug_picture.cpp @@ -172,7 +172,7 @@ void Window_DebugPictureInfo::Refresh() { // Common Flags std::vector flags = { { "Fixed", d.fixed_to_map }, - { "Chroma", d.use_transparent_color }, + { "Transparent", d.use_transparent_color }, { "Tint", d.flags.affected_by_tint }, { "Flash", d.flags.affected_by_flash }, { "Shake", d.flags.affected_by_shake }, @@ -192,7 +192,8 @@ void Window_DebugPictureInfo::Refresh() { y = DrawLine(y, "File", name_str); if (d.spritesheet_cols > 1 || d.spritesheet_rows > 1) { - std::string cell_str = fmt::format("#{} ({}x{})", d.spritesheet_frame, d.spritesheet_cols, d.spritesheet_rows); + // The editor frame number is 1-based so the user should see the expected value here + std::string cell_str = fmt::format("#{} ({}x{})", d.spritesheet_frame + 1, d.spritesheet_cols, d.spritesheet_rows); y = DrawLine(y, "Cell", cell_str); if (d.spritesheet_speed > 0) {