From 26faa7e0af6f5fd4885f9e4146f9eea561f30a56 Mon Sep 17 00:00:00 2001 From: scawful Date: Mon, 13 Oct 2025 18:01:12 -0400 Subject: [PATCH] feat(editor): enhance ScreenEditor with layer visibility controls and custom map loading/saving - Added checkboxes for toggling visibility of title screen background layers (BG1 and BG2). - Implemented a new method for rendering the composite view of the title screen. - Introduced functionality for loading and saving custom maps from external binary files, enhancing user flexibility in map management. Benefits: - Improves user experience by allowing dynamic control over layer visibility during title screen editing. - Expands the capabilities of the ScreenEditor with custom map handling, facilitating easier map modifications and sharing. --- src/app/editor/graphics/screen_editor.cc | 119 ++++++++++- src/app/editor/graphics/screen_editor.h | 3 + src/zelda3/screen/overworld_map_screen.cc | 236 +++++++++++++--------- src/zelda3/screen/overworld_map_screen.h | 13 ++ 4 files changed, 262 insertions(+), 109 deletions(-) diff --git a/src/app/editor/graphics/screen_editor.cc b/src/app/editor/graphics/screen_editor.cc index f75d5a60..1934cc57 100644 --- a/src/app/editor/graphics/screen_editor.cc +++ b/src/app/editor/graphics/screen_editor.cc @@ -748,23 +748,35 @@ void ScreenEditor::DrawTitleScreenEditor() { ImGui::EndPopup(); } - // Layout: 3-column table for layers - if (ImGui::BeginTable("TitleScreenTable", 3, + // Layer visibility controls + bool prev_bg1 = show_title_bg1_; + bool prev_bg2 = show_title_bg2_; + ImGui::Checkbox("Show BG1", &show_title_bg1_); + ImGui::SameLine(); + ImGui::Checkbox("Show BG2", &show_title_bg2_); + + // Re-render composite if visibility changed + if (prev_bg1 != show_title_bg1_ || prev_bg2 != show_title_bg2_) { + status_ = title_screen_.RenderCompositeLayer(show_title_bg1_, show_title_bg2_); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &title_screen_.composite_bitmap()); + } + } + + // Layout: 2-column table (composite view + tile selector) + if (ImGui::BeginTable("TitleScreenTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) { - ImGui::TableSetupColumn("BG1 Layer"); - ImGui::TableSetupColumn("BG2 Layer"); + ImGui::TableSetupColumn("Title Screen (Composite)"); ImGui::TableSetupColumn("Tile Selector"); ImGui::TableHeadersRow(); - // Column 1: BG1 Canvas + // Column 1: Composite Canvas (BG1+BG2 stacked) ImGui::TableNextColumn(); - DrawTitleScreenBG1Canvas(); + DrawTitleScreenCompositeCanvas(); - // Column 2: BG2 Canvas - ImGui::TableNextColumn(); - DrawTitleScreenBG2Canvas(); - - // Column 3: Blockset Selector + // Column 2: Blockset Selector ImGui::TableNextColumn(); DrawTitleScreenBlocksetSelector(); @@ -772,6 +784,58 @@ void ScreenEditor::DrawTitleScreenEditor() { } } +void ScreenEditor::DrawTitleScreenCompositeCanvas() { + title_bg1_canvas_.DrawBackground(); + title_bg1_canvas_.DrawContextMenu(); + + // Draw composite tilemap (BG1+BG2 stacked with transparency) + auto& composite_bitmap = title_screen_.composite_bitmap(); + if (composite_bitmap.is_active()) { + title_bg1_canvas_.DrawBitmap(composite_bitmap, 0, 0, 2.0f, 255); + } + + // Handle tile painting - always paint to BG1 layer + if (current_mode_ == EditingMode::DRAW && selected_title_tile16_ >= 0) { + if (title_bg1_canvas_.DrawTileSelector(8.0f)) { + if (!title_bg1_canvas_.points().empty()) { + auto click_pos = title_bg1_canvas_.points().front(); + int tile_x = static_cast(click_pos.x) / 8; + int tile_y = static_cast(click_pos.y) / 8; + + if (tile_x >= 0 && tile_x < 32 && tile_y >= 0 && tile_y < 32) { + int tilemap_index = tile_y * 32 + tile_x; + + // Create tile word: tile_id | (palette << 10) | h_flip | v_flip + uint16_t tile_word = selected_title_tile16_ & 0x3FF; + tile_word |= (title_palette_ & 0x07) << 10; + if (title_h_flip_) tile_word |= 0x4000; + if (title_v_flip_) tile_word |= 0x8000; + + // Update BG1 buffer and re-render both layers and composite + title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word; + status_ = title_screen_.RenderBG1Layer(); + if (status_.ok()) { + // Update BG1 texture + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &title_screen_.bg1_bitmap()); + + // Re-render and update composite + status_ = title_screen_.RenderCompositeLayer(show_title_bg1_, show_title_bg2_); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &composite_bitmap); + } + } + } + } + } + } + + title_bg1_canvas_.DrawGrid(); + title_bg1_canvas_.DrawOverlay(); +} + void ScreenEditor::DrawTitleScreenBG1Canvas() { title_bg1_canvas_.DrawBackground(); title_bg1_canvas_.DrawContextMenu(); @@ -941,8 +1005,41 @@ void ScreenEditor::DrawOverworldMapEditor() { gfx::Arena::TextureCommandType::UPDATE, &ow_map_screen_.map_bitmap()); } } + ImGui::SameLine(); + + // Custom map load/save buttons + if (ImGui::Button("Load Custom Map...")) { + std::string path = util::FileDialogWrapper::ShowOpenFileDialog(); + if (!path.empty()) { + status_ = ow_map_screen_.LoadCustomMap(path); + if (!status_.ok()) { + ImGui::OpenPopup("CustomMapLoadError"); + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Save Custom Map...")) { + std::string path = util::FileDialogWrapper::ShowSaveFileDialog(); + if (!path.empty()) { + status_ = ow_map_screen_.SaveCustomMap(path, ow_show_dark_world_); + if (status_.ok()) { + ImGui::OpenPopup("CustomMapSaveSuccess"); + } + } + } + ImGui::SameLine(); ImGui::Text("Selected Tile: %d", selected_ow_tile_); + + // Custom map error/success popups + if (ImGui::BeginPopup("CustomMapLoadError")) { + ImGui::Text("Error loading custom map: %s", status_.message().data()); + ImGui::EndPopup(); + } + if (ImGui::BeginPopup("CustomMapSaveSuccess")) { + ImGui::Text("Custom map saved successfully!"); + ImGui::EndPopup(); + } // Save success popup if (ImGui::BeginPopup("OWSaveSuccess")) { diff --git a/src/app/editor/graphics/screen_editor.h b/src/app/editor/graphics/screen_editor.h index 6a15b0c0..43957d29 100644 --- a/src/app/editor/graphics/screen_editor.h +++ b/src/app/editor/graphics/screen_editor.h @@ -67,6 +67,7 @@ class ScreenEditor : public Editor { void DrawInventoryToolset(); // Title screen layer editing + void DrawTitleScreenCompositeCanvas(); void DrawTitleScreenBG1Canvas(); void DrawTitleScreenBG2Canvas(); void DrawTitleScreenBlocksetSelector(); @@ -134,6 +135,8 @@ class ScreenEditor : public Editor { bool title_h_flip_ = false; bool title_v_flip_ = false; int title_palette_ = 0; + bool show_title_bg1_ = true; + bool show_title_bg2_ = true; // Overworld map screen state int selected_ow_tile_ = 0; diff --git a/src/zelda3/screen/overworld_map_screen.cc b/src/zelda3/screen/overworld_map_screen.cc index 8127ddc5..04c0c4c5 100644 --- a/src/zelda3/screen/overworld_map_screen.cc +++ b/src/zelda3/screen/overworld_map_screen.cc @@ -1,8 +1,11 @@ -#include "overworld_map_screen.h" +#include "zelda3/screen/overworld_map_screen.h" + +#include #include "app/gfx/resource/arena.h" #include "app/gfx/types/snes_color.h" -#include "util/macro.h" +#include "app/rom.h" +#include "app/snes.h" namespace yaze { namespace zelda3 { @@ -12,44 +15,47 @@ absl::Status OverworldMapScreen::Create(Rom* rom) { return absl::InvalidArgumentError("ROM is not loaded"); } - // Initialize bitmaps - tiles8_bitmap_.Create(128, 128, 8, std::vector(128 * 128)); - map_bitmap_.Create(512, 512, 8, std::vector(512 * 512)); - // Set metadata for overworld map bitmaps // Mode 7 graphics use full 128-color palettes - tiles8_bitmap_.metadata().source_bpp = 8; - tiles8_bitmap_.metadata().palette_format = 0; // Full palette - tiles8_bitmap_.metadata().source_type = "mode7"; - tiles8_bitmap_.metadata().palette_colors = 128; - - map_bitmap_.metadata().source_bpp = 8; - map_bitmap_.metadata().palette_format = 0; // Full palette - map_bitmap_.metadata().source_type = "mode7"; - map_bitmap_.metadata().palette_colors = 128; - // Load mode 7 graphics from 0x0C4000 + // Load Mode 7 graphics (256 tiles, 8x8 pixels each, 8BPP) const int mode7_gfx_addr = 0x0C4000; - auto& tiles8_data = tiles8_bitmap_.mutable_data(); + std::vector mode7_gfx_raw(0x4000); // Raw tileset data from ROM - // Mode 7 graphics are stored as 8x8 tiles, 16 tiles per row + for (int i = 0; i < 0x4000; i++) { + ASSIGN_OR_RETURN(mode7_gfx_raw[i], rom->ReadByte(mode7_gfx_addr + i)); + } + + // Mode 7 tiles are stored in tiled format (each tile's rows are consecutive) + // but we need linear bitmap format (all tiles' first rows, then all second rows) + // Convert from tiled to linear bitmap layout + std::vector mode7_gfx(0x4000); int pos = 0; - for (int sy = 0; sy < 16; sy++) { - for (int sx = 0; sx < 16; sx++) { - for (int y = 0; y < 8; y++) { - for (int x = 0; x < 8; x++) { - int dest_index = x + (sx * 8) + (y * 128) + (sy * 1024); - if (dest_index < tiles8_data.size() && mode7_gfx_addr + pos < rom->size()) { - ASSIGN_OR_RETURN(uint8_t pixel, rom->ReadByte(mode7_gfx_addr + pos)); - tiles8_data[dest_index] = pixel; - } + for (int sy = 0; sy < 16 * 1024; sy += 1024) { // 16 rows of tiles + for (int sx = 0; sx < 16 * 8; sx += 8) { // 16 columns of tiles + for (int y = 0; y < 8 * 128; y += 128) { // 8 pixel rows within tile + for (int x = 0; x < 8; x++) { // 8 pixels per row + mode7_gfx[x + sx + y + sy] = mode7_gfx_raw[pos]; pos++; } } } } - // Load palettes (128 colors each for mode 7 graphics) + // Create tiles8 bitmap: 128×128 pixels (16×16 tiles = 256 tiles) + tiles8_bitmap_.Create(128, 128, 8, mode7_gfx); + tiles8_bitmap_.metadata().source_bpp = 8; + tiles8_bitmap_.metadata().palette_format = 0; + tiles8_bitmap_.metadata().source_type = "mode7_tileset"; + tiles8_bitmap_.metadata().palette_colors = 128; + + // Create map bitmap (512x512 for 64x64 tiles at 8x8 each) + map_bitmap_.Create(512, 512, 8, std::vector(512 * 512)); + map_bitmap_.metadata().source_bpp = 8; + map_bitmap_.metadata().palette_format = 0; + map_bitmap_.metadata().source_type = "mode7_map"; + map_bitmap_.metadata().palette_colors = 128; + // Light World palette at 0x055B27 const int lw_pal_addr = 0x055B27; for (int i = 0; i < 128; i++) { @@ -90,31 +96,31 @@ absl::Status OverworldMapScreen::Create(Rom* rom) { } absl::Status OverworldMapScreen::LoadMapData(Rom* rom) { - // Map data is stored in 4 sections with interleaved left/right format - // Based on ZScream's implementation in ScreenEditor.cs lines 221-322 + // Map data is stored in interleaved format across 4 sections + 1 DW section + // Based on ZScream's Constants.IDKZarby = 0x054727 + // The data alternates between left (32 columns) and right (32 columns) + // for the first 2048 tiles, then continues for bottom half - const int p1_addr = 0x0564F8; // First section (left) - const int p2_addr = 0x05634C; // First section (right) - const int p3_addr = 0x056BF8; // Second section (left) - const int p4_addr = 0x056A4C; // Second section (right) - const int p5_addr = 0x057404; // Dark World additional section - - int count = 0; - int cSide = 0; - bool rSide = false; - - // Load Light World and Dark World base data - while (count < 0x1000) { - int p1 = p1_addr + (count - (rSide ? 1 : 0)); - int p2 = p2_addr + (count - (rSide ? 1 : 0)); - int p3 = p3_addr + (count - (rSide ? 1 : 0) - 0x800); - int p4 = p4_addr + (count - (rSide ? 1 : 0) - 0x800); - - if (count < 0x800) { + const int base_addr = 0x054727; // IDKZarby constant from ZScream + int p1 = base_addr + 0x0000; // Top-left quadrant data + int p2 = base_addr + 0x0400; // Top-right quadrant data + int p3 = base_addr + 0x0800; // Bottom-left quadrant data + int p4 = base_addr + 0x0C00; // Bottom-right quadrant data + int p5 = base_addr + 0x1000; // Dark World additional section + + bool rSide = false; // false = left side, true = right side + int cSide = 0; // Column counter within side (0-31) + int count = 0; // Output tile index + + // Load 64x64 map with interleaved left/right format + while (count < 64 * 64) { + if (count < 0x800) { // Top half (first 2048 tiles) if (!rSide) { + // Read from left side (p1) ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p1)); lw_map_tiles_[count] = tile; dw_map_tiles_[count] = tile; + p1++; if (cSide >= 31) { cSide = 0; @@ -123,9 +129,11 @@ absl::Status OverworldMapScreen::LoadMapData(Rom* rom) { continue; } } else { + // Read from right side (p2) ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p2)); lw_map_tiles_[count] = tile; dw_map_tiles_[count] = tile; + p2++; if (cSide >= 31) { cSide = 0; @@ -134,11 +142,13 @@ absl::Status OverworldMapScreen::LoadMapData(Rom* rom) { continue; } } - } else { + } else { // Bottom half (remaining 2048 tiles) if (!rSide) { + // Read from left side (p3) ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p3)); lw_map_tiles_[count] = tile; dw_map_tiles_[count] = tile; + p3++; if (cSide >= 31) { cSide = 0; @@ -147,9 +157,11 @@ absl::Status OverworldMapScreen::LoadMapData(Rom* rom) { continue; } } else { + // Read from right side (p4) ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p4)); lw_map_tiles_[count] = tile; dw_map_tiles_[count] = tile; + p4++; if (cSide >= 31) { cSide = 0; @@ -159,24 +171,18 @@ absl::Status OverworldMapScreen::LoadMapData(Rom* rom) { } } } - + cSide++; count++; } - - // Load Dark World specific section (bottom-right 32x32 area) + + // Load Dark World specific data (bottom-right 32x32 section) count = 0; int line = 0; while (true) { - int addr = p5_addr + count + (line * 32); - if (addr < rom->size()) { - ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(addr)); - int dest_index = 1040 + count + (line * 64); - if (dest_index < dw_map_tiles_.size()) { - dw_map_tiles_[dest_index] = tile; - } - } - + ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p5)); + dw_map_tiles_[1040 + count + (line * 64)] = tile; + p5++; count++; if (count >= 32) { count = 0; @@ -217,42 +223,32 @@ absl::Status OverworldMapScreen::RenderMapLayer(bool use_dark_world) { } } } - - // Update surface with rendered pixel data - map_bitmap_.UpdateSurfacePixels(); - // Apply appropriate palette - map_bitmap_.SetPalette(use_dark_world ? dw_palette_ : lw_palette_); + // Copy pixel data to SDL surface + map_bitmap_.UpdateSurfacePixels(); return absl::OkStatus(); } absl::Status OverworldMapScreen::Save(Rom* rom) { - if (!rom || !rom->is_loaded()) { - return absl::InvalidArgumentError("ROM is not loaded"); - } - - // Save data back in the same interleaved format - const int p1_addr = 0x0564F8; - const int p2_addr = 0x05634C; - const int p3_addr = 0x056BF8; - const int p4_addr = 0x056A4C; - const int p5_addr = 0x057404; - - int count = 0; - int cSide = 0; + // Write data back in the same interleaved format + const int base_addr = 0x054727; + int p1 = base_addr + 0x0000; + int p2 = base_addr + 0x0400; + int p3 = base_addr + 0x0800; + int p4 = base_addr + 0x0C00; + int p5 = base_addr + 0x1000; + bool rSide = false; - - // Save Light World data (same pattern as loading) - while (count < 0x1000) { - int p1 = p1_addr + (count - (rSide ? 1 : 0)); - int p2 = p2_addr + (count - (rSide ? 1 : 0)); - int p3 = p3_addr + (count - (rSide ? 1 : 0) - 0x800); - int p4 = p4_addr + (count - (rSide ? 1 : 0) - 0x800); - + int cSide = 0; + int count = 0; + + // Write 64x64 map with interleaved left/right format + while (count < 64 * 64) { if (count < 0x800) { if (!rSide) { RETURN_IF_ERROR(rom->WriteByte(p1, lw_map_tiles_[count])); + p1++; if (cSide >= 31) { cSide = 0; @@ -262,6 +258,7 @@ absl::Status OverworldMapScreen::Save(Rom* rom) { } } else { RETURN_IF_ERROR(rom->WriteByte(p2, lw_map_tiles_[count])); + p2++; if (cSide >= 31) { cSide = 0; @@ -273,6 +270,7 @@ absl::Status OverworldMapScreen::Save(Rom* rom) { } else { if (!rSide) { RETURN_IF_ERROR(rom->WriteByte(p3, lw_map_tiles_[count])); + p3++; if (cSide >= 31) { cSide = 0; @@ -282,6 +280,7 @@ absl::Status OverworldMapScreen::Save(Rom* rom) { } } else { RETURN_IF_ERROR(rom->WriteByte(p4, lw_map_tiles_[count])); + p4++; if (cSide >= 31) { cSide = 0; @@ -291,22 +290,17 @@ absl::Status OverworldMapScreen::Save(Rom* rom) { } } } - + cSide++; count++; } - - // Save Dark World specific section + + // Write Dark World specific data count = 0; int line = 0; while (true) { - int addr = p5_addr + count + (line * 32); - int src_index = 1040 + count + (line * 64); - - if (src_index < dw_map_tiles_.size()) { - RETURN_IF_ERROR(rom->WriteByte(addr, dw_map_tiles_[src_index])); - } - + RETURN_IF_ERROR(rom->WriteByte(p5, dw_map_tiles_[1040 + count + (line * 64)])); + p5++; count++; if (count >= 32) { count = 0; @@ -320,6 +314,52 @@ absl::Status OverworldMapScreen::Save(Rom* rom) { return absl::OkStatus(); } +absl::Status OverworldMapScreen::LoadCustomMap(const std::string& file_path) { + // Load custom map from external binary file + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return absl::NotFoundError("Could not open custom map file: " + file_path); + } + + std::streamsize size = file.tellg(); + if (size != 4096) { + return absl::InvalidArgumentError( + "Custom map file must be exactly 4096 bytes (64×64 tiles)"); + } + + file.seekg(0, std::ios::beg); + + // Read into Light World map buffer (could add option for Dark World later) + file.read(reinterpret_cast(lw_map_tiles_.data()), 4096); + + if (!file) { + return absl::InternalError("Failed to read custom map data"); + } + + // Re-render with new data + RETURN_IF_ERROR(RenderMapLayer(false)); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &map_bitmap_); + + return absl::OkStatus(); +} + +absl::Status OverworldMapScreen::SaveCustomMap(const std::string& file_path, + bool use_dark_world) { + std::ofstream file(file_path, std::ios::binary); + if (!file.is_open()) { + return absl::InternalError("Could not create custom map file: " + file_path); + } + + const auto& tiles = use_dark_world ? dw_map_tiles_ : lw_map_tiles_; + file.write(reinterpret_cast(tiles.data()), tiles.size()); + + if (!file) { + return absl::InternalError("Failed to write custom map data"); + } + + return absl::OkStatus(); +} + } // namespace zelda3 } // namespace yaze - diff --git a/src/zelda3/screen/overworld_map_screen.h b/src/zelda3/screen/overworld_map_screen.h index 27d124e0..c6e1ca79 100644 --- a/src/zelda3/screen/overworld_map_screen.h +++ b/src/zelda3/screen/overworld_map_screen.h @@ -57,6 +57,19 @@ class OverworldMapScreen { */ absl::Status RenderMapLayer(bool use_dark_world); + /** + * @brief Load custom map from external binary file + * @param file_path Path to .bin file containing 64×64 tile indices + */ + absl::Status LoadCustomMap(const std::string& file_path); + + /** + * @brief Save map data to external binary file + * @param file_path Path to output .bin file + * @param use_dark_world If true, save DW tiles, otherwise LW tiles + */ + absl::Status SaveCustomMap(const std::string& file_path, bool use_dark_world); + private: /** * @brief Load map tile data from ROM