docs: Add comprehensive Dungeon Editor technical guide

- Introduced a new DUNGEON_EDITOR_COMPLETE_GUIDE.md that details the features, architecture, and usage of the Dungeon Editor.
- Documented critical bug fixes, including segfaults and loading order issues, along with their resolutions.
- Enhanced the guide with a structured overview, quick start instructions, and troubleshooting tips for users.
- Updated the architecture section to reflect the new card-based system and self-contained room management.
- Included detailed testing commands and expected outputs to assist developers in verifying functionality.
This commit is contained in:
scawful
2025-10-09 20:49:10 -04:00
parent c512dd7f35
commit c33a9c9635
9 changed files with 508 additions and 154 deletions

View File

@@ -14,42 +14,7 @@ namespace yaze::editor {
using ImGui::Separator;
void DungeonCanvasViewer::DrawDungeonTabView() {
static int next_tab_id = 0;
if (ImGui::BeginTabBar("MyTabBar", ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_TabListPopupButton)) {
if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) {
if (std::find(active_rooms_.begin(), active_rooms_.end(), current_active_room_tab_) != active_rooms_.end()) {
next_tab_id++;
}
active_rooms_.push_back(next_tab_id++);
}
// Submit our regular tabs
for (int n = 0; n < active_rooms_.Size;) {
bool open = true;
if (active_rooms_[n] > sizeof(zelda3::kRoomNames) / 4) {
active_rooms_.erase(active_rooms_.Data + n);
continue;
}
if (ImGui::BeginTabItem(zelda3::kRoomNames[active_rooms_[n]].data(), &open, ImGuiTabItemFlags_None)) {
current_active_room_tab_ = n;
DrawDungeonCanvas(active_rooms_[n]);
ImGui::EndTabItem();
}
if (!open)
active_rooms_.erase(active_rooms_.Data + n);
else
n++;
}
ImGui::EndTabBar();
}
Separator();
}
// DrawDungeonTabView() removed - DungeonEditorV2 uses EditorCard system for flexible docking
void DungeonCanvasViewer::Draw(int room_id) {
DrawDungeonCanvas(room_id);
@@ -86,9 +51,24 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
ImGui::SameLine();
gui::InputHexByte("Palette", &room.palette);
gui::InputHexByte("Floor1", &room.floor1);
// Floor graphics - use temp variables and setters (floor1/floor2 are now accessors)
uint8_t floor1_val = room.floor1();
uint8_t floor2_val = room.floor2();
if (gui::InputHexByte("Floor1", &floor1_val) && ImGui::IsItemDeactivatedAfterEdit()) {
room.set_floor1(floor1_val);
// Trigger re-render since floor graphics changed
if (room.rom() && room.rom()->is_loaded()) {
room.RenderRoomGraphics();
}
}
ImGui::SameLine();
gui::InputHexByte("Floor2", &room.floor2);
if (gui::InputHexByte("Floor2", &floor2_val) && ImGui::IsItemDeactivatedAfterEdit()) {
room.set_floor2(floor2_val);
// Trigger re-render since floor graphics changed
if (room.rom() && room.rom()->is_loaded()) {
room.RenderRoomGraphics();
}
}
ImGui::SameLine();
gui::InputHexWord("Message ID", &room.message_id_);
@@ -118,11 +98,9 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
// Only reload if ROM is properly loaded
if (room.rom() && room.rom()->is_loaded()) {
// Force reload of room graphics
// Room buffers are now self-contained - no need for separate palette operations
room.LoadRoomGraphics(room.blockset);
room.RenderRoomGraphics();
// Render palettes to graphics sheets
RenderGraphicsSheetPalettes(room_id);
room.RenderRoomGraphics(); // Applies palettes internally
}
prev_blockset = room.blockset;
@@ -220,6 +198,10 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
// This already includes objects rendered by ObjectDrawer in Room::RenderObjectsToBackground()
DrawRoomBackgroundLayers(room_id);
// VISUALIZATION: Draw object position rectangles (for debugging)
// This shows where objects are placed regardless of whether graphics render
DrawObjectPositionOutlines(room);
// Render sprites as simple 16x16 squares with labels
// (Sprites are not part of the background buffers)
RenderSprites(room);
@@ -389,6 +371,58 @@ void DungeonCanvasViewer::CalculateWallDimensions(const zelda3::RoomObject& obje
height = std::min(height, 256);
}
// Object visualization methods
void DungeonCanvasViewer::DrawObjectPositionOutlines(const zelda3::Room& room) {
// Draw colored rectangles showing object positions
// This helps visualize object placement even if graphics don't render correctly
const auto& objects = room.GetTileObjects();
for (const auto& obj : objects) {
// Convert object position (tile coordinates) to canvas pixel coordinates
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
// Calculate object dimensions based on type and size
int width = 8; // Default 8x8 pixels
int height = 8;
// Use ZScream pattern: size field determines dimensions
// Lower nibble = horizontal size, upper nibble = vertical size
int size_h = (obj.size() & 0x0F);
int size_v = (obj.size() >> 4) & 0x0F;
// Objects are typically (size+1) tiles wide/tall
width = (size_h + 1) * 8;
height = (size_v + 1) * 8;
// Clamp to reasonable sizes
width = std::min(width, 512);
height = std::min(height, 512);
// Color-code by layer
ImVec4 outline_color;
if (obj.GetLayerValue() == 0) {
outline_color = ImVec4(1.0f, 0.0f, 0.0f, 0.7f); // Red for layer 0
} else if (obj.GetLayerValue() == 1) {
outline_color = ImVec4(0.0f, 1.0f, 0.0f, 0.7f); // Green for layer 1
} else {
outline_color = ImVec4(0.0f, 0.0f, 1.0f, 0.7f); // Blue for layer 2
}
// Draw outline rectangle
canvas_.DrawRect(canvas_x, canvas_y, width, height, outline_color);
// Draw object ID label
std::string label = absl::StrFormat("0x%02X", obj.id_);
canvas_.DrawText(label, canvas_x + 2, canvas_y + 2);
}
// Log object count
if (!objects.empty()) {
LOG_DEBUG("[DrawObjectPositionOutlines]", "Drew %zu object outlines", objects.size());
}
}
// Room graphics management methods
absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(int room_id) {
LOG_DEBUG("[LoadAndRender]", "START room_id=%d", room_id);
@@ -432,81 +466,15 @@ absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(int room_id) {
}
}
// Render the room graphics to the graphics arena
// Render the room graphics (self-contained - handles all palette application)
LOG_DEBUG("[LoadAndRender]", "Calling room.RenderRoomGraphics()...");
room.RenderRoomGraphics();
LOG_DEBUG("[LoadAndRender]", "RenderRoomGraphics() complete");
// Update the background layers with proper palette
LOG_DEBUG("[LoadAndRender]", "Rendering palettes to graphics sheets...");
RETURN_IF_ERROR(RenderGraphicsSheetPalettes(room_id));
LOG_DEBUG("[LoadAndRender]", "RenderGraphicsSheetPalettes() complete");
LOG_DEBUG("[LoadAndRender]", "RenderRoomGraphics() complete - room buffers self-contained");
LOG_DEBUG("[LoadAndRender]", "SUCCESS");
return absl::OkStatus();
}
absl::Status DungeonCanvasViewer::RenderGraphicsSheetPalettes(int room_id) {
if (room_id < 0 || room_id >= 128) {
return absl::InvalidArgumentError("Invalid room ID");
}
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
if (!rooms_) {
return absl::FailedPreconditionError("Room data not available");
}
auto& room = (*rooms_)[room_id];
// Validate palette group access
if (current_palette_group_id_ >= rom_->palette_group().dungeon_main.size()) {
return absl::FailedPreconditionError("Invalid palette group ID");
}
// Get the current room's palette
auto current_palette = rom_->palette_group().dungeon_main[current_palette_group_id_];
// Update BG1 (background layer 1) with proper palette
if (room.blocks().size() >= 8) {
for (int i = 0; i < 8; i++) {
int block = room.blocks()[i];
if (block >= 0 && block < gfx::Arena::Get().gfx_sheets().size()) {
if (current_palette_id_ < current_palette_group_.size()) {
gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent(
current_palette_group_[current_palette_id_], 0);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE,
&gfx::Arena::Get().gfx_sheets()[block]);
}
}
}
}
// Update BG2 (background layer 2) with sprite auxiliary palette
if (room.blocks().size() >= 16) {
auto sprites_aux1_pal_group = rom_->palette_group().sprites_aux1;
if (current_palette_id_ < sprites_aux1_pal_group.size()) {
for (int i = 8; i < 16; i++) {
int block = room.blocks()[i];
if (block >= 0 && block < gfx::Arena::Get().gfx_sheets().size()) {
gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent(
sprites_aux1_pal_group[current_palette_id_], 0);
// Queue texture update via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE,
&gfx::Arena::Get().gfx_sheets()[block]);
}
}
}
}
return absl::OkStatus();
}
void DungeonCanvasViewer::DrawRoomBackgroundLayers(int room_id) {
if (room_id < 0 || room_id >= 128 || !rooms_) return;

View File

@@ -24,21 +24,21 @@ namespace editor {
*/
class DungeonCanvasViewer {
public:
explicit DungeonCanvasViewer(Rom* rom = nullptr)
: rom_(rom), object_renderer_(rom), object_interaction_(&canvas_) {}
explicit DungeonCanvasViewer(Rom* rom = nullptr)
: rom_(rom), object_interaction_(&canvas_) {}
void DrawDungeonTabView();
// DrawDungeonTabView() removed - using EditorCard system instead
void DrawDungeonCanvas(int room_id);
void Draw(int room_id);
void SetRom(Rom* rom) {
rom_ = rom;
object_renderer_.SetROM(rom);
}
Rom* rom() const { return rom_; }
// Room data access
void SetRooms(std::array<zelda3::Room, 0x128>* rooms) { rooms_ = rooms; }
// Used by overworld editor when double-clicking entrances
void set_active_rooms(const ImVector<int>& rooms) { active_rooms_ = rooms; }
void set_current_active_room_tab(int tab) { current_active_room_tab_ = tab; }
@@ -104,19 +104,22 @@ class DungeonCanvasViewer {
// Object dimension calculation
void CalculateWallDimensions(const zelda3::RoomObject& object, int& width, int& height);
// Object visualization
void DrawObjectPositionOutlines(const zelda3::Room& room);
// Room graphics management
// Load: Read from ROM, Render: Process pixels, Draw: Display on canvas
absl::Status LoadAndRenderRoomGraphics(int room_id);
absl::Status RenderGraphicsSheetPalettes(int room_id); // Renamed from UpdateRoomBackgroundLayers
void DrawRoomBackgroundLayers(int room_id); // Renamed from RenderRoomBackgroundLayers
void DrawRoomBackgroundLayers(int room_id); // Draw room buffers to canvas
Rom* rom_ = nullptr;
gui::Canvas canvas_{"##DungeonCanvas", ImVec2(0x200, 0x200)};
zelda3::ObjectRenderer object_renderer_;
// ObjectRenderer removed - use ObjectDrawer for rendering (production system)
DungeonObjectInteraction object_interaction_;
// Room data
std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
// Used by overworld editor for double-click entrance → open dungeon room
ImVector<int> active_rooms_;
int current_active_room_tab_ = 0;

View File

@@ -433,24 +433,30 @@ void DungeonEditorV2::DrawRoomTab(int room_id) {
}
}
// Initialize room graphics and objects if not already done
// This ensures objects are drawn to background buffers before canvas displays them
// Initialize room graphics and objects in CORRECT ORDER
// Critical sequence: 1. Load data from ROM, 2. Load objects (sets floor graphics), 3. Render
if (room.IsLoaded()) {
// Load room graphics (populates blocks, gfx sheets)
bool needs_render = false;
// Step 1: Load room data from ROM (blocks, blockset info)
if (room.blocks().empty()) {
room.RenderRoomGraphics();
room.LoadRoomGraphics(room.blockset);
needs_render = true;
LOG_DEBUG("[DungeonEditorV2]", "Loaded room %d graphics from ROM", room_id);
}
// Load room objects (populates tile_objects_)
// Step 2: Load objects from ROM (CRITICAL: sets floor1_graphics_, floor2_graphics_!)
if (room.GetTileObjects().empty()) {
room.LoadObjects();
needs_render = true;
LOG_DEBUG("[DungeonEditorV2]", "Loaded room %d objects from ROM", room_id);
}
// Render objects to background buffers (CRITICAL: this must happen before canvas drawing)
// This uses ObjectDrawer to draw all objects into bg1_buffer_ and bg2_buffer_
// Step 3: Render to bitmaps (now floor graphics are set correctly!)
auto& bg1_bitmap = room.bg1_buffer().bitmap();
if (!bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
room.RenderObjectsToBackground();
if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
room.RenderRoomGraphics(); // Includes RenderObjectsToBackground() internally
LOG_DEBUG("[DungeonEditorV2]", "Rendered room %d to bitmaps", room_id);
}
}

View File

@@ -85,23 +85,9 @@ void DungeonObjectSelector::DrawObjectRenderer() {
int preview_x = 128 - 16; // Center horizontally
int preview_y = 128 - 16; // Center vertically
auto preview_result = object_renderer_.RenderObject(preview_object_, preview_palette_);
if (preview_result.ok()) {
auto preview_bitmap = std::move(preview_result.value());
if (preview_bitmap.width() > 0 && preview_bitmap.height() > 0) {
preview_bitmap.SetPalette(preview_palette_);
// Queue texture creation via Arena's deferred system
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::CREATE, &preview_bitmap);
object_canvas_.DrawBitmap(preview_bitmap, preview_x, preview_y, 1.0f, 255);
} else {
// Fallback: Draw primitive shape
RenderObjectPrimitive(preview_object_, preview_x, preview_y);
}
} else {
// Fallback: Draw primitive shape
RenderObjectPrimitive(preview_object_, preview_x, preview_y);
}
// TODO: Implement preview using ObjectDrawer + small BackgroundBuffer
// For now, use primitive shape rendering (shows object ID and rough dimensions)
RenderObjectPrimitive(preview_object_, preview_x, preview_y);
}
object_canvas_.DrawOverlay();

View File

@@ -3,7 +3,7 @@
#include "app/gui/canvas.h"
#include "app/rom.h"
#include "app/zelda3/dungeon/object_renderer.h"
// object_renderer.h removed - using ObjectDrawer for production rendering
#include "app/zelda3/dungeon/dungeon_object_editor.h"
#include "app/zelda3/dungeon/dungeon_editor_system.h"
#include "app/gfx/snes_palette.h"
@@ -17,7 +17,7 @@ namespace editor {
*/
class DungeonObjectSelector {
public:
explicit DungeonObjectSelector(Rom* rom = nullptr) : rom_(rom), object_renderer_(rom) {}
explicit DungeonObjectSelector(Rom* rom = nullptr) : rom_(rom) {}
void DrawTileSelector();
void DrawObjectRenderer();
@@ -26,11 +26,9 @@ class DungeonObjectSelector {
void set_rom(Rom* rom) {
rom_ = rom;
object_renderer_.SetROM(rom);
}
void SetRom(Rom* rom) {
rom_ = rom;
object_renderer_.SetROM(rom);
}
Rom* rom() const { return rom_; }
@@ -89,7 +87,7 @@ class DungeonObjectSelector {
Rom* rom_ = nullptr;
gui::Canvas room_gfx_canvas_{"##RoomGfxCanvas", ImVec2(0x100 + 1, 0x10 * 0x40 + 1)};
gui::Canvas object_canvas_;
zelda3::ObjectRenderer object_renderer_;
// ObjectRenderer removed - using ObjectDrawer in Room::RenderObjectsToBackground()
// Editor systems
std::unique_ptr<zelda3::DungeonEditorSystem>* dungeon_editor_system_ = nullptr;

View File

@@ -286,6 +286,10 @@ void Room::CopyRoomGraphicsToBuffer() {
void Room::RenderRoomGraphics() {
CopyRoomGraphicsToBuffer();
// Debug: Log floor graphics values
LOG_DEBUG("[RenderRoomGraphics]", "Room %d: floor1=%d, floor2=%d, blocks_size=%zu",
room_id_, floor1_graphics_, floor2_graphics_, blocks_.size());
// CRITICAL: Load graphics sheets into Arena with actual ROM data
LoadGraphicsSheetsIntoArena();

View File

@@ -286,8 +286,7 @@ class Room {
void SetStaircaseRoom(int index, uint8_t room) {
if (index >= 0 && index < 4) staircase_rooms_[index] = room;
}
void SetFloor1(uint8_t floor1) { this->floor1 = floor1; }
void SetFloor2(uint8_t floor2) { this->floor2 = floor2; }
// SetFloor1/SetFloor2 removed - use set_floor1()/set_floor2() instead (defined above)
void SetMessageId(uint16_t message_id) { message_id_ = message_id; }
// Getters for LoadRoomFromRom function
@@ -333,9 +332,21 @@ class Room {
uint8_t palette = 0;
uint8_t layout = 0;
uint8_t holewarp = 0;
uint8_t floor1 = 0;
uint8_t floor2 = 0;
// NOTE: floor1/floor2 removed - use floor1() and floor2() accessors instead
// Floor graphics are now private (floor1_graphics_, floor2_graphics_)
uint16_t message_id_ = 0;
// Floor graphics accessors (use these instead of direct members!)
uint8_t floor1() const { return floor1_graphics_; }
uint8_t floor2() const { return floor2_graphics_; }
void set_floor1(uint8_t value) {
floor1_graphics_ = value;
// TODO: Trigger re-render if needed
}
void set_floor2(uint8_t value) {
floor2_graphics_ = value;
// TODO: Trigger re-render if needed
}
// Enhanced object parsing methods
void ParseObjectsFromLocation(int objects_location);
void HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,

View File

@@ -551,7 +551,7 @@ absl::Status HandleDungeonDescribeRoomCommand(
room.blockset, room.spriteset, room.palette);
std::cout << absl::StrFormat(
" \"floors\": {\"primary\": %u, \"secondary\": %u},\n",
room.floor1, room.floor2);
room.floor1(), room.floor2());
std::cout << absl::StrFormat(
" \"message_id\": \"0x%03X\",\n", room.message_id_);
std::cout << absl::StrFormat(
@@ -625,7 +625,7 @@ absl::Status HandleDungeonDescribeRoomCommand(
room.blockset, room.spriteset, room.palette);
std::cout << absl::StrFormat(
" Floors → Main:%u Alt:%u Message ID:0x%03X Hole warp:0x%02X\n",
room.floor1, room.floor2, room.message_id_, room.holewarp);
room.floor1(), room.floor2(), room.message_id_, room.holewarp);
if (!stairs.empty()) {
std::cout << " Staircases:\n";
for (const auto& stair : stairs) {