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:
@@ -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() {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
322
src/zelda3/screen/overworld_map_screen.cc
Normal file
322
src/zelda3/screen/overworld_map_screen.cc
Normal 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
|
||||
|
||||
81
src/zelda3/screen/overworld_map_screen.h
Normal file
81
src/zelda3/screen/overworld_map_screen.h
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user