feat(editor): enhance screen editor with title screen and inventory item icon features

- Implemented title screen editing capabilities, including loading and rendering of title screen layers.
- Added inventory item icon management, allowing for the display and selection of item icons within the inventory menu.
- Updated the inventory creation process to ensure proper ROM loading and error handling.
- Introduced new canvas components for title screen and inventory item icons, improving the user interface for editing.

Benefits:
- Enhances the functionality of the screen editor, providing users with tools to edit title screens and manage inventory icons effectively.
- Improves user experience by ensuring robust error handling and visual feedback during inventory management.
This commit is contained in:
scawful
2025-10-13 14:28:17 -04:00
parent a582210fa8
commit 27aba01864
7 changed files with 725 additions and 201 deletions

View File

@@ -58,11 +58,19 @@ absl::Status ScreenEditor::Load() {
zelda3::LoadDungeonMaps(*rom(), dungeon_map_labels_));
RETURN_IF_ERROR(zelda3::LoadDungeonMapTile16(
tile16_blockset_, *rom(), rom()->graphics_buffer(), false));
// TODO: Load roomset gfx based on dungeon ID
// Load graphics sheets and apply dungeon palette
sheets_.try_emplace(0, gfx::Arena::Get().gfx_sheets()[212]);
sheets_.try_emplace(1, gfx::Arena::Get().gfx_sheets()[213]);
sheets_.try_emplace(2, gfx::Arena::Get().gfx_sheets()[214]);
sheets_.try_emplace(3, gfx::Arena::Get().gfx_sheets()[215]);
// Apply dungeon palette to all sheets
for (int i = 0; i < 4; i++) {
sheets_[i].SetPalette(*rom()->mutable_dungeon_palette(3));
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &sheets_[i]);
}
/**
int current_tile8 = 0;
int tile_data_offset = 0;
@@ -103,24 +111,30 @@ absl::Status ScreenEditor::Update() {
// Get visibility flags from card manager and pass to Begin()
if (dungeon_maps_card.Begin(card_manager.GetVisibilityFlag("screen.dungeon_maps"))) {
DrawDungeonMapsEditor();
dungeon_maps_card.End();
}
dungeon_maps_card.End();
if (inventory_menu_card.Begin(card_manager.GetVisibilityFlag("screen.inventory_menu"))) {
DrawInventoryMenuEditor();
inventory_menu_card.End();
}
inventory_menu_card.End();
if (overworld_map_card.Begin(card_manager.GetVisibilityFlag("screen.overworld_map"))) {
DrawOverworldMapEditor();
overworld_map_card.End();
}
overworld_map_card.End();
if (title_screen_card.Begin(card_manager.GetVisibilityFlag("screen.title_screen"))) {
DrawTitleScreenEditor();
title_screen_card.End();
}
title_screen_card.End();
if (naming_screen_card.Begin(card_manager.GetVisibilityFlag("screen.naming_screen"))) {
DrawNamingScreenEditor();
naming_screen_card.End();
DrawNamingScreenEditor();
}
naming_screen_card.End();
return status_;
}
@@ -133,16 +147,23 @@ void ScreenEditor::DrawToolset() {
void ScreenEditor::DrawInventoryMenuEditor() {
static bool create = false;
if (!create && rom()->is_loaded()) {
status_ = inventory_.Create();
palette_ = inventory_.palette();
create = true;
status_ = inventory_.Create(rom());
if (status_.ok()) {
palette_ = inventory_.palette();
create = true;
} else {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error loading inventory: %s",
status_.message().data());
return;
}
}
DrawInventoryToolset();
if (ImGui::BeginTable("InventoryScreen", 3, ImGuiTableFlags_Resizable)) {
if (ImGui::BeginTable("InventoryScreen", 4, ImGuiTableFlags_Resizable)) {
ImGui::TableSetupColumn("Canvas");
ImGui::TableSetupColumn("Tilesheet");
ImGui::TableSetupColumn("Item Icons");
ImGui::TableSetupColumn("Palette");
ImGui::TableHeadersRow();
@@ -160,12 +181,22 @@ void ScreenEditor::DrawInventoryMenuEditor() {
tilesheet_canvas_.DrawGrid(16.0f);
tilesheet_canvas_.DrawOverlay();
ImGui::TableNextColumn();
DrawInventoryItemIcons();
ImGui::TableNextColumn();
gui::DisplayPalette(palette_, create);
ImGui::EndTable();
}
ImGui::Separator();
// TODO(scawful): Future Oracle of Secrets menu editor integration
// - Full inventory screen layout editor
// - Item slot assignment and positioning
// - Heart container and magic meter editor
// - Equipment display customization
// - A/B button equipment quick-select editor
}
void ScreenEditor::DrawInventoryToolset() {
@@ -213,6 +244,59 @@ void ScreenEditor::DrawInventoryToolset() {
}
}
void ScreenEditor::DrawInventoryItemIcons() {
if (ImGui::BeginChild("##ItemIconsList", ImVec2(0, 0), true,
ImGuiWindowFlags_HorizontalScrollbar)) {
ImGui::Text("Item Icons (2x2 tiles each)");
ImGui::Separator();
auto& icons = inventory_.item_icons();
if (icons.empty()) {
ImGui::TextWrapped("No item icons loaded. Icons will be loaded when the "
"inventory is initialized.");
ImGui::EndChild();
return;
}
// Display icons in a table format
if (ImGui::BeginTable("##IconsTable", 2,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Icon Name");
ImGui::TableSetupColumn("Tile Data");
ImGui::TableHeadersRow();
for (size_t i = 0; i < icons.size(); i++) {
const auto& icon = icons[i];
ImGui::TableNextRow();
ImGui::TableNextColumn();
// Display icon name with selectable row
if (ImGui::Selectable(icon.name.c_str(), false,
ImGuiSelectableFlags_SpanAllColumns)) {
// TODO: Select this icon for editing
}
ImGui::TableNextColumn();
// Display tile word data in hex format
ImGui::Text("TL:%04X TR:%04X", icon.tile_tl, icon.tile_tr);
ImGui::SameLine();
ImGui::Text("BL:%04X BR:%04X", icon.tile_bl, icon.tile_br);
}
ImGui::EndTable();
}
ImGui::Separator();
ImGui::TextWrapped(
"NOTE: Individual icon editing will be implemented in the future "
"Oracle of Secrets menu editor. Each icon is composed of 4 tile words "
"representing a 2x2 arrangement of 8x8 tiles in SNES tile format "
"(vhopppcc cccccccc).");
}
ImGui::EndChild();
}
void ScreenEditor::DrawDungeonMapScreen(int i) {
gfx::ScopedTimer timer("screen_editor_draw_dungeon_map_screen");
@@ -559,6 +643,135 @@ void ScreenEditor::LoadBinaryGfx() {
}
void ScreenEditor::DrawTitleScreenEditor() {
// Initialize title screen on first draw
if (!title_screen_loaded_ && rom()->is_loaded()) {
status_ = title_screen_.Create(rom());
if (!status_.ok()) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error loading title screen: %s",
status_.message().data());
return;
}
title_screen_loaded_ = true;
}
if (!title_screen_loaded_) {
ImGui::Text("Title screen 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_ = title_screen_.Save(rom());
if (status_.ok()) {
ImGui::OpenPopup("SaveSuccess");
}
}
ImGui::SameLine();
ImGui::Text("Selected Tile: %d", selected_title_tile16_);
// Save success popup
if (ImGui::BeginPopup("SaveSuccess")) {
ImGui::Text("Title screen saved successfully!");
ImGui::EndPopup();
}
// Layout: 3-column table for layers
if (ImGui::BeginTable("TitleScreenTable", 3,
ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("BG1 Layer");
ImGui::TableSetupColumn("BG2 Layer");
ImGui::TableSetupColumn("Tile Selector");
ImGui::TableHeadersRow();
// Column 1: BG1 Canvas
ImGui::TableNextColumn();
DrawTitleScreenBG1Canvas();
// Column 2: BG2 Canvas
ImGui::TableNextColumn();
DrawTitleScreenBG2Canvas();
// Column 3: Blockset Selector
ImGui::TableNextColumn();
DrawTitleScreenBlocksetSelector();
ImGui::EndTable();
}
}
void ScreenEditor::DrawTitleScreenBG1Canvas() {
title_bg1_canvas_.DrawBackground();
title_bg1_canvas_.DrawContextMenu();
// Draw BG1 tilemap
auto& bg1_bitmap = title_screen_.bg1_bitmap();
if (bg1_bitmap.is_active()) {
title_bg1_canvas_.DrawBitmap(bg1_bitmap, 0, 0, 2.0f, 255);
}
// 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
}
title_bg1_canvas_.DrawGrid(16.0f);
title_bg1_canvas_.DrawOverlay();
}
void ScreenEditor::DrawTitleScreenBG2Canvas() {
title_bg2_canvas_.DrawBackground();
title_bg2_canvas_.DrawContextMenu();
// Draw BG2 tilemap
auto& bg2_bitmap = title_screen_.bg2_bitmap();
if (bg2_bitmap.is_active()) {
title_bg2_canvas_.DrawBitmap(bg2_bitmap, 0, 0, 2.0f, 255);
}
// 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
}
title_bg2_canvas_.DrawGrid(16.0f);
title_bg2_canvas_.DrawOverlay();
}
void ScreenEditor::DrawTitleScreenBlocksetSelector() {
title_blockset_canvas_.DrawBackground();
title_blockset_canvas_.DrawContextMenu();
// Draw tile8 bitmap (8x8 tiles used to compose tile16)
auto& tiles8_bitmap = title_screen_.tiles8_bitmap();
if (tiles8_bitmap.is_active()) {
title_blockset_canvas_.DrawBitmap(tiles8_bitmap, 0, 0, 2.0f, 255);
}
// Handle tile selection
if (title_blockset_canvas_.DrawTileSelector(16.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
selected_title_tile16_ = tile_x + (tile_y * tiles_per_row);
}
}
title_blockset_canvas_.DrawGrid(16.0f);
title_blockset_canvas_.DrawOverlay();
// Show selected tile preview
if (selected_title_tile16_ >= 0) {
ImGui::Text("Selected Tile: %d", selected_title_tile16_);
}
}
void ScreenEditor::DrawNamingScreenEditor() {

View File

@@ -13,6 +13,7 @@
#include "app/rom.h"
#include "zelda3/screen/dungeon_map.h"
#include "zelda3/screen/inventory.h"
#include "zelda3/screen/title_screen.h"
#include "app/gui/app/editor_layout.h"
#include "imgui/imgui.h"
@@ -59,10 +60,16 @@ class ScreenEditor : public Editor {
void DrawOverworldMapEditor();
void DrawInventoryMenuEditor();
void DrawInventoryItemIcons();
void DrawToolset();
void DrawDungeonMapToolset();
void DrawInventoryToolset();
// Title screen layer editing
void DrawTitleScreenBG1Canvas();
void DrawTitleScreenBG2Canvas();
void DrawTitleScreenBlocksetSelector();
absl::Status LoadDungeonMapTile16(const std::vector<uint8_t>& gfx_data,
bool bin_mode = false);
absl::Status SaveDungeonMapTile16();
@@ -105,7 +112,24 @@ class ScreenEditor : public Editor {
gui::Canvas tilemap_canvas_{"##TilemapCanvas", ImVec2(128 + 2, (192) + 4),
gui::CanvasGridSize::k8x8, 2.f};
// Title screen canvases
// Title screen is 32x32 tiles at 8x8 pixels = 256x256 pixels total
gui::Canvas title_bg1_canvas_{"##TitleBG1Canvas", ImVec2(256, 256),
gui::CanvasGridSize::k8x8, 2.0f};
gui::Canvas title_bg2_canvas_{"##TitleBG2Canvas", ImVec2(256, 256),
gui::CanvasGridSize::k8x8, 2.0f};
// Blockset is 128 pixels wide x 512 pixels tall (16x64 8x8 tiles)
gui::Canvas title_blockset_canvas_{"##TitleBlocksetCanvas",
ImVec2(128, 512),
gui::CanvasGridSize::k8x8, 2.0f};
zelda3::Inventory inventory_;
zelda3::TitleScreen title_screen_;
// Title screen state
int selected_title_tile16_ = 0;
bool title_screen_loaded_ = false;
Rom* rom_;
absl::Status status_;
};

View File

@@ -6,6 +6,7 @@
#include "util/file_util.h"
#include "app/core/window.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/types/snes_tile.h"
#include "app/gfx/render/tilemap.h"
#include "app/gfx/backend/irenderer.h"
@@ -131,8 +132,11 @@ absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom,
}
tile16_blockset.atlas.SetPalette(*rom.mutable_dungeon_palette(3));
// TODO: Queue texture for later rendering.
// core::Renderer::Get().RenderBitmap(&tile16_blockset.atlas);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&tile16_blockset.atlas);
return absl::OkStatus();
}
@@ -189,8 +193,10 @@ absl::Status LoadDungeonMapGfxFromBinary(Rom &rom,
converted_bin.begin() + ((i + 1) * 0x1000));
sheets[i] = gfx::Bitmap(128, 32, 8, gfx_sheets[i]);
sheets[i].SetPalette(*rom.mutable_dungeon_palette(3));
// TODO: Queue texture for later rendering.
// core::Renderer::Get().RenderBitmap(&sheets[i]);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
&sheets[i]);
}
}
file.close();

View File

@@ -3,80 +3,46 @@
#include "app/gfx/backend/irenderer.h"
#include "app/core/window.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/types/snes_tile.h"
#include "app/rom.h"
#include "app/snes.h"
namespace yaze {
namespace zelda3 {
absl::Status Inventory::Create() {
absl::Status Inventory::Create(Rom* rom) {
if (!rom || !rom->is_loaded()) {
return absl::InvalidArgumentError("ROM is not loaded");
}
// Build the tileset first (loads 2BPP graphics)
RETURN_IF_ERROR(BuildTileset(rom));
// Load item icons from ROM
RETURN_IF_ERROR(LoadItemIcons(rom));
// TODO(scawful): For now, create a simple display bitmap
// Future: Oracle of Secrets menu editor will handle full menu layout
data_.reserve(256 * 256);
for (int i = 0; i < 256 * 256; i++) {
data_.push_back(0xFF);
}
RETURN_IF_ERROR(BuildTileset())
for (int i = 0; i < 0x500; i += 0x08) {
ASSIGN_OR_RETURN(auto t1, rom()->ReadWord(i + kBowItemPos));
ASSIGN_OR_RETURN(auto t2, rom()->ReadWord(i + kBowItemPos + 0x02));
ASSIGN_OR_RETURN(auto t3, rom()->ReadWord(i + kBowItemPos + 0x04));
ASSIGN_OR_RETURN(auto t4, rom()->ReadWord(i + kBowItemPos + 0x06));
tiles_.push_back(gfx::GetTilesInfo(t1));
tiles_.push_back(gfx::GetTilesInfo(t2));
tiles_.push_back(gfx::GetTilesInfo(t3));
tiles_.push_back(gfx::GetTilesInfo(t4));
}
const int offsets[] = {0x00, 0x08, 0x800, 0x808};
auto xx = 0;
auto yy = 0;
int i = 0;
for (const auto& tile : tiles_) {
int offset = offsets[i];
for (auto y = 0; y < 0x08; ++y) {
for (auto x = 0; x < 0x08; ++x) {
int mx = x;
int my = y;
if (tile.horizontal_mirror_ != 0) {
mx = 0x07 - x;
}
if (tile.vertical_mirror_ != 0) {
my = 0x07 - y;
}
int xpos = ((tile.id_ % 0x10) * 0x08);
int ypos = (((tile.id_ / 0x10)) * 0x400);
int source = ypos + xpos + (x + (y * 0x80));
auto destination = xx + yy + offset + (mx + (my * 0x100));
data_[destination] = (test_[source] & 0x0F) + tile.palette_ * 0x08;
}
}
if (i == 4) {
i = 0;
xx += 0x10;
if (xx >= 0x100) {
yy += 0x1000;
xx = 0;
}
} else {
i++;
}
}
bitmap_.Create(256, 256, 8, data_);
bitmap_.SetPalette(palette_);
// TODO: Queue texture for later rendering.
// Renderer::Get().RenderBitmap(&bitmap_);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &bitmap_);
return absl::OkStatus();
}
absl::Status Inventory::BuildTileset() {
absl::Status Inventory::BuildTileset(Rom* rom) {
tilesheets_.reserve(6 * 0x2000);
for (int i = 0; i < 6 * 0x2000; i++) tilesheets_.push_back(0xFF);
ASSIGN_OR_RETURN(tilesheets_, Load2BppGraphics(*rom()));
ASSIGN_OR_RETURN(tilesheets_, Load2BppGraphics(*rom));
std::vector<uint8_t> test;
for (int i = 0; i < 0x4000; i++) {
test_.push_back(tilesheets_[i]);
@@ -85,11 +51,104 @@ absl::Status Inventory::BuildTileset() {
test_.push_back(tilesheets_[i]);
}
tilesheets_bmp_.Create(128, 0x130, 64, test_);
auto hud_pal_group = rom()->palette_group().hud;
auto hud_pal_group = rom->palette_group().hud;
palette_ = hud_pal_group[0];
tilesheets_bmp_.SetPalette(palette_);
// TODO: Queue texture for later rendering.
// Renderer::Get().RenderBitmap(&tilesheets_bmp_);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &tilesheets_bmp_);
return absl::OkStatus();
}
absl::Status Inventory::LoadItemIcons(Rom* rom) {
// Convert SNES address to PC address
int pc_addr = SnesToPc(kItemIconsPtr);
// Define icon categories and their ROM offsets (relative to kItemIconsPtr)
// Based on bank_0D.asm ItemIcons structure
struct IconDef {
int offset;
std::string name;
};
// Bow icons (.bows section)
std::vector<IconDef> bow_icons = {
{0x00, "No bow"},
{0x08, "Empty bow"},
{0x10, "Bow and arrows"},
{0x18, "Empty silvers bow"},
{0x20, "Silver bow and arrows"}
};
// Boomerang icons (.booms section)
std::vector<IconDef> boom_icons = {
{0x28, "No boomerang"},
{0x30, "Blue boomerang"},
{0x38, "Red boomerang"}
};
// Hookshot icons (.hook section)
std::vector<IconDef> hook_icons = {
{0x40, "No hookshot"},
{0x48, "Hookshot"}
};
// Bomb icons (.bombs section)
std::vector<IconDef> bomb_icons = {
{0x50, "No bombs"},
{0x58, "Bombs"}
};
// Load all icon categories
auto load_icons = [&](const std::vector<IconDef>& icons) -> absl::Status {
for (const auto& icon_def : icons) {
ItemIcon icon;
int icon_addr = pc_addr + icon_def.offset;
ASSIGN_OR_RETURN(icon.tile_tl, rom->ReadWord(icon_addr));
ASSIGN_OR_RETURN(icon.tile_tr, rom->ReadWord(icon_addr + 2));
ASSIGN_OR_RETURN(icon.tile_bl, rom->ReadWord(icon_addr + 4));
ASSIGN_OR_RETURN(icon.tile_br, rom->ReadWord(icon_addr + 6));
icon.name = icon_def.name;
item_icons_.push_back(icon);
}
return absl::OkStatus();
};
RETURN_IF_ERROR(load_icons(bow_icons));
RETURN_IF_ERROR(load_icons(boom_icons));
RETURN_IF_ERROR(load_icons(hook_icons));
RETURN_IF_ERROR(load_icons(bomb_icons));
// TODO(scawful): Load remaining icon categories:
// - Mushroom/Powder (.shroom)
// - Magic powder (.powder)
// - Fire rod (.fires)
// - Ice rod (.ices)
// - Bombos medallion (.bombos)
// - Ether medallion (.ether)
// - Quake medallion (.quake)
// - Lantern (.lamp)
// - Hammer (.hammer)
// - Flute (.flute)
// - Bug net (.net)
// - Book of Mudora (.book)
// - Bottles (.bottles) - Multiple variants (empty, red potion, green potion, etc.)
// - Cane of Somaria (.canes)
// - Cane of Byrna (.byrn)
// - Magic cape (.cape)
// - Magic mirror (.mirror)
// - Gloves (.glove)
// - Boots (.boots)
// - Flippers (.flippers)
// - Moon pearl (.pearl)
// - Swords (.swords)
// - Shields (.shields)
// - Armor (.armor)
return absl::OkStatus();
}

View File

@@ -12,21 +12,51 @@ namespace yaze {
namespace zelda3 {
constexpr int kInventoryStart = 0x6564A;
constexpr int kBowItemPos = 0x6F631;
// ItemIcons base address in SNES format (0x0DF629)
constexpr int kItemIconsPtr = 0x0DF629;
/**
* @brief Represents a single item icon (2x2 tiles = 4 tile words)
*/
struct ItemIcon {
uint16_t tile_tl; // Top-left tile word (vhopppcc cccccccc format)
uint16_t tile_tr; // Top-right tile word
uint16_t tile_bl; // Bottom-left tile word
uint16_t tile_br; // Bottom-right tile word
std::string name; // Human-readable name for debugging
};
/**
* @brief Inventory manages the inventory screen graphics and layout.
*
* The inventory screen consists of a 256x256 bitmap displaying equipment,
* items, and UI elements using 2BPP graphics and HUD palette.
*/
class Inventory {
public:
absl::Status Create();
/**
* @brief Initialize and load inventory screen data from ROM
* @param rom ROM instance to read data from
*/
absl::Status Create(Rom* rom);
auto &bitmap() { return bitmap_; }
auto &tilesheet() { return tilesheets_bmp_; }
auto &palette() { return palette_; }
void LoadRom(Rom *rom) { rom_ = rom; }
auto rom() { return rom_; }
auto &item_icons() { return item_icons_; }
private:
absl::Status BuildTileset();
/**
* @brief Build the tileset from 2BPP graphics
* @param rom ROM instance to read graphics from
*/
absl::Status BuildTileset(Rom* rom);
/**
* @brief Load individual item icons from ROM
* @param rom ROM instance to read icon data from
*/
absl::Status LoadItemIcons(Rom* rom);
std::vector<uint8_t> data_;
gfx::Bitmap bitmap_;
@@ -36,9 +66,9 @@ class Inventory {
gfx::Bitmap tilesheets_bmp_;
gfx::SnesPalette palette_;
Rom *rom_;
gui::Canvas canvas_;
std::vector<gfx::TileInfo> tiles_;
std::vector<ItemIcon> item_icons_;
};
} // namespace zelda3

View File

@@ -3,121 +3,295 @@
#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"
namespace yaze {
namespace zelda3 {
void TitleScreen::Create() {
tiles8Bitmap.Create(128, 512, 8, std::vector<uint8_t>(0x20000));
tilesBG1Bitmap.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
tilesBG2Bitmap.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
oamBGBitmap.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
BuildTileset();
LoadTitleScreen();
absl::Status TitleScreen::Create(Rom* rom) {
if (!rom || !rom->is_loaded()) {
return absl::InvalidArgumentError("ROM is not loaded");
}
// Initialize bitmaps for each layer
tiles8_bitmap_.Create(128, 512, 8, std::vector<uint8_t>(0x20000));
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));
// Initialize tilemap buffers
tiles_bg1_buffer_.fill(0x492); // Default empty tile
tiles_bg2_buffer_.fill(0x492);
// Load palette (title screen uses sprite graphics)
auto sprite_pal_group = rom->palette_group().sprites_aux1;
palette_ = sprite_pal_group[0];
// Build tile16 blockset from graphics
RETURN_IF_ERROR(BuildTileset(rom));
// Load tilemap data from ROM
RETURN_IF_ERROR(LoadTitleScreen(rom));
return absl::OkStatus();
}
void TitleScreen::BuildTileset() {
absl::Status TitleScreen::BuildTileset(Rom* rom) {
// Title screen uses specific graphics sheets
// Sheet arrangement for title screen (from ALTTP disassembly):
// 8-15: Graphics sheets 115, 115+6, 115+7, 112, etc.
uint8_t staticgfx[16] = {0};
// Main Blocksets
// Title screen specific graphics sheets
staticgfx[8] = 115 + 0; // Title logo graphics
staticgfx[9] = 115 + 3; // Sprite graphics
staticgfx[10] = 115 + 6; // Additional graphics
staticgfx[11] = 115 + 7; // Additional graphics
staticgfx[12] = 115 + 0; // More sprite graphics
staticgfx[13] = 112; // UI graphics
staticgfx[14] = 112; // UI graphics
staticgfx[15] = 112; // UI graphics
// TODO: get the gfx from the GFX class rather than the rom.
// for (int i = 0; i < 8; i++) {
// staticgfx[i] = GfxGroups.mainGfx[titleScreenTilesGFX][i];
// }
// Get ROM graphics buffer (contains 3BPP/4BPP SNES format data)
const auto& gfx_buffer = rom->graphics_buffer();
auto& tiles8_data = tiles8_bitmap_.mutable_data();
staticgfx[8] = 115 + 0;
// staticgfx[9] = (GfxGroups.spriteGfx[titleScreenSpritesGFX][3] + 115);
staticgfx[10] = 115 + 6;
staticgfx[11] = 115 + 7;
// staticgfx[12] = (GfxGroups.spriteGfx[titleScreenSpritesGFX][0] + 115);
staticgfx[13] = 112;
staticgfx[14] = 112;
staticgfx[15] = 112;
// Loaded gfx for the current screen (empty at this point)
uint8_t* currentmapgfx8Data = tiles8Bitmap.mutable_data().data();
// All gfx of the game pack of 2048 bytes (4bpp)
uint8_t* allgfxData = nullptr;
// Load and convert each graphics sheet from 3BPP SNES format to 8BPP indexed
// Each sheet is 2048 bytes in 3BPP format -> converts to 0x1000 bytes (4096) in 8BPP
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 2048; j++) {
uint8_t mapByte = allgfxData[j + (staticgfx[i] * 2048)];
switch (i) {
case 0:
case 3:
case 4:
case 5:
mapByte += 0x88;
break;
}
int sheet_id = staticgfx[i];
int source_offset = sheet_id * 2048;
currentmapgfx8Data[(i * 2048) + j] = mapByte; // Upload used gfx data
if (source_offset + 2048 <= gfx_buffer.size()) {
// Extract 3BPP sheet data
std::vector<uint8_t> sheet_3bpp(gfx_buffer.begin() + source_offset,
gfx_buffer.begin() + source_offset + 2048);
// Convert 3BPP SNES format to 8BPP indexed format
auto sheet_8bpp = gfx::SnesTo8bppSheet(sheet_3bpp, 3, 1);
// Copy converted data to tiles8_bitmap at correct position
// Each converted sheet is 0x1000 bytes (128x32 pixels)
int dest_offset = i * 0x1000;
if (dest_offset + sheet_8bpp.size() <= tiles8_data.size()) {
std::copy(sheet_8bpp.begin(), sheet_8bpp.end(),
tiles8_data.begin() + dest_offset);
}
}
}
// Set palette on tiles8 bitmap
tiles8_bitmap_.SetPalette(palette_);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &tiles8_bitmap_);
// TODO: Build tile16 blockset from tile8 data
// This would involve composing 16x16 tiles from 8x8 tiles
// For now, we'll use the tile8 data directly
return absl::OkStatus();
}
void TitleScreen::LoadTitleScreen() {
int pos =
(rom_[0x138C + 3] << 16) + (rom_[0x1383 + 3] << 8) + rom_[0x137A + 3];
for (int i = 0; i < 1024; i++) {
tilesBG1Buffer[i] = 492;
tilesBG2Buffer[i] = 492;
}
absl::Status TitleScreen::LoadTitleScreen(Rom* rom) {
// Title screen tilemap is stored in compressed format
// The tilemap data pointer is stored at ROM address 0x0137A (3 bytes)
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;
pos = SnesToPc(pos);
while ((rom_[pos] & 0x80) != 0x80) {
int dest_addr = pos; // $03 and $04
pos += 2;
short length = pos;
bool increment64 = (length & 0x8000) == 0x8000;
bool fixsource = (length & 0x4000) == 0x4000;
// Initialize buffers with default empty tile
for (int i = 0; i < 1024; i++) {
tiles_bg1_buffer_[i] = 0x492;
tiles_bg2_buffer_[i] = 0x492;
}
// Read compressed tilemap data
// Format: destination address (word), length (word), tile data
while (pos < rom->size()) {
ASSIGN_OR_RETURN(uint8_t first_byte, rom->ReadByte(pos));
if ((first_byte & 0x80) == 0x80) {
break; // End of data marker
}
ASSIGN_OR_RETURN(uint16_t dest_addr, rom->ReadWord(pos));
pos += 2;
length = (short)((length & 0x07FF));
ASSIGN_OR_RETURN(uint16_t length_flags, rom->ReadWord(pos));
pos += 2;
bool increment64 = (length_flags & 0x8000) == 0x8000;
bool fixsource = (length_flags & 0x4000) == 0x4000;
int length = (length_flags & 0x07FF);
int j = 0;
int jj = 0;
int posB = pos;
while (j < (length / 2) + 1) {
uint16_t tiledata = (uint16_t)pos;
if (dest_addr >= 0x1000) {
// destAddr -= 0x1000;
if (dest_addr < 0x2000) {
tilesBG1Buffer[dest_addr - 0x1000] = tiledata;
}
} else {
if (dest_addr < 0x1000) {
tilesBG2Buffer[dest_addr] = tiledata;
}
for (int j = 0; j < (length / 2) + 1; j++) {
ASSIGN_OR_RETURN(uint16_t tiledata, rom->ReadWord(pos));
// Determine which layer this tile belongs to
if (dest_addr >= 0x1000 && dest_addr < 0x2000) {
// BG1 layer
tiles_bg1_buffer_[dest_addr - 0x1000] = tiledata;
} else if (dest_addr < 0x1000) {
// BG2 layer
tiles_bg2_buffer_[dest_addr] = tiledata;
}
// Advance destination address
if (increment64) {
dest_addr += 32;
} else {
dest_addr++;
}
// Advance source position
if (!fixsource) {
pos += 2;
}
jj += 2;
j++;
}
if (fixsource) {
pos += 2;
} else {
pos = posB + jj;
pos = posB + ((length / 2) + 1) * 2;
}
}
pal_selected_ = 2;
// Render tilemaps into bitmap pixels
RETURN_IF_ERROR(RenderBG1Layer());
RETURN_IF_ERROR(RenderBG2Layer());
// Apply palettes to layer bitmaps
tiles_bg1_bitmap_.SetPalette(palette_);
tiles_bg2_bitmap_.SetPalette(palette_);
oam_bg_bitmap_.SetPalette(palette_);
// Queue texture creation for all layer bitmaps
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &tiles_bg1_bitmap_);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &tiles_bg2_bitmap_);
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &oam_bg_bitmap_);
return absl::OkStatus();
}
absl::Status TitleScreen::RenderBG1Layer() {
// BG1 layer is 32x32 tiles (256x256 pixels)
auto& bg1_data = tiles_bg1_bitmap_.mutable_data();
const auto& tile8_bitmap_data = tiles8_bitmap_.vector();
// Render each tile in the 32x32 tilemap
for (int tile_y = 0; tile_y < 32; tile_y++) {
for (int tile_x = 0; tile_x < 32; tile_x++) {
int tilemap_index = tile_y * 32 + tile_x;
uint16_t tile_word = tiles_bg1_buffer_[tilemap_index];
// Extract tile info from SNES tile word (vhopppcc cccccccc format)
int tile_id = tile_word & 0x3FF; // Bits 0-9: tile ID
int palette = (tile_word >> 10) & 0x07; // Bits 10-12: palette
bool h_flip = (tile_word & 0x4000) != 0; // Bit 14: horizontal flip
bool v_flip = (tile_word & 0x8000) != 0; // Bit 15: vertical flip
// Calculate source position in tiles8_bitmap_ (16 tiles per row, 8x8 each)
int src_tile_x = (tile_id % 16) * 8;
int src_tile_y = (tile_id / 16) * 8;
// Copy 8x8 tile pixels from tile8 bitmap to BG1 bitmap
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
// Apply flipping
int src_px = h_flip ? (7 - px) : px;
int src_py = v_flip ? (7 - py) : py;
// Calculate source and destination positions
int src_x = src_tile_x + src_px;
int src_y = src_tile_y + src_py;
int src_pos = src_y * 128 + src_x; // tiles8_bitmap_ is 128 pixels wide
int dest_x = tile_x * 8 + px;
int dest_y = tile_y * 8 + py;
int dest_pos = dest_y * 256 + dest_x; // BG1 is 256 pixels wide
// Copy pixel with palette application
if (src_pos < tile8_bitmap_data.size() && dest_pos < bg1_data.size()) {
uint8_t pixel_value = tile8_bitmap_data[src_pos];
// Apply palette index (each palette has 8 colors, pixel uses lower 3 bits)
bg1_data[dest_pos] = (pixel_value & 0x07) | (palette << 3);
}
}
}
}
}
return absl::OkStatus();
}
absl::Status TitleScreen::RenderBG2Layer() {
// BG2 layer is 32x32 tiles (256x256 pixels)
auto& bg2_data = tiles_bg2_bitmap_.mutable_data();
const auto& tile8_bitmap_data = tiles8_bitmap_.vector();
// Render each tile in the 32x32 tilemap
for (int tile_y = 0; tile_y < 32; tile_y++) {
for (int tile_x = 0; tile_x < 32; tile_x++) {
int tilemap_index = tile_y * 32 + tile_x;
uint16_t tile_word = tiles_bg2_buffer_[tilemap_index];
// Extract tile info from SNES tile word (vhopppcc cccccccc format)
int tile_id = tile_word & 0x3FF; // Bits 0-9: tile ID
int palette = (tile_word >> 10) & 0x07; // Bits 10-12: palette
bool h_flip = (tile_word & 0x4000) != 0; // Bit 14: horizontal flip
bool v_flip = (tile_word & 0x8000) != 0; // Bit 15: vertical flip
// Calculate source position in tiles8_bitmap_ (16 tiles per row, 8x8 each)
int src_tile_x = (tile_id % 16) * 8;
int src_tile_y = (tile_id / 16) * 8;
// Copy 8x8 tile pixels from tile8 bitmap to BG2 bitmap
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
// Apply flipping
int src_px = h_flip ? (7 - px) : px;
int src_py = v_flip ? (7 - py) : py;
// Calculate source and destination positions
int src_x = src_tile_x + src_px;
int src_y = src_tile_y + src_py;
int src_pos = src_y * 128 + src_x; // tiles8_bitmap_ is 128 pixels wide
int dest_x = tile_x * 8 + px;
int dest_y = tile_y * 8 + py;
int dest_pos = dest_y * 256 + dest_x; // BG2 is 256 pixels wide
// Copy pixel with palette application
if (src_pos < tile8_bitmap_data.size() && dest_pos < bg2_data.size()) {
uint8_t pixel_value = tile8_bitmap_data[src_pos];
// Apply palette index (each palette has 8 colors, pixel uses lower 3 bits)
bg2_data[dest_pos] = (pixel_value & 0x07) | (palette << 3);
}
}
}
}
}
return absl::OkStatus();
}
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");
}
} // namespace zelda3

View File

@@ -1,73 +1,91 @@
#ifndef YAZE_APP_ZELDA3_SCREEN_H
#define YAZE_APP_ZELDA3_SCREEN_H
#include "absl/status/status.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/render/tilemap.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gfx/types/snes_tile.h"
#include "app/rom.h"
namespace yaze {
namespace zelda3 {
/**
* @brief TitleScreen manages the title screen graphics and tilemap data.
*
* The title screen consists of three layers:
* - BG1: Main logo and graphics
* - BG2: Background elements
* - OAM: Sprite layer (sword, etc.)
*
* Each layer is stored as a 32x32 tilemap (0x400 tiles = 0x1000 bytes as words)
*/
class TitleScreen {
public:
void Create();
/**
* @brief Initialize and load title screen data from ROM
* @param rom ROM instance to read data from
*/
absl::Status Create(Rom* rom);
// Accessors for layer data
auto& bg1_buffer() { return tiles_bg1_buffer_; }
auto& bg2_buffer() { return tiles_bg2_buffer_; }
auto& oam_buffer() { return oam_data_; }
// Accessors for bitmaps
auto& bg1_bitmap() { return tiles_bg1_bitmap_; }
auto& bg2_bitmap() { return tiles_bg2_bitmap_; }
auto& oam_bitmap() { return oam_bg_bitmap_; }
auto& tiles8_bitmap() { return tiles8_bitmap_; }
auto& blockset() { return tile16_blockset_; }
// Palette access
auto& palette() { return palette_; }
// Save changes back to ROM
absl::Status Save(Rom* rom);
private:
void BuildTileset();
void LoadTitleScreen();
/**
* @brief Build the tile16 blockset from ROM graphics
* @param rom ROM instance to read graphics from
*/
absl::Status BuildTileset(Rom* rom);
int sword_x_ = 0;
int mx_click_ = 0;
int my_click_ = 0;
int mx_dist_ = 0;
int my_dist_ = 0;
int last_x_ = 0;
int last_y_ = 0;
int x_in_ = 0;
int y_in_ = 0;
int dungmap_selected_tile_ = 0;
int dungmap_selected_ = 0;
int selected_palette_ = 0;
int total_floors_ = 0;
int current_floor_ = 0;
int num_basement_ = 0;
int num_floor_ = 0;
int selected_map_tile = 0;
int current_floor_rooms; // [1][];
int current_floor_gfx; // [1][];
int copied_data_rooms; // 25
int copied_data_gfx; // 25
int pal_selected_;
int addresses[7] = {0x53de4, 0x53e2c, 0x53e08, 0x53e50,
0x53e74, 0x53e98, 0x53ebc};
int addressesgfx[7] = {0x53ee0, 0x53f04, 0x53ef2, 0x53f16,
0x53f28, 0x53f3a, 0x53f4c};
/**
* @brief Load title screen tilemap data from ROM
* @param rom ROM instance to read tilemap from
*/
absl::Status LoadTitleScreen(Rom* rom);
uint16_t bossRoom = 0x000F;
uint16_t selected_tile = 0;
uint16_t tilesBG1Buffer[0x1000]; // 0x1000
uint16_t tilesBG2Buffer[0x1000]; // 0x1000
uint8_t mapdata; // 64 * 64
uint8_t dwmapdata; // 64 * 64
/**
* @brief Render BG1 tilemap into bitmap pixels
* Converts tile IDs from tiles_bg1_buffer_ into pixel data
*/
absl::Status RenderBG1Layer();
bool mDown = false;
bool swordSelected = false;
bool darkWorld = false;
bool currentDungeonChanged = false;
bool editedFromEditor = false;
bool mouseDown = false;
bool mdown = false;
/**
* @brief Render BG2 tilemap into bitmap pixels
* Converts tile IDs from tiles_bg2_buffer_ into pixel data
*/
absl::Status RenderBG2Layer();
Rom rom_;
int pal_selected_ = 2;
gfx::OamTile oam_data[10];
gfx::OamTile selected_oam_tile;
gfx::OamTile last_selected_oam_tile;
std::array<uint16_t, 0x1000> tiles_bg1_buffer_; // BG1 tilemap (32x32 tiles)
std::array<uint16_t, 0x1000> tiles_bg2_buffer_; // BG2 tilemap (32x32 tiles)
gfx::Bitmap tilesBG1Bitmap; // 0x80000
gfx::Bitmap tilesBG2Bitmap; // 0x80000
gfx::Bitmap oamBGBitmap; // 0x80000
gfx::Bitmap tiles8Bitmap; // 0x20000
gfx::OamTile oam_data_[10];
gfx::Bitmap tiles_bg1_bitmap_; // Rendered BG1 layer
gfx::Bitmap tiles_bg2_bitmap_; // Rendered BG2 layer
gfx::Bitmap oam_bg_bitmap_; // Rendered OAM layer
gfx::Bitmap tiles8_bitmap_; // 8x8 tile graphics
gfx::Tilemap tile16_blockset_; // 16x16 tile blockset
gfx::SnesPalette palette_; // Title screen palette
};
} // namespace zelda3