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.
This commit is contained in:
scawful
2025-10-13 18:01:12 -04:00
parent 521df1f546
commit 26faa7e0af
4 changed files with 262 additions and 109 deletions

View File

@@ -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<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 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")) {

View File

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

View File

@@ -1,8 +1,11 @@
#include "overworld_map_screen.h"
#include "zelda3/screen/overworld_map_screen.h"
#include <fstream>
#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<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
// 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<uint8_t> 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<uint8_t> 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<uint8_t>(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<char*>(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<const char*>(tiles.data()), tiles.size());
if (!file) {
return absl::InternalError("Failed to write custom map data");
}
return absl::OkStatus();
}
} // namespace zelda3
} // namespace yaze

View File

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