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.
This commit is contained in:
scawful
2025-10-13 16:51:55 -04:00
parent 965bb0946b
commit 1314d9daf9
7 changed files with 760 additions and 36 deletions

View File

@@ -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<int>(click_pos.x) / 8;
int tile_y = static_cast<int>(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<int>(click_pos.x) / 8;
int tile_y = static_cast<int>(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<int>(click_pos.x) / 16;
int tile_y = static_cast<int>(click_pos.y) / 16;
int tiles_per_row = 128 / 16; // 8 tiles per row
int tile_x = static_cast<int>(click_pos.x) / 8;
int tile_y = static_cast<int>(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<int>(click_pos.x) / 8;
int tile_y = static_cast<int>(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<int>(click_pos.x) / 8;
int tile_y = static_cast<int>(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() {

View File

@@ -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_;

View File

@@ -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<uint8_t>(128 * 128));
map_bitmap_.Create(512, 512, 8, std::vector<uint8_t>(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

View File

@@ -0,0 +1,81 @@
#ifndef YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H
#define YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H
#include <array>
#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<uint8_t, 64 * 64> lw_map_tiles_; // Light World tile indices
std::array<uint8_t, 64 * 64> 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

View File

@@ -3,7 +3,6 @@
#include <cstdint>
#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<uint8_t>(0x80000));
tiles_bg2_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
oam_bg_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(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<uint8_t> 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

View File

@@ -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<uint16_t, 0x1000> tiles_bg1_buffer_; // BG1 tilemap (32x32 tiles)

View File

@@ -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