From 1314d9daf9e4906f21e1e7c37be2d262b95ccc41 Mon Sep 17 00:00:00 2001 From: scawful Date: Mon, 13 Oct 2025 16:51:55 -0400 Subject: [PATCH] feat(editor): implement overworld map editing features in ScreenEditor - Added functionality for managing the overworld map, including loading, rendering, and saving map data for both Light and Dark Worlds. - Introduced new canvas components for overworld map editing, allowing users to select and paint tiles directly onto the map. - Enhanced the ScreenEditor with controls for tile flipping and palette selection, improving the user interface for overworld map management. Benefits: - Expands the capabilities of the ScreenEditor, providing users with tools to edit and manage the overworld map effectively. - Improves user experience by enabling intuitive tile editing and visual feedback during map modifications. --- src/app/editor/graphics/screen_editor.cc | 215 +++++++++++++-- src/app/editor/graphics/screen_editor.h | 16 ++ src/zelda3/screen/overworld_map_screen.cc | 322 ++++++++++++++++++++++ src/zelda3/screen/overworld_map_screen.h | 81 ++++++ src/zelda3/screen/title_screen.cc | 133 ++++++++- src/zelda3/screen/title_screen.h | 28 +- src/zelda3/zelda3_library.cmake | 1 + 7 files changed, 760 insertions(+), 36 deletions(-) create mode 100644 src/zelda3/screen/overworld_map_screen.cc create mode 100644 src/zelda3/screen/overworld_map_screen.h diff --git a/src/app/editor/graphics/screen_editor.cc b/src/app/editor/graphics/screen_editor.cc index a65dfee9..f75d5a60 100644 --- a/src/app/editor/graphics/screen_editor.cc +++ b/src/app/editor/graphics/screen_editor.cc @@ -7,17 +7,13 @@ #include "absl/strings/str_format.h" #include "app/gfx/debug/performance/performance_profiler.h" #include "util/file_util.h" -#include "app/core/window.h" #include "app/gfx/resource/arena.h" -#include "app/gfx/render/atlas_renderer.h" #include "app/gfx/core/bitmap.h" -#include "app/gfx/debug/performance/performance_profiler.h" #include "app/gfx/types/snes_tile.h" #include "app/gui/canvas/canvas.h" #include "app/gui/core/color.h" #include "app/gui/core/icons.h" #include "app/gui/core/input.h" -#include "app/gui/core/ui_helpers.h" #include "imgui/imgui.h" #include "util/hex.h" #include "util/macro.h" @@ -788,11 +784,34 @@ void ScreenEditor::DrawTitleScreenBG1Canvas() { // Handle tile painting if (current_mode_ == EditingMode::DRAW && selected_title_tile16_ >= 0) { - // TODO: Implement tile painting when user clicks on canvas - // This would modify the BG1 buffer and re-render the bitmap + 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 buffer and re-render + title_screen_.mutable_bg1_buffer()[tilemap_index] = tile_word; + status_ = title_screen_.RenderBG1Layer(); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &bg1_bitmap); + } + } + } + } } - title_bg1_canvas_.DrawGrid(16.0f); + title_bg1_canvas_.DrawGrid(8.0f); title_bg1_canvas_.DrawOverlay(); } @@ -808,11 +827,34 @@ void ScreenEditor::DrawTitleScreenBG2Canvas() { // Handle tile painting if (current_mode_ == EditingMode::DRAW && selected_title_tile16_ >= 0) { - // TODO: Implement tile painting when user clicks on canvas - // This would modify the BG2 buffer and re-render the bitmap + if (title_bg2_canvas_.DrawTileSelector(8.0f)) { + if (!title_bg2_canvas_.points().empty()) { + auto click_pos = title_bg2_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 buffer and re-render + title_screen_.mutable_bg2_buffer()[tilemap_index] = tile_word; + status_ = title_screen_.RenderBG2Layer(); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &bg2_bitmap); + } + } + } + } } - title_bg2_canvas_.DrawGrid(16.0f); + title_bg2_canvas_.DrawGrid(8.0f); title_bg2_canvas_.DrawOverlay(); } @@ -826,24 +868,33 @@ void ScreenEditor::DrawTitleScreenBlocksetSelector() { title_blockset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255); } - // Handle tile selection - if (title_blockset_canvas_.DrawTileSelector(16.0f)) { + // Handle tile selection (8x8 tiles) + if (title_blockset_canvas_.DrawTileSelector(8.0f)) { // Calculate selected tile ID from click position if (!title_blockset_canvas_.points().empty()) { auto click_pos = title_blockset_canvas_.points().front(); - int tile_x = static_cast(click_pos.x) / 16; - int tile_y = static_cast(click_pos.y) / 16; - int tiles_per_row = 128 / 16; // 8 tiles per row + int tile_x = static_cast(click_pos.x) / 8; + int tile_y = static_cast(click_pos.y) / 8; + int tiles_per_row = 128 / 8; // 16 tiles per row for 8x8 tiles selected_title_tile16_ = tile_x + (tile_y * tiles_per_row); } } - title_blockset_canvas_.DrawGrid(16.0f); + title_blockset_canvas_.DrawGrid(8.0f); title_blockset_canvas_.DrawOverlay(); - // Show selected tile preview + // Show selected tile preview and controls if (selected_title_tile16_ >= 0) { ImGui::Text("Selected Tile: %d", selected_title_tile16_); + + // Flip controls + ImGui::Checkbox("H Flip", &title_h_flip_); + ImGui::SameLine(); + ImGui::Checkbox("V Flip", &title_v_flip_); + + // Palette selector (0-7 for 3BPP graphics) + ImGui::SetNextItemWidth(100); + ImGui::SliderInt("Palette", &title_palette_, 0, 7); } } @@ -851,6 +902,136 @@ void ScreenEditor::DrawNamingScreenEditor() { } void ScreenEditor::DrawOverworldMapEditor() { + // Initialize overworld map on first draw + if (!ow_map_loaded_ && rom()->is_loaded()) { + status_ = ow_map_screen_.Create(rom()); + if (!status_.ok()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error loading overworld map: %s", + status_.message().data()); + return; + } + ow_map_loaded_ = true; + } + + if (!ow_map_loaded_) { + ImGui::Text("Overworld map not loaded. Ensure ROM is loaded."); + return; + } + + // Toolbar with mode controls + if (ImGui::Button(ICON_MD_DRAW)) { + current_mode_ = EditingMode::DRAW; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_SAVE)) { + status_ = ow_map_screen_.Save(rom()); + if (status_.ok()) { + ImGui::OpenPopup("OWSaveSuccess"); + } + } + ImGui::SameLine(); + + // World toggle + if (ImGui::Button(ow_show_dark_world_ ? "Dark World" : "Light World")) { + ow_show_dark_world_ = !ow_show_dark_world_; + // Re-render map with new world + status_ = ow_map_screen_.RenderMapLayer(ow_show_dark_world_); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &ow_map_screen_.map_bitmap()); + } + } + ImGui::SameLine(); + ImGui::Text("Selected Tile: %d", selected_ow_tile_); + + // Save success popup + if (ImGui::BeginPopup("OWSaveSuccess")) { + ImGui::Text("Overworld map saved successfully!"); + ImGui::EndPopup(); + } + + // Layout: 3-column table + if (ImGui::BeginTable("OWMapTable", 3, + ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Map Canvas"); + ImGui::TableSetupColumn("Tileset"); + ImGui::TableSetupColumn("Palette"); + ImGui::TableHeadersRow(); + + // Column 1: Map Canvas + ImGui::TableNextColumn(); + ow_map_canvas_.DrawBackground(); + ow_map_canvas_.DrawContextMenu(); + + auto& map_bitmap = ow_map_screen_.map_bitmap(); + if (map_bitmap.is_active()) { + ow_map_canvas_.DrawBitmap(map_bitmap, 0, 0, 1.0f, 255); + } + + // Handle tile painting + if (current_mode_ == EditingMode::DRAW && selected_ow_tile_ >= 0) { + if (ow_map_canvas_.DrawTileSelector(8.0f)) { + if (!ow_map_canvas_.points().empty()) { + auto click_pos = ow_map_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 < 64 && tile_y >= 0 && tile_y < 64) { + int tile_index = tile_x + (tile_y * 64); + + // Update appropriate world's tile data + if (ow_show_dark_world_) { + ow_map_screen_.mutable_dw_tiles()[tile_index] = selected_ow_tile_; + } else { + ow_map_screen_.mutable_lw_tiles()[tile_index] = selected_ow_tile_; + } + + // Re-render map + status_ = ow_map_screen_.RenderMapLayer(ow_show_dark_world_); + if (status_.ok()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &map_bitmap); + } + } + } + } + } + + ow_map_canvas_.DrawGrid(8.0f); + ow_map_canvas_.DrawOverlay(); + + // Column 2: Tileset Selector + ImGui::TableNextColumn(); + ow_tileset_canvas_.DrawBackground(); + ow_tileset_canvas_.DrawContextMenu(); + + auto& tiles8_bitmap = ow_map_screen_.tiles8_bitmap(); + if (tiles8_bitmap.is_active()) { + ow_tileset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255); + } + + // Handle tile selection + if (ow_tileset_canvas_.DrawTileSelector(8.0f)) { + if (!ow_tileset_canvas_.points().empty()) { + auto click_pos = ow_tileset_canvas_.points().front(); + int tile_x = static_cast(click_pos.x) / 8; + int tile_y = static_cast(click_pos.y) / 8; + selected_ow_tile_ = tile_x + (tile_y * 16); // 16 tiles per row + } + } + + ow_tileset_canvas_.DrawGrid(8.0f); + ow_tileset_canvas_.DrawOverlay(); + + // Column 3: Palette Display + ImGui::TableNextColumn(); + auto& palette = ow_show_dark_world_ ? ow_map_screen_.dw_palette() + : ow_map_screen_.lw_palette(); + // Use inline palette editor for full 128-color palette + gui::InlinePaletteEditor(palette, "Overworld Map Palette"); + + ImGui::EndTable(); + } } void ScreenEditor::DrawDungeonMapToolset() { diff --git a/src/app/editor/graphics/screen_editor.h b/src/app/editor/graphics/screen_editor.h index 4abb32fa..6a15b0c0 100644 --- a/src/app/editor/graphics/screen_editor.h +++ b/src/app/editor/graphics/screen_editor.h @@ -14,6 +14,7 @@ #include "zelda3/screen/dungeon_map.h" #include "zelda3/screen/inventory.h" #include "zelda3/screen/title_screen.h" +#include "zelda3/screen/overworld_map_screen.h" #include "app/gui/app/editor_layout.h" #include "imgui/imgui.h" @@ -125,10 +126,25 @@ class ScreenEditor : public Editor { zelda3::Inventory inventory_; zelda3::TitleScreen title_screen_; + zelda3::OverworldMapScreen ow_map_screen_; // Title screen state int selected_title_tile16_ = 0; bool title_screen_loaded_ = false; + bool title_h_flip_ = false; + bool title_v_flip_ = false; + int title_palette_ = 0; + + // Overworld map screen state + int selected_ow_tile_ = 0; + bool ow_map_loaded_ = false; + bool ow_show_dark_world_ = false; + + // Overworld map canvases + gui::Canvas ow_map_canvas_{"##OWMapCanvas", ImVec2(512, 512), + gui::CanvasGridSize::k8x8, 1.0f}; + gui::Canvas ow_tileset_canvas_{"##OWTilesetCanvas", ImVec2(128, 128), + gui::CanvasGridSize::k8x8, 2.0f}; Rom* rom_; absl::Status status_; diff --git a/src/zelda3/screen/overworld_map_screen.cc b/src/zelda3/screen/overworld_map_screen.cc new file mode 100644 index 00000000..53674d73 --- /dev/null +++ b/src/zelda3/screen/overworld_map_screen.cc @@ -0,0 +1,322 @@ +#include "overworld_map_screen.h" + +#include "app/gfx/resource/arena.h" +#include "app/gfx/types/snes_color.h" +#include "util/macro.h" + +namespace yaze { +namespace zelda3 { + +absl::Status OverworldMapScreen::Create(Rom* rom) { + if (!rom || !rom->is_loaded()) { + 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 + const int mode7_gfx_addr = 0x0C4000; + auto& tiles8_data = tiles8_bitmap_.mutable_data(); + + // Mode 7 graphics are stored as 8x8 tiles, 16 tiles per row + 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; + } + pos++; + } + } + } + } + + // Load palettes (128 colors each for mode 7 graphics) + // Light World palette at 0x055B27 + const int lw_pal_addr = 0x055B27; + for (int i = 0; i < 128; i++) { + ASSIGN_OR_RETURN(uint16_t snes_color, rom->ReadWord(lw_pal_addr + (i * 2))); + // Create SnesColor directly from SNES 15-bit format + lw_palette_.AddColor(gfx::SnesColor(snes_color)); + } + + // Dark World palette at 0x055C27 + const int dw_pal_addr = 0x055C27; + for (int i = 0; i < 128; i++) { + ASSIGN_OR_RETURN(uint16_t snes_color, rom->ReadWord(dw_pal_addr + (i * 2))); + // Create SnesColor directly from SNES 15-bit format + dw_palette_.AddColor(gfx::SnesColor(snes_color)); + } + + // Load map tile data + RETURN_IF_ERROR(LoadMapData(rom)); + + // Render initial map (Light World) + RETURN_IF_ERROR(RenderMapLayer(false)); + + // Apply palettes AFTER bitmaps are fully initialized + tiles8_bitmap_.SetPalette(lw_palette_); + map_bitmap_.SetPalette(lw_palette_); // Map also needs palette + + // Ensure bitmaps are marked as active + tiles8_bitmap_.set_active(true); + map_bitmap_.set_active(true); + + // Queue texture creation + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &tiles8_bitmap_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &map_bitmap_); + + return absl::OkStatus(); +} + +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 + + 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) { + if (!rSide) { + ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p1)); + lw_map_tiles_[count] = tile; + dw_map_tiles_[count] = tile; + + if (cSide >= 31) { + cSide = 0; + rSide = true; + count++; + continue; + } + } else { + ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p2)); + lw_map_tiles_[count] = tile; + dw_map_tiles_[count] = tile; + + if (cSide >= 31) { + cSide = 0; + rSide = false; + count++; + continue; + } + } + } else { + if (!rSide) { + ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p3)); + lw_map_tiles_[count] = tile; + dw_map_tiles_[count] = tile; + + if (cSide >= 31) { + cSide = 0; + rSide = true; + count++; + continue; + } + } else { + ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p4)); + lw_map_tiles_[count] = tile; + dw_map_tiles_[count] = tile; + + if (cSide >= 31) { + cSide = 0; + rSide = false; + count++; + continue; + } + } + } + + cSide++; + count++; + } + + // Load Dark World specific section (bottom-right 32x32 area) + 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; + } + } + + count++; + if (count >= 32) { + count = 0; + line++; + if (line >= 32) { + break; + } + } + } + + return absl::OkStatus(); +} + +absl::Status OverworldMapScreen::RenderMapLayer(bool use_dark_world) { + auto& map_data = map_bitmap_.mutable_data(); + const auto& tiles8_data = tiles8_bitmap_.vector(); + const auto& tile_source = use_dark_world ? dw_map_tiles_ : lw_map_tiles_; + + // Render 64x64 tiles (each 8x8 pixels) into 512x512 bitmap + for (int yy = 0; yy < 64; yy++) { + for (int xx = 0; xx < 64; xx++) { + uint8_t tile_id = tile_source[xx + (yy * 64)]; + + // Calculate tile position in tiles8_bitmap (16 tiles per row) + int tile_x = (tile_id % 16) * 8; + int tile_y = (tile_id / 16) * 8; + + // Copy 8x8 tile pixels + for (int py = 0; py < 8; py++) { + for (int px = 0; px < 8; px++) { + int src_index = (tile_x + px) + ((tile_y + py) * 128); + int dest_index = (xx * 8 + px) + ((yy * 8 + py) * 512); + + if (src_index < tiles8_data.size() && dest_index < map_data.size()) { + map_data[dest_index] = tiles8_data[src_index]; + } + } + } + } + } + + // Apply appropriate palette + map_bitmap_.SetPalette(use_dark_world ? dw_palette_ : lw_palette_); + + 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; + 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); + + if (count < 0x800) { + if (!rSide) { + RETURN_IF_ERROR(rom->WriteByte(p1, lw_map_tiles_[count])); + + if (cSide >= 31) { + cSide = 0; + rSide = true; + count++; + continue; + } + } else { + RETURN_IF_ERROR(rom->WriteByte(p2, lw_map_tiles_[count])); + + if (cSide >= 31) { + cSide = 0; + rSide = false; + count++; + continue; + } + } + } else { + if (!rSide) { + RETURN_IF_ERROR(rom->WriteByte(p3, lw_map_tiles_[count])); + + if (cSide >= 31) { + cSide = 0; + rSide = true; + count++; + continue; + } + } else { + RETURN_IF_ERROR(rom->WriteByte(p4, lw_map_tiles_[count])); + + if (cSide >= 31) { + cSide = 0; + rSide = false; + count++; + continue; + } + } + } + + cSide++; + count++; + } + + // Save Dark World specific section + 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])); + } + + count++; + if (count >= 32) { + count = 0; + line++; + if (line >= 32) { + break; + } + } + } + + 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 new file mode 100644 index 00000000..27d124e0 --- /dev/null +++ b/src/zelda3/screen/overworld_map_screen.h @@ -0,0 +1,81 @@ +#ifndef YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H +#define YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H + +#include + +#include "absl/status/status.h" +#include "app/gfx/core/bitmap.h" +#include "app/gfx/types/snes_palette.h" +#include "app/rom.h" + +namespace yaze { +namespace zelda3 { + +/** + * @brief OverworldMapScreen manages the overworld map (pause menu) graphics. + * + * The overworld map screen shows the mini-map when the player pauses. + * It consists of: + * - 64x64 tiles (8x8 pixels each) for Light World map + * - 64x64 tiles (8x8 pixels each) for Dark World map + * - Mode 7 graphics stored at 0x0C4000 + * - Tile data in interleaved format across 4 sections + */ +class OverworldMapScreen { + public: + /** + * @brief Initialize and load overworld map data from ROM + * @param rom ROM instance to read data from + */ + absl::Status Create(Rom* rom); + + /** + * @brief Save changes back to ROM + * @param rom ROM instance to write data to + */ + absl::Status Save(Rom* rom); + + // Accessors for tile data + auto& lw_tiles() { return lw_map_tiles_; } + auto& dw_tiles() { return dw_map_tiles_; } + + // Mutable accessors for editing + auto& mutable_lw_tiles() { return lw_map_tiles_; } + auto& mutable_dw_tiles() { return dw_map_tiles_; } + + // Bitmap accessors + auto& tiles8_bitmap() { return tiles8_bitmap_; } + auto& map_bitmap() { return map_bitmap_; } + + // Palette accessors + auto& lw_palette() { return lw_palette_; } + auto& dw_palette() { return dw_palette_; } + + /** + * @brief Render map tiles into bitmap + * @param use_dark_world If true, render DW tiles, otherwise LW tiles + */ + absl::Status RenderMapLayer(bool use_dark_world); + + private: + /** + * @brief Load map tile data from ROM + * Reads the interleaved tile format from 4 ROM sections + */ + absl::Status LoadMapData(Rom* rom); + + std::array lw_map_tiles_; // Light World tile indices + std::array dw_map_tiles_; // Dark World tile indices + + gfx::Bitmap tiles8_bitmap_; // 128x128 tileset (mode 7 graphics) + gfx::Bitmap map_bitmap_; // 512x512 rendered map (64 tiles × 8 pixels) + + gfx::SnesPalette lw_palette_; // Light World palette + gfx::SnesPalette dw_palette_; // Dark World palette +}; + +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H + diff --git a/src/zelda3/screen/title_screen.cc b/src/zelda3/screen/title_screen.cc index d65b9c70..3aedd9f1 100644 --- a/src/zelda3/screen/title_screen.cc +++ b/src/zelda3/screen/title_screen.cc @@ -3,7 +3,6 @@ #include #include "app/gfx/core/bitmap.h" -#include "app/gfx/render/tilemap.h" #include "app/gfx/resource/arena.h" #include "app/rom.h" #include "app/snes.h" @@ -21,14 +20,45 @@ absl::Status TitleScreen::Create(Rom* rom) { tiles_bg1_bitmap_.Create(256, 256, 8, std::vector(0x80000)); tiles_bg2_bitmap_.Create(256, 256, 8, std::vector(0x80000)); oam_bg_bitmap_.Create(256, 256, 8, std::vector(0x80000)); + + // Set metadata for title screen bitmaps + // Title screen uses 3BPP graphics with 8 palettes of 8 colors (64 total) + tiles8_bitmap_.metadata().source_bpp = 3; + tiles8_bitmap_.metadata().palette_format = 0; // Full 64-color palette + tiles8_bitmap_.metadata().source_type = "graphics_sheet"; + tiles8_bitmap_.metadata().palette_colors = 64; + + tiles_bg1_bitmap_.metadata().source_bpp = 3; + tiles_bg1_bitmap_.metadata().palette_format = 0; // Uses full palette with sub-palette indexing + tiles_bg1_bitmap_.metadata().source_type = "screen_buffer"; + tiles_bg1_bitmap_.metadata().palette_colors = 64; + + tiles_bg2_bitmap_.metadata().source_bpp = 3; + tiles_bg2_bitmap_.metadata().palette_format = 0; + tiles_bg2_bitmap_.metadata().source_type = "screen_buffer"; + tiles_bg2_bitmap_.metadata().palette_colors = 64; + + oam_bg_bitmap_.metadata().source_bpp = 3; + oam_bg_bitmap_.metadata().palette_format = 0; + oam_bg_bitmap_.metadata().source_type = "screen_buffer"; + oam_bg_bitmap_.metadata().palette_colors = 64; // Initialize tilemap buffers tiles_bg1_buffer_.fill(0x492); // Default empty tile tiles_bg2_buffer_.fill(0x492); - // Load palette (title screen uses sprite graphics) + // Load palette (title screen uses 3BPP graphics with 8 palettes of 8 colors each) + // Build a full 64-color palette from sprite palettes auto sprite_pal_group = rom->palette_group().sprites_aux1; - palette_ = sprite_pal_group[0]; + + // Title screen needs 8 palettes (64 colors total for 3BPP mode) + // Each palette in sprites_aux1 has 8 colors (7 actual + 1 transparent) + for (int pal = 0; pal < 8 && pal < sprite_pal_group.size(); pal++) { + auto sub_palette = sprite_pal_group[pal]; + for (int col = 0; col < sub_palette.size(); col++) { + palette_.AddColor(sub_palette[col]); + } + } // Build tile16 blockset from graphics RETURN_IF_ERROR(BuildTileset(rom)); @@ -179,10 +209,15 @@ absl::Status TitleScreen::LoadTitleScreen(Rom* rom) { RETURN_IF_ERROR(RenderBG1Layer()); RETURN_IF_ERROR(RenderBG2Layer()); - // Apply palettes to layer bitmaps + // Apply palettes to layer bitmaps AFTER rendering tiles_bg1_bitmap_.SetPalette(palette_); tiles_bg2_bitmap_.SetPalette(palette_); oam_bg_bitmap_.SetPalette(palette_); + + // Ensure bitmaps are marked as active + tiles_bg1_bitmap_.set_active(true); + tiles_bg2_bitmap_.set_active(true); + oam_bg_bitmap_.set_active(true); // Queue texture creation for all layer bitmaps gfx::Arena::Get().QueueTextureCommand( @@ -302,9 +337,93 @@ absl::Status TitleScreen::RenderBG2Layer() { } absl::Status TitleScreen::Save(Rom* rom) { - // TODO: Implement saving title screen tilemap back to ROM - // This would involve compressing the tilemap data and writing it back - return absl::UnimplementedError("Title screen saving not yet implemented"); + if (!rom || !rom->is_loaded()) { + return absl::InvalidArgumentError("ROM is not loaded"); + } + + // Title screen uses compressed tilemap format + // We'll write the data back in the same compressed format + std::vector compressed_data; + + // Helper to write word (little endian) + auto WriteWord = [&compressed_data](uint16_t value) { + compressed_data.push_back(value & 0xFF); + compressed_data.push_back((value >> 8) & 0xFF); + }; + + // Compress BG2 layer (dest < 0x1000) + uint16_t bg2_dest = 0x0000; + for (int i = 0; i < 1024; i++) { + if (i == 0 || tiles_bg2_buffer_[i] != tiles_bg2_buffer_[i - 1]) { + // Start a new run + WriteWord(bg2_dest + i); // Destination address + + // Count consecutive identical tiles + int run_length = 1; + uint16_t tile_value = tiles_bg2_buffer_[i]; + while (i + run_length < 1024 && tiles_bg2_buffer_[i + run_length] == tile_value) { + run_length++; + } + + // Write length/flags (bit 14 = fixsource if run > 1) + uint16_t length_flags = (run_length - 1) * 2; // Length in bytes + if (run_length > 1) { + length_flags |= 0x4000; // fixsource flag + } + WriteWord(length_flags); + + // Write tile data + WriteWord(tile_value); + + i += run_length - 1; // Skip already processed tiles + } + } + + // Compress BG1 layer (dest >= 0x1000) + uint16_t bg1_dest = 0x1000; + for (int i = 0; i < 1024; i++) { + if (i == 0 || tiles_bg1_buffer_[i] != tiles_bg1_buffer_[i - 1]) { + // Start a new run + WriteWord(bg1_dest + i); // Destination address + + // Count consecutive identical tiles + int run_length = 1; + uint16_t tile_value = tiles_bg1_buffer_[i]; + while (i + run_length < 1024 && tiles_bg1_buffer_[i + run_length] == tile_value) { + run_length++; + } + + // Write length/flags (bit 14 = fixsource if run > 1) + uint16_t length_flags = (run_length - 1) * 2; // Length in bytes + if (run_length > 1) { + length_flags |= 0x4000; // fixsource flag + } + WriteWord(length_flags); + + // Write tile data + WriteWord(tile_value); + + i += run_length - 1; // Skip already processed tiles + } + } + + // Write terminator byte + compressed_data.push_back(0x80); + + // Calculate ROM address to write to + ASSIGN_OR_RETURN(uint8_t byte0, rom->ReadByte(0x137A + 3)); + ASSIGN_OR_RETURN(uint8_t byte1, rom->ReadByte(0x1383 + 3)); + ASSIGN_OR_RETURN(uint8_t byte2, rom->ReadByte(0x138C + 3)); + + int pos = (byte2 << 16) + (byte1 << 8) + byte0; + int write_pos = SnesToPc(pos); + + // Write compressed data to ROM + for (size_t i = 0; i < compressed_data.size(); i++) { + RETURN_IF_ERROR(rom->WriteByte(write_pos + i, compressed_data[i])); + } + + return absl::OkStatus(); } } // namespace zelda3 diff --git a/src/zelda3/screen/title_screen.h b/src/zelda3/screen/title_screen.h index 9b5da7a6..016f87c1 100644 --- a/src/zelda3/screen/title_screen.h +++ b/src/zelda3/screen/title_screen.h @@ -33,6 +33,10 @@ class TitleScreen { auto& bg1_buffer() { return tiles_bg1_buffer_; } auto& bg2_buffer() { return tiles_bg2_buffer_; } auto& oam_buffer() { return oam_data_; } + + // Mutable accessors for editing + auto& mutable_bg1_buffer() { return tiles_bg1_buffer_; } + auto& mutable_bg2_buffer() { return tiles_bg2_buffer_; } // Accessors for bitmaps auto& bg1_bitmap() { return tiles_bg1_bitmap_; } @@ -47,6 +51,18 @@ class TitleScreen { // Save changes back to ROM absl::Status Save(Rom* rom); + /** + * @brief Render BG1 tilemap into bitmap pixels + * Converts tile IDs from tiles_bg1_buffer_ into pixel data + */ + absl::Status RenderBG1Layer(); + + /** + * @brief Render BG2 tilemap into bitmap pixels + * Converts tile IDs from tiles_bg2_buffer_ into pixel data + */ + absl::Status RenderBG2Layer(); + private: /** * @brief Build the tile16 blockset from ROM graphics @@ -60,18 +76,6 @@ class TitleScreen { */ absl::Status LoadTitleScreen(Rom* rom); - /** - * @brief Render BG1 tilemap into bitmap pixels - * Converts tile IDs from tiles_bg1_buffer_ into pixel data - */ - absl::Status RenderBG1Layer(); - - /** - * @brief Render BG2 tilemap into bitmap pixels - * Converts tile IDs from tiles_bg2_buffer_ into pixel data - */ - absl::Status RenderBG2Layer(); - int pal_selected_ = 2; std::array tiles_bg1_buffer_; // BG1 tilemap (32x32 tiles) diff --git a/src/zelda3/zelda3_library.cmake b/src/zelda3/zelda3_library.cmake index 7db307cc..fa533db3 100644 --- a/src/zelda3/zelda3_library.cmake +++ b/src/zelda3/zelda3_library.cmake @@ -14,6 +14,7 @@ set( zelda3/screen/dungeon_map.cc zelda3/screen/inventory.cc zelda3/screen/title_screen.cc + zelda3/screen/overworld_map_screen.cc zelda3/sprite/sprite.cc zelda3/sprite/sprite_builder.cc zelda3/zelda3_labels.cc