From a71f1e02c9f44ae6450387ec6a34e41e109b525a Mon Sep 17 00:00:00 2001 From: scawful Date: Wed, 24 Sep 2025 23:39:50 -0400 Subject: [PATCH] Add Dungeon Room and Object Selection Features - Introduced new classes for DungeonRoomSelector and DungeonObjectSelector to enhance room and object management within the dungeon editor. - Implemented UI components for selecting rooms and entrances, allowing users to easily navigate and manage dungeon layouts. - Added functionality for rendering room graphics and object previews, improving the visual editing experience. - Updated the DungeonEditor class to integrate the new selectors, streamlining the overall editing workflow. - Enhanced error handling and validation in room and object management processes to ensure robust functionality. --- .../editor/dungeon/dungeon_canvas_viewer.cc | 415 +++++++++++ .../editor/dungeon/dungeon_canvas_viewer.h | 91 +++ src/app/editor/dungeon/dungeon_editor.cc | 490 +++++++------ src/app/editor/dungeon/dungeon_editor.h | 4 + .../editor/dungeon/dungeon_object_selector.cc | 672 ++++++++++++++++++ .../editor/dungeon/dungeon_object_selector.h | 82 +++ .../editor/dungeon/dungeon_room_selector.cc | 124 ++++ .../editor/dungeon/dungeon_room_selector.h | 55 ++ src/app/editor/editor.cmake | 3 + src/app/zelda3/dungeon/room.cc | 100 ++- src/app/zelda3/sprite/sprite.cc | 28 +- 11 files changed, 1819 insertions(+), 245 deletions(-) create mode 100644 src/app/editor/dungeon/dungeon_canvas_viewer.cc create mode 100644 src/app/editor/dungeon/dungeon_canvas_viewer.h create mode 100644 src/app/editor/dungeon/dungeon_object_selector.cc create mode 100644 src/app/editor/dungeon/dungeon_object_selector.h create mode 100644 src/app/editor/dungeon/dungeon_room_selector.cc create mode 100644 src/app/editor/dungeon/dungeon_room_selector.h diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc new file mode 100644 index 00000000..855e5059 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -0,0 +1,415 @@ +#include "dungeon_canvas_viewer.h" + +#include "absl/strings/str_format.h" +#include "app/core/window.h" +#include "app/gfx/arena.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_renderer.h" +#include "app/zelda3/dungeon/room.h" +#include "imgui/imgui.h" +#include "util/hex.h" + +namespace yaze::editor { + +using core::Renderer; + +using ImGui::BeginChild; +using ImGui::BeginTabBar; +using ImGui::BeginTabItem; +using ImGui::Button; +using ImGui::EndChild; +using ImGui::EndTabBar; +using ImGui::EndTabItem; +using ImGui::Separator; +using ImGui::Text; + +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(); +} + +void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { + // Validate room_id and ROM + if (room_id < 0 || room_id >= 128) { + ImGui::Text("Invalid room ID: %d", room_id); + return; + } + + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + return; + } + + ImGui::BeginGroup(); + + if (rooms_) { + gui::InputHexByte("Layout", &(*rooms_)[room_id].layout); + ImGui::SameLine(); + gui::InputHexByte("Blockset", &(*rooms_)[room_id].blockset); + ImGui::SameLine(); + gui::InputHexByte("Spriteset", &(*rooms_)[room_id].spriteset); + ImGui::SameLine(); + gui::InputHexByte("Palette", &(*rooms_)[room_id].palette); + + gui::InputHexByte("Floor1", &(*rooms_)[room_id].floor1); + ImGui::SameLine(); + gui::InputHexByte("Floor2", &(*rooms_)[room_id].floor2); + ImGui::SameLine(); + gui::InputHexWord("Message ID", &(*rooms_)[room_id].message_id_); + ImGui::SameLine(); + + if (Button("Load Room Graphics")) { + (void)LoadAndRenderRoomGraphics(room_id); + } + } + + ImGui::EndGroup(); + + canvas_.DrawBackground(); + canvas_.DrawContextMenu(); + + if (rooms_ && rom_->is_loaded()) { + auto& room = (*rooms_)[room_id]; + + // Automatically load room graphics if not already loaded + if (room.blocks().empty()) { + (void)LoadAndRenderRoomGraphics(room_id); + } + + // Load room objects if not already loaded + if (room.GetTileObjects().empty()) { + room.LoadObjects(); + } + + // Render background layers with proper positioning + RenderRoomBackgroundLayers(room_id); + + // Render room objects on top of background using the room's palette + if (current_palette_id_ < current_palette_group_.size()) { + auto room_palette = current_palette_group_[current_palette_id_]; + for (const auto& object : room.GetTileObjects()) { + RenderObjectInCanvas(object, room_palette); + } + } + } + + canvas_.DrawGrid(); + canvas_.DrawOverlay(); +} + +void DungeonCanvasViewer::RenderObjectInCanvas(const zelda3::RoomObject &object, + const gfx::SnesPalette &palette) { + // Validate ROM is loaded + if (!rom_ || !rom_->is_loaded()) { + return; + } + + // Create a mutable copy of the object to ensure tiles are loaded + auto mutable_object = object; + mutable_object.set_rom(rom_); + mutable_object.EnsureTilesLoaded(); + + // Check if tiles were loaded successfully + if (mutable_object.tiles().empty()) { + return; // Skip objects without tiles + } + + // Convert room coordinates to canvas coordinates using helper function + auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_); + + // Check if object is within canvas bounds (accounting for scrolling) + if (!IsWithinCanvasBounds(canvas_x, canvas_y, 32)) { + return; // Skip objects outside visible area + } + + // Render the object to a bitmap + auto render_result = object_renderer_.RenderObject(mutable_object, palette); + if (!render_result.ok()) { + return; // Skip if rendering failed + } + + auto object_bitmap = std::move(render_result.value()); + + // Set the palette for the bitmap + object_bitmap.SetPalette(palette); + + // Render the bitmap to a texture so it can be drawn + core::Renderer::Get().RenderBitmap(&object_bitmap); + + // Draw the object bitmap to the canvas + canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); +} + +void DungeonCanvasViewer::DisplayObjectInfo(const zelda3::RoomObject &object, + int canvas_x, int canvas_y) { + // Display object information as text overlay + std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_, + object.x_, object.y_, object.size_); + + // Draw text at the object position + canvas_.DrawText(info_text, canvas_x, canvas_y - 12); +} + +void DungeonCanvasViewer::RenderLayoutObjects(const zelda3::RoomLayout &layout, + const gfx::SnesPalette &palette) { + // Render layout objects (walls, floors, etc.) as simple colored rectangles + for (const auto &layout_obj : layout.GetObjects()) { + // Convert room coordinates to canvas coordinates using helper function + auto [canvas_x, canvas_y] = + RoomToCanvasCoordinates(layout_obj.x(), layout_obj.y()); + + // Check if layout object is within canvas bounds + if (!IsWithinCanvasBounds(canvas_x, canvas_y, 16)) { + continue; // Skip objects outside visible area + } + + // Choose color based on object type + gfx::SnesColor color; + switch (layout_obj.type()) { + case zelda3::RoomLayoutObject::Type::kWall: + color = gfx::SnesColor(0x7FFF); // Gray + break; + case zelda3::RoomLayoutObject::Type::kFloor: + color = gfx::SnesColor(0x4210); // Dark brown + break; + case zelda3::RoomLayoutObject::Type::kCeiling: + color = gfx::SnesColor(0x739C); // Light gray + break; + case zelda3::RoomLayoutObject::Type::kPit: + color = gfx::SnesColor(0x0000); // Black + break; + case zelda3::RoomLayoutObject::Type::kWater: + color = gfx::SnesColor(0x001F); // Blue + break; + case zelda3::RoomLayoutObject::Type::kStairs: + color = gfx::SnesColor(0x7E0F); // Yellow + break; + case zelda3::RoomLayoutObject::Type::kDoor: + color = gfx::SnesColor(0xF800); // Red + break; + default: + color = gfx::SnesColor(0x7C1F); // Magenta for unknown + break; + } + + // Draw a simple rectangle for the layout object + canvas_.DrawRect(canvas_x, canvas_y, 16, 16, + gui::ConvertSnesColorToImVec4(color)); + } +} + +// Coordinate conversion helper functions +std::pair DungeonCanvasViewer::RoomToCanvasCoordinates(int room_x, + int room_y) const { + // Convert room coordinates (16x16 tile units) to canvas coordinates (pixels) + return {room_x * 16, room_y * 16}; +} + +std::pair DungeonCanvasViewer::CanvasToRoomCoordinates(int canvas_x, + int canvas_y) const { + // Convert canvas coordinates (pixels) to room coordinates (16x16 tile units) + return {canvas_x / 16, canvas_y / 16}; +} + +bool DungeonCanvasViewer::IsWithinCanvasBounds(int canvas_x, int canvas_y, + int margin) const { + // Check if coordinates are within canvas bounds with optional margin + auto canvas_width = canvas_.width(); + auto canvas_height = canvas_.height(); + return (canvas_x >= -margin && canvas_y >= -margin && + canvas_x <= canvas_width + margin && + canvas_y <= canvas_height + margin); +} + +// Room graphics management methods +absl::Status DungeonCanvasViewer::LoadAndRenderRoomGraphics(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]; + + // Load room graphics with proper blockset + room.LoadRoomGraphics(room.blockset); + + // Load the room's palette with bounds checking + if (room.palette < rom_->paletteset_ids.size() && + !rom_->paletteset_ids[room.palette].empty()) { + auto dungeon_palette_ptr = rom_->paletteset_ids[room.palette][0]; + auto palette_id = rom_->ReadWord(0xDEC4B + dungeon_palette_ptr); + if (palette_id.ok()) { + current_palette_group_id_ = palette_id.value() / 180; + if (current_palette_group_id_ < rom_->palette_group().dungeon_main.size()) { + auto full_palette = rom_->palette_group().dungeon_main[current_palette_group_id_]; + ASSIGN_OR_RETURN(current_palette_group_, + gfx::CreatePaletteGroupFromLargePalette(full_palette)); + } + } + } + + // Render the room graphics to the graphics arena + room.RenderRoomGraphics(); + + // Update the background layers with proper palette + RETURN_IF_ERROR(UpdateRoomBackgroundLayers(room_id)); + + return absl::OkStatus(); +} + +absl::Status DungeonCanvasViewer::UpdateRoomBackgroundLayers(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); + core::Renderer::Get().UpdateBitmap(&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); + core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + } + } + } + } + + return absl::OkStatus(); +} + +void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { + if (room_id < 0 || room_id >= 128) { + return; + } + + if (!rom_ || !rom_->is_loaded()) { + return; + } + + if (!rooms_) { + return; + } + + // Get canvas dimensions to limit rendering + int canvas_width = canvas_.width(); + int canvas_height = canvas_.height(); + + // Validate canvas dimensions + if (canvas_width <= 0 || canvas_height <= 0) { + return; + } + + // Render the room's background layers using the graphics arena + // BG1 (background layer 1) - main room graphics + auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap(); + if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) { + // Scale the background to fit the canvas + float scale_x = static_cast(canvas_width) / bg1_bitmap.width(); + float scale_y = static_cast(canvas_height) / bg1_bitmap.height(); + float scale = std::min(scale_x, scale_y); + + int scaled_width = static_cast(bg1_bitmap.width() * scale); + int scaled_height = static_cast(bg1_bitmap.height() * scale); + int offset_x = (canvas_width - scaled_width) / 2; + int offset_y = (canvas_height - scaled_height) / 2; + + canvas_.DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255); + } + + // BG2 (background layer 2) - sprite graphics (overlay) + auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap(); + if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) { + // Scale the background to fit the canvas + float scale_x = static_cast(canvas_width) / bg2_bitmap.width(); + float scale_y = static_cast(canvas_height) / bg2_bitmap.height(); + float scale = std::min(scale_x, scale_y); + + int scaled_width = static_cast(bg2_bitmap.width() * scale); + int scaled_height = static_cast(bg2_bitmap.height() * scale); + int offset_x = (canvas_width - scaled_width) / 2; + int offset_y = (canvas_height - scaled_height) / 2; + + canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200); // Semi-transparent overlay + } +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.h b/src/app/editor/dungeon/dungeon_canvas_viewer.h new file mode 100644 index 00000000..16c095a4 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.h @@ -0,0 +1,91 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_CANVAS_VIEWER_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_CANVAS_VIEWER_H + +#include "app/gui/canvas.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_renderer.h" +#include "app/zelda3/dungeon/room.h" +#include "app/gfx/snes_palette.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles the main dungeon canvas rendering and interaction + */ +class DungeonCanvasViewer { + public: + explicit DungeonCanvasViewer(Rom* rom = nullptr) : rom_(rom), object_renderer_(rom) {} + + void DrawDungeonTabView(); + void DrawDungeonCanvas(int room_id); + + void set_rom(Rom* rom) { + rom_ = rom; + object_renderer_.SetROM(rom); + } + Rom* rom() const { return rom_; } + + // Room data access + void set_rooms(std::array* rooms) { rooms_ = rooms; } + void set_active_rooms(const ImVector& rooms) { active_rooms_ = rooms; } + void set_current_active_room_tab(int tab) { current_active_room_tab_ = tab; } + + // Palette access + void set_current_palette_group_id(uint64_t id) { current_palette_group_id_ = id; } + void set_current_palette_id(uint64_t id) { current_palette_id_ = id; } + void set_current_palette_group(const gfx::PaletteGroup& group) { current_palette_group_ = group; } + + // Canvas access + gui::Canvas& canvas() { return canvas_; } + const gui::Canvas& canvas() const { return canvas_; } + + private: + void RenderObjectInCanvas(const zelda3::RoomObject &object, + const gfx::SnesPalette &palette); + void DisplayObjectInfo(const zelda3::RoomObject &object, int canvas_x, + int canvas_y); + void RenderLayoutObjects(const zelda3::RoomLayout &layout, + const gfx::SnesPalette &palette); + + // Coordinate conversion helpers + std::pair RoomToCanvasCoordinates(int room_x, int room_y) const; + std::pair CanvasToRoomCoordinates(int canvas_x, int canvas_y) const; + bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin = 32) const; + + // Room graphics management + absl::Status LoadAndRenderRoomGraphics(int room_id); + absl::Status UpdateRoomBackgroundLayers(int room_id); + void RenderRoomBackgroundLayers(int room_id); + + Rom* rom_ = nullptr; + gui::Canvas canvas_{"##DungeonCanvas", ImVec2(0x200, 0x200)}; + zelda3::ObjectRenderer object_renderer_; + + // Room data + std::array* rooms_ = nullptr; + ImVector active_rooms_; + int current_active_room_tab_ = 0; + + // Palette data + uint64_t current_palette_group_id_ = 0; + uint64_t current_palette_id_ = 0; + gfx::PaletteGroup current_palette_group_; + + // Object rendering cache + struct ObjectRenderCache { + int object_id; + int object_x, object_y, object_size; + uint64_t palette_hash; + gfx::Bitmap rendered_bitmap; + bool is_valid; + }; + std::vector object_render_cache_; + uint64_t last_palette_hash_ = 0; +}; + +} // namespace editor +} // namespace yaze + +#endif diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index 0cf65c59..1d72ee9b 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -91,7 +91,7 @@ absl::Status DungeonEditor::Load() { gfx::CreatePaletteGroupFromLargePalette(full_palette_)); CalculateUsageStats(); - + // Initialize the new editor system if (dungeon_editor_system_) { auto status = dungeon_editor_system_->Initialize(); @@ -99,7 +99,7 @@ absl::Status DungeonEditor::Load() { return status; } } - + is_loaded_ = true; return absl::OkStatus(); } @@ -115,7 +115,7 @@ absl::Status DungeonEditor::Update() { status_ = UpdateDungeonRoomView(); ImGui::EndTabItem(); } - + if (ImGui::BeginTabItem("Usage Statistics")) { if (is_loaded_) { DrawUsageStats(); @@ -221,15 +221,16 @@ absl::Status DungeonEditor::UpdateDungeonRoomView() { ImGui::End(); } - if (BeginTable("#DungeonEditTable", 4, kDungeonTableFlags, ImVec2(0, 0))) { - TableSetupColumn("Room Selector"); + // Simplified 3-column layout: Room/Entrance Selector | Canvas | Object Selector/Editor + if (BeginTable("#DungeonEditTable", 3, kDungeonTableFlags, ImVec2(0, 0))) { + TableSetupColumn("Room/Entrance Selector", ImGuiTableColumnFlags_WidthFixed, 250); TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch, ImGui::GetContentRegionAvail().x); - TableSetupColumn("Editing Panels"); - TableSetupColumn("Object Selector"); + TableSetupColumn("Object Selector/Editor", ImGuiTableColumnFlags_WidthFixed, 300); TableHeadersRow(); TableNextRow(); + // Column 1: Room and Entrance Selector TableNextColumn(); if (ImGui::BeginTabBar("##DungeonRoomTabBar")) { TAB_ITEM("Rooms"); @@ -241,14 +242,22 @@ absl::Status DungeonEditor::UpdateDungeonRoomView() { ImGui::EndTabBar(); } + // Column 2: Main Canvas TableNextColumn(); DrawDungeonTabView(); + // Column 3: Object Selector and Editor TableNextColumn(); - DrawIntegratedEditingPanels(); - - TableNextColumn(); - DrawTileSelector(); + if (ImGui::BeginTabBar("##ObjectEditorTabBar")) { + TAB_ITEM("Graphics"); + DrawTileSelector(); + END_TAB_ITEM(); + TAB_ITEM("Editor"); + DrawIntegratedEditingPanels(); + END_TAB_ITEM(); + ImGui::EndTabBar(); + } + ImGui::EndTable(); } return absl::OkStatus(); @@ -489,6 +498,7 @@ void DungeonEditor::DrawDungeonTabView() { if (BeginTabItem(zelda3::kRoomNames[active_rooms_[n]].data(), &open, ImGuiTabItemFlags_None)) { + current_active_room_tab_ = n; // Track which tab is currently active DrawDungeonCanvas(active_rooms_[n]); EndTabItem(); } @@ -505,6 +515,17 @@ void DungeonEditor::DrawDungeonTabView() { } void DungeonEditor::DrawDungeonCanvas(int room_id) { + // Validate room_id and ROM + if (room_id < 0 || room_id >= rooms_.size()) { + ImGui::Text("Invalid room ID: %d", room_id); + return; + } + + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + return; + } + ImGui::BeginGroup(); gui::InputHexByte("Layout", &rooms_[room_id].layout); @@ -536,30 +557,38 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { (void)ReloadAllRoomGraphics(); } - static bool show_objects = false; - ImGui::Checkbox("Show Object Outlines", &show_objects); - - static bool render_objects = true; - ImGui::Checkbox("Render Objects", &render_objects); - - static bool show_object_info = false; - ImGui::Checkbox("Show Object Info", &show_object_info); - - static bool show_layout_objects = false; - ImGui::Checkbox("Show Layout Objects", &show_layout_objects); - - static bool show_debug_info = false; - ImGui::Checkbox("Show Debug Info", &show_debug_info); - - if (ImGui::Button("Clear Object Cache")) { - object_render_cache_.clear(); + // Debug and control popup + static bool show_debug_popup = false; + if (ImGui::Button("Room Debug Info")) { + show_debug_popup = true; } - ImGui::SameLine(); - ImGui::Text("Cache: %zu objects", object_render_cache_.size()); + if (show_debug_popup) { + ImGui::OpenPopup("Room Debug Info"); + show_debug_popup = false; + } - // Object statistics and metadata - if (show_debug_info) { + if (ImGui::BeginPopupModal("Room Debug Info", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + static bool show_objects = false; + ImGui::Checkbox("Show Object Outlines", &show_objects); + + static bool render_objects = true; + ImGui::Checkbox("Render Objects", &render_objects); + + static bool show_object_info = false; + ImGui::Checkbox("Show Object Info", &show_object_info); + + static bool show_layout_objects = false; + ImGui::Checkbox("Show Layout Objects", &show_layout_objects); + + if (ImGui::Button("Clear Object Cache")) { + object_render_cache_.clear(); + } + + ImGui::SameLine(); + ImGui::Text("Cache: %zu objects", object_render_cache_.size()); + + // Object statistics and metadata ImGui::Separator(); ImGui::Text("Room Statistics:"); ImGui::Text("Objects: %zu", rooms_[room_id].GetTileObjects().size()); @@ -595,39 +624,44 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { ImGui::Text("Walls: %zu", walls.size()); ImGui::Text("Floors: %zu", floors.size()); ImGui::Text("Doors: %zu", doors.size()); - } - // Object selection and editing - static int selected_object_id = -1; - if (ImGui::Button("Select Object")) { - // This would open an object selection dialog - // For now, just cycle through objects - if (!rooms_[room_id].GetTileObjects().empty()) { - selected_object_id = - (selected_object_id + 1) % rooms_[room_id].GetTileObjects().size(); + // Object selection and editing + static int selected_object_id = -1; + if (ImGui::Button("Select Object")) { + // This would open an object selection dialog + // For now, just cycle through objects + if (!rooms_[room_id].GetTileObjects().empty()) { + selected_object_id = + (selected_object_id + 1) % rooms_[room_id].GetTileObjects().size(); + } } - } - if (selected_object_id >= 0 && - selected_object_id < (int)rooms_[room_id].GetTileObjects().size()) { - const auto &selected_obj = - rooms_[room_id].GetTileObjects()[selected_object_id]; - ImGui::Separator(); - ImGui::Text("Selected Object:"); - ImGui::Text("ID: 0x%02X", selected_obj.id_); - ImGui::Text("Position: (%d, %d)", selected_obj.x_, selected_obj.y_); - ImGui::Text("Size: 0x%02X", selected_obj.size_); - ImGui::Text("Layer: %d", static_cast(selected_obj.layer_)); - ImGui::Text("Tile Count: %d", selected_obj.GetTileCount()); + if (selected_object_id >= 0 && + selected_object_id < (int)rooms_[room_id].GetTileObjects().size()) { + const auto &selected_obj = + rooms_[room_id].GetTileObjects()[selected_object_id]; + ImGui::Separator(); + ImGui::Text("Selected Object:"); + ImGui::Text("ID: 0x%02X", selected_obj.id_); + ImGui::Text("Position: (%d, %d)", selected_obj.x_, selected_obj.y_); + ImGui::Text("Size: 0x%02X", selected_obj.size_); + ImGui::Text("Layer: %d", static_cast(selected_obj.layer_)); + ImGui::Text("Tile Count: %d", selected_obj.GetTileCount()); - // Object editing controls - if (ImGui::Button("Edit Object")) { - // This would open an object editing dialog + // Object editing controls + if (ImGui::Button("Edit Object")) { + // This would open an object editing dialog + } + ImGui::SameLine(); + if (ImGui::Button("Delete Object")) { + // This would remove the object from the room + } } - ImGui::SameLine(); - if (ImGui::Button("Delete Object")) { - // This would remove the object from the room + + if (ImGui::Button("Close")) { + ImGui::CloseCurrentPopup(); } + ImGui::EndPopup(); } ImGui::EndGroup(); @@ -640,77 +674,19 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { (void)LoadAndRenderRoomGraphics(room_id); } + // Load room objects if not already loaded + if (rooms_[room_id].GetTileObjects().empty()) { + rooms_[room_id].LoadObjects(); + } + // Render background layers with proper positioning RenderRoomBackgroundLayers(room_id); - // Render room objects on top of background - for (const auto& object : rooms_[room_id].GetTileObjects()) { - RenderObjectInCanvas(object, current_palette_group_[current_palette_id_]); - } - - if (show_objects || render_objects) { - // Get the current room's palette for object rendering - auto current_palette = - rom()->palette_group().dungeon_main[current_palette_group_id_]; - - // Clear cache if palette changed - uint64_t current_palette_hash = 0; - for (size_t i = 0; i < current_palette.size() && i < 16; ++i) { - current_palette_hash ^= - std::hash{}(current_palette[i].snes()) + 0x9e3779b9 + - (current_palette_hash << 6) + (current_palette_hash >> 2); - } - - if (current_palette_hash != last_palette_hash_) { - object_render_cache_.clear(); - last_palette_hash_ = current_palette_hash; - } - - // Render layout objects (walls, floors, etc.) first - if (show_layout_objects) { - RenderLayoutObjects(rooms_[room_id].GetLayout(), current_palette); - } - - if (rooms_[room_id].GetTileObjects().empty()) { - // Load the objects for the room - rooms_[room_id].LoadObjects(); - } - - // Render regular room objects - for (const auto &object : rooms_[room_id].GetTileObjects()) { - // Convert room coordinates to canvas coordinates using helper function - auto [canvas_x, canvas_y] = - RoomToCanvasCoordinates(object.x_, object.y_); - - if (show_objects) { - // Draw object outline - use size_ to determine dimensions - int outline_width = 16; // Default 16x16 - int outline_height = 16; - - // Calculate dimensions based on object size - if (object.size_ > 0) { - // Size encoding: bits 0-1 = width, bits 2-3 = height - int width_bits = object.size_ & 0x03; - int height_bits = (object.size_ >> 2) & 0x03; - - outline_width = (width_bits + 1) * 16; - outline_height = (height_bits + 1) * 16; - } - - // Use canvas coordinates for outline - canvas_.DrawOutline(canvas_x, canvas_y, outline_width, - outline_height); - } - - if (render_objects) { - // Render the actual object using ObjectRenderer - RenderObjectInCanvas(object, current_palette); - } - - if (show_object_info) { - // Display object information - DisplayObjectInfo(object, canvas_x, canvas_y); - } + // Render room objects on top of background using the room's palette + if (current_palette_id_ < current_palette_group_.size()) { + auto room_palette = current_palette_group_[current_palette_id_]; + for (const auto& object : rooms_[room_id].GetTileObjects()) { + RenderObjectInCanvas(object, room_palette); } } } @@ -720,6 +696,11 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { void DungeonEditor::RenderObjectInCanvas(const zelda3::RoomObject &object, const gfx::SnesPalette &palette) { + // Validate ROM is loaded + if (!rom_ || !rom_->is_loaded()) { + return; + } + // Create a mutable copy of the object to ensure tiles are loaded auto mutable_object = object; mutable_object.set_rom(rom_); @@ -859,21 +840,52 @@ void DungeonEditor::DrawRoomGraphics() { room_gfx_canvas_.DrawContextMenu(); room_gfx_canvas_.DrawTileSelector(32); if (is_loaded_) { - auto blocks = rooms_[current_room_id_].blocks(); + // Use the currently active room tab + int active_room_id = 0; + if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) { + active_room_id = active_rooms_[current_active_room_tab_]; + } + + auto blocks = rooms_[active_room_id].blocks(); + + // Load graphics for this room if not already loaded + if (blocks.empty()) { + (void)LoadAndRenderRoomGraphics(active_room_id); + blocks = rooms_[active_room_id].blocks(); + } + int current_block = 0; + const int max_blocks_per_row = 4; // Limit blocks per row to fit canvas + const int block_width = 0x100; // 256 pixels per block + const int block_height = 0x40; // 64 pixels per block + for (int block : blocks) { - int offset = height * (current_block + 1); - int top_left_y = room_gfx_canvas_.zero_point().y + 2; - if (current_block >= 1) { - top_left_y = room_gfx_canvas_.zero_point().y + height * current_block; + if (current_block >= 16) break; // Only show first 16 blocks + + // Ensure the graphics sheet is loaded and has a valid texture + if (block < gfx::Arena::Get().gfx_sheets().size()) { + auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; + + // Calculate position in a grid layout instead of horizontal concatenation + int row = current_block / max_blocks_per_row; + int col = current_block % max_blocks_per_row; + + int x = room_gfx_canvas_.zero_point().x + 2 + (col * block_width); + int y = room_gfx_canvas_.zero_point().y + 2 + (row * block_height); + + // Ensure we don't exceed canvas bounds + if (x + block_width <= room_gfx_canvas_.zero_point().x + room_gfx_canvas_.width() && + y + block_height <= room_gfx_canvas_.zero_point().y + room_gfx_canvas_.height()) { + + // Only draw if the texture is valid + if (gfx_sheet.texture() != 0) { + room_gfx_canvas_.draw_list()->AddImage( + (ImTextureID)(intptr_t)gfx_sheet.texture(), + ImVec2(x, y), + ImVec2(x + block_width, y + block_height)); + } + } } - room_gfx_canvas_.draw_list()->AddImage( - (ImTextureID)(intptr_t)gfx::Arena::Get() - .gfx_sheets()[block] - .texture(), - ImVec2(room_gfx_canvas_.zero_point().x + 2, top_left_y), - ImVec2(room_gfx_canvas_.zero_point().x + 0x100, - room_gfx_canvas_.zero_point().y + offset)); current_block += 1; } } @@ -951,9 +963,9 @@ void DungeonEditor::DrawObjectRenderer() { // Render object preview if available if (object_loaded_ && preview_object_.id_ >= 0) { - // Render preview object at center of canvas - int preview_x = 8 * 16; // Center horizontally (8 tiles * 16 pixels) - int preview_y = 8 * 16; // Center vertically + // Render preview object at center of canvas (object_canvas_ is 256x256) + int preview_x = 128 - 16; // Center horizontally (256/2 - 16 for 32x32 object) + int preview_y = 128 - 16; // Center vertically auto preview_result = object_renderer_.RenderObject(preview_object_, preview_palette_); @@ -1283,7 +1295,7 @@ void DungeonEditor::DrawObjectEditor() { // Object count and selection info ImGui::Separator(); ImGui::Text("Objects: %zu", object_editor_->GetObjectCount()); - + auto selection = object_editor_->GetSelection(); if (!selection.selected_objects.empty()) { ImGui::Text("Selected: %zu objects", selection.selected_objects.size()); @@ -1309,11 +1321,11 @@ void DungeonEditor::DrawSpriteEditor() { // Display current room sprites auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(current_room); - + if (sprites_result.ok()) { auto sprites = sprites_result.value(); ImGui::Text("Sprites in room %d: %zu", current_room, sprites.size()); - + for (const auto &sprite : sprites) { ImGui::Text("ID: %d, Type: %d, Position: (%d, %d)", sprite.sprite_id, static_cast(sprite.type), sprite.x, sprite.y); @@ -1343,7 +1355,7 @@ void DungeonEditor::DrawSpriteEditor() { sprite_data.x = new_sprite_x; sprite_data.y = new_sprite_y; sprite_data.layer = new_sprite_layer; - + auto status = dungeon_editor_system_->AddSprite(sprite_data); if (!status.ok()) { ImGui::Text("Error adding sprite: %s", status.message().data()); @@ -1363,13 +1375,13 @@ void DungeonEditor::DrawItemEditor() { // Display current room items auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto items_result = dungeon_editor_system_->GetItemsByRoom(current_room); - + if (items_result.ok()) { auto items = items_result.value(); ImGui::Text("Items in room %d: %zu", current_room, items.size()); - + for (const auto &item : items) { - ImGui::Text("ID: %d, Type: %d, Position: (%d, %d), Hidden: %s", + ImGui::Text("ID: %d, Type: %d, Position: (%d, %d), Hidden: %s", item.item_id, static_cast(item.type), item.x, item.y, item.is_hidden ? "Yes" : "No"); } @@ -1399,7 +1411,7 @@ void DungeonEditor::DrawItemEditor() { item_data.y = new_item_y; item_data.room_id = current_room; item_data.is_hidden = new_item_hidden; - + auto status = dungeon_editor_system_->AddItem(item_data); if (!status.ok()) { ImGui::Text("Error adding item: %s", status.message().data()); @@ -1420,16 +1432,16 @@ void DungeonEditor::DrawEntranceEditor() { auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(current_room); - + if (entrances_result.ok()) { auto entrances = entrances_result.value(); ImGui::Text("Entrances in room %d: %zu", current_room, entrances.size()); - + for (const auto &entrance : entrances) { ImGui::Text( "ID: %d, Type: %d, Target Room: %d, Target Position: (%d, %d)", - entrance.entrance_id, static_cast(entrance.type), - entrance.target_room_id, entrance.target_x, entrance.target_y); + entrance.entrance_id, static_cast(entrance.type), + entrance.target_room_id, entrance.target_x, entrance.target_y); } } else { ImGui::Text("Error loading entrances: %s", @@ -1472,13 +1484,13 @@ void DungeonEditor::DrawDoorEditor() { // Display current room doors auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto doors_result = dungeon_editor_system_->GetDoorsByRoom(current_room); - + if (doors_result.ok()) { auto doors = doors_result.value(); ImGui::Text("Doors in room %d: %zu", current_room, doors.size()); - + for (const auto &door : doors) { - ImGui::Text("ID: %d, Position: (%d, %d), Direction: %d, Target Room: %d", + ImGui::Text("ID: %d, Position: (%d, %d), Direction: %d, Target Room: %d", door.door_id, door.x, door.y, door.direction, door.target_room_id); } @@ -1522,7 +1534,7 @@ void DungeonEditor::DrawDoorEditor() { door_data.is_locked = door_locked; door_data.requires_key = door_requires_key; door_data.key_type = door_key_type; - + auto status = dungeon_editor_system_->AddDoor(door_data); if (!status.ok()) { ImGui::Text("Error adding door: %s", status.message().data()); @@ -1542,14 +1554,14 @@ void DungeonEditor::DrawChestEditor() { // Display current room chests auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto chests_result = dungeon_editor_system_->GetChestsByRoom(current_room); - + if (chests_result.ok()) { auto chests = chests_result.value(); ImGui::Text("Chests in room %d: %zu", current_room, chests.size()); - + for (const auto &chest : chests) { - ImGui::Text("ID: %d, Position: (%d, %d), Big: %s, Item: %d, Quantity: %d", - chest.chest_id, chest.x, chest.y, + ImGui::Text("ID: %d, Position: (%d, %d), Big: %s, Item: %d, Quantity: %d", + chest.chest_id, chest.x, chest.y, chest.is_big_chest ? "Yes" : "No", chest.item_id, chest.item_quantity); } @@ -1581,7 +1593,7 @@ void DungeonEditor::DrawChestEditor() { chest_data.is_big_chest = chest_big; chest_data.item_id = chest_item_id; chest_data.item_quantity = chest_item_quantity; - + auto status = dungeon_editor_system_->AddChest(chest_data); if (!status.ok()) { ImGui::Text("Error adding chest: %s", status.message().data()); @@ -1971,10 +1983,10 @@ void DungeonEditor::DrawCompactPropertiesEditor() { auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto properties_result = dungeon_editor_system_->GetRoomProperties(current_room); - + if (properties_result.ok()) { auto properties = properties_result.value(); - + static char room_name[128] = {0}; static int dungeon_id = 0; static int floor_level = 0; @@ -2006,7 +2018,7 @@ void DungeonEditor::DrawCompactPropertiesEditor() { new_properties.is_boss_room = is_boss_room; new_properties.is_save_room = is_save_room; new_properties.music_id = music_id; - + auto status = dungeon_editor_system_->SetRoomProperties(current_room, new_properties); if (!status.ok()) { @@ -2020,7 +2032,7 @@ void DungeonEditor::DrawCompactPropertiesEditor() { // Dungeon settings summary ImGui::Separator(); ImGui::Text("Dungeon Settings"); - + auto dungeon_settings_result = dungeon_editor_system_->GetDungeonSettings(); if (dungeon_settings_result.ok()) { auto settings = dungeon_settings_result.value(); @@ -2060,11 +2072,30 @@ absl::Status DungeonEditor::LoadAndRenderRoomGraphics(int room_id) { return absl::InvalidArgumentError("Invalid room ID"); } + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + auto& room = rooms_[room_id]; // Load room graphics with proper blockset (void)room.LoadRoomGraphics(room.blockset); + // Load the room's palette with bounds checking + if (room.palette < rom()->paletteset_ids.size() && + !rom()->paletteset_ids[room.palette].empty()) { + auto dungeon_palette_ptr = rom()->paletteset_ids[room.palette][0]; + auto palette_id = rom()->ReadWord(0xDEC4B + dungeon_palette_ptr); + if (palette_id.ok()) { + current_palette_group_id_ = palette_id.value() / 180; + if (current_palette_group_id_ < rom()->palette_group().dungeon_main.size()) { + full_palette_ = rom()->palette_group().dungeon_main[current_palette_group_id_]; + ASSIGN_OR_RETURN(current_palette_group_, + gfx::CreatePaletteGroupFromLargePalette(full_palette_)); + } + } + } + // Render the room graphics to the graphics arena (void)room.RenderRoomGraphics(); @@ -2091,8 +2122,17 @@ absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int room_id) { return absl::InvalidArgumentError("Invalid room ID"); } + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + 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_]; @@ -2100,10 +2140,12 @@ absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int room_id) { if (room.blocks().size() >= 8) { for (int i = 0; i < 8; i++) { int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( - current_palette_group_[current_palette_id_], 0); - core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + 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); + core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + } } } } @@ -2111,12 +2153,14 @@ absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int room_id) { // Update BG2 (background layer 2) with sprite auxiliary palette if (room.blocks().size() >= 16) { auto sprites_aux1_pal_group = rom()->palette_group().sprites_aux1; - for (int i = 8; i < 16; i++) { - int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( - sprites_aux1_pal_group[current_palette_id_], 0); - core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + 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); + core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + } } } } @@ -2129,60 +2173,52 @@ void DungeonEditor::RenderRoomBackgroundLayers(int room_id) { return; } - auto& room = rooms_[room_id]; - - // Render BG1 (background layer 1) - main room graphics - if (room.blocks().size() >= 8) { - // Calculate the total size of all BG1 blocks - int bg1_width = 0; - int bg1_height = 0; - - for (int i = 0; i < 8; i++) { - int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; - bg1_width += gfx_sheet.width(); - bg1_height = std::max(bg1_height, gfx_sheet.height()); - } - } - - // Render BG1 blocks in sequence - int current_x = 0; - for (int i = 0; i < 8; i++) { - int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; - canvas_.DrawBitmap(gfx_sheet, current_x, 0, 1.0f, 255); - current_x += gfx_sheet.width(); - } - } + if (!rom_ || !rom_->is_loaded()) { + return; } - // Render BG2 (background layer 2) - sprite graphics - if (room.blocks().size() >= 16) { - // Calculate the total size of all BG2 blocks - int bg2_width = 0; - int bg2_height = 0; + auto& room = rooms_[room_id]; + + // Get canvas dimensions to limit rendering + int canvas_width = canvas_.width(); + int canvas_height = canvas_.height(); + + // Validate canvas dimensions + if (canvas_width <= 0 || canvas_height <= 0) { + return; + } + + // Render the room's background layers using the graphics arena + // BG1 (background layer 1) - main room graphics + auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap(); + if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) { + // Scale the background to fit the canvas + float scale_x = static_cast(canvas_width) / bg1_bitmap.width(); + float scale_y = static_cast(canvas_height) / bg1_bitmap.height(); + float scale = std::min(scale_x, scale_y); - for (int i = 8; i < 16; i++) { - int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; - bg2_width += gfx_sheet.width(); - bg2_height = std::max(bg2_height, gfx_sheet.height()); - } - } + int scaled_width = static_cast(bg1_bitmap.width() * scale); + int scaled_height = static_cast(bg1_bitmap.height() * scale); + int offset_x = (canvas_width - scaled_width) / 2; + int offset_y = (canvas_height - scaled_height) / 2; - // Render BG2 blocks in sequence (overlay on BG1) - int current_x = 0; - for (int i = 8; i < 16; i++) { - int block = room.blocks()[i]; - if (block < gfx::Arena::Get().gfx_sheets().size()) { - auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; - canvas_.DrawBitmap(gfx_sheet, current_x, 0, 1.0f, 200); // Semi-transparent - current_x += gfx_sheet.width(); - } - } + canvas_.DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255); + } + + // BG2 (background layer 2) - sprite graphics (overlay) + auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap(); + if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) { + // Scale the background to fit the canvas + float scale_x = static_cast(canvas_width) / bg2_bitmap.width(); + float scale_y = static_cast(canvas_height) / bg2_bitmap.height(); + float scale = std::min(scale_x, scale_y); + + int scaled_width = static_cast(bg2_bitmap.width() * scale); + int scaled_height = static_cast(bg2_bitmap.height() * scale); + int offset_x = (canvas_width - scaled_width) / 2; + int offset_y = (canvas_height - scaled_height) / 2; + + canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200); // Semi-transparent overlay } } diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index 5bde2f6d..7faa0d0b 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -14,6 +14,9 @@ #include "zelda3/dungeon/room.h" #include "zelda3/dungeon/room_entrance.h" #include "zelda3/dungeon/room_object.h" +#include "dungeon_room_selector.h" +#include "dungeon_canvas_viewer.h" +#include "dungeon_object_selector.h" namespace yaze { namespace editor { @@ -190,6 +193,7 @@ class DungeonEditor : public Editor { uint64_t current_palette_group_id_ = 0; ImVector active_rooms_; + int current_active_room_tab_ = 0; // Track which room tab is currently active GfxGroupEditor gfx_group_editor_; PaletteEditor palette_editor_; diff --git a/src/app/editor/dungeon/dungeon_object_selector.cc b/src/app/editor/dungeon/dungeon_object_selector.cc new file mode 100644 index 00000000..3670baef --- /dev/null +++ b/src/app/editor/dungeon/dungeon_object_selector.cc @@ -0,0 +1,672 @@ +#include "dungeon_object_selector.h" + +#include "absl/strings/str_format.h" +#include "app/core/window.h" +#include "app/gfx/arena.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_renderer.h" +#include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/dungeon_editor_system.h" +#include "app/zelda3/dungeon/dungeon_object_editor.h" +#include "imgui/imgui.h" +#include "util/hex.h" + +namespace yaze::editor { + +using core::Renderer; + +using ImGui::BeginChild; +using ImGui::BeginTabBar; +using ImGui::BeginTabItem; +using ImGui::Button; +using ImGui::EndChild; +using ImGui::EndTabBar; +using ImGui::EndTabItem; +using ImGui::Separator; +using ImGui::Text; + +void DungeonObjectSelector::DrawTileSelector() { + if (ImGui::BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) { + if (ImGui::BeginTabItem("Room Graphics")) { + if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)3); + BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + DrawRoomGraphics(); + } + EndChild(); + EndTabItem(); + } + + if (ImGui::BeginTabItem("Object Renderer")) { + DrawObjectRenderer(); + EndTabItem(); + } + EndTabBar(); + } +} + +void DungeonObjectSelector::DrawObjectRenderer() { + if (ImGui::BeginTable("DungeonObjectEditorTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV, ImVec2(0, 0))) { + ImGui::TableSetupColumn("Dungeon Objects", ImGuiTableColumnFlags_WidthStretch, ImGui::GetContentRegionAvail().x); + ImGui::TableSetupColumn("Canvas"); + + ImGui::TableNextColumn(); + BeginChild("DungeonObjectButtons", ImVec2(250, 0), true); + + static int selected_object = 0; + int i = 0; + for (const auto object_name : zelda3::Type1RoomObjectNames) { + if (ImGui::Selectable(object_name.data(), selected_object == i)) { + selected_object = i; + + // Create a test object and render it + auto test_object = zelda3::RoomObject(i, 8, 8, 0x12, 0); // Center in canvas + test_object.set_rom(rom_); + test_object.EnsureTilesLoaded(); + + // Get current palette + if (rom_ && rom_->is_loaded()) { + auto palette = rom_->palette_group().dungeon_main[current_palette_group_id_]; + + // Render object preview + auto result = object_renderer_.GetObjectPreview(test_object, palette); + if (result.ok()) { + object_loaded_ = true; + preview_object_ = test_object; // Store for rendering + preview_palette_ = palette; + } + } + } + i += 1; + } + + EndChild(); + + // Right side of the table - Canvas + ImGui::TableNextColumn(); + BeginChild("DungeonObjectCanvas", ImVec2(276, 0x10 * 0x40 + 1), true); + + object_canvas_.DrawBackground(ImVec2(256 + 1, 0x10 * 0x40 + 1)); + object_canvas_.DrawContextMenu(); + object_canvas_.DrawTileSelector(32); + object_canvas_.DrawGrid(32.0f); + + // Render object preview if available + if (object_loaded_ && preview_object_.id_ >= 0) { + // Render preview object at center of canvas (object_canvas_ is 256x256) + int preview_x = 128 - 16; // Center horizontally (256/2 - 16 for 32x32 object) + 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()); + preview_bitmap.SetPalette(preview_palette_); + core::Renderer::Get().RenderBitmap(&preview_bitmap); + object_canvas_.DrawBitmap(preview_bitmap, preview_x, preview_y, 1.0f, 255); + } + } + + object_canvas_.DrawOverlay(); + + EndChild(); + ImGui::EndTable(); + } + + if (object_loaded_) { + ImGui::Begin("Object Preview", &object_loaded_, 0); + ImGui::Text("Object ID: 0x%02X", preview_object_.id_); + ImGui::Text("Position: (%d, %d)", preview_object_.x_, preview_object_.y_); + ImGui::Text("Size: 0x%02X", preview_object_.size_); + ImGui::Text("Layer: %d", static_cast(preview_object_.layer_)); + ImGui::End(); + } +} + +void DungeonObjectSelector::DrawRoomGraphics() { + const auto height = 0x40; + room_gfx_canvas_.DrawBackground(); + room_gfx_canvas_.DrawContextMenu(); + room_gfx_canvas_.DrawTileSelector(32); + + if (rom_ && rom_->is_loaded() && rooms_) { + int active_room_id = current_room_id_; + auto& room = (*rooms_)[active_room_id]; + auto blocks = room.blocks(); + + // Load graphics for this room if not already loaded + if (blocks.empty()) { + room.LoadRoomGraphics(room.blockset); + blocks = room.blocks(); + } + + int current_block = 0; + const int max_blocks_per_row = 4; // Limit blocks per row to fit canvas + const int block_width = 0x100; // 256 pixels per block + const int block_height = 0x40; // 64 pixels per block + + for (int block : blocks) { + if (current_block >= 16) break; // Only show first 16 blocks + + // Ensure the graphics sheet is loaded and has a valid texture + if (block < gfx::Arena::Get().gfx_sheets().size()) { + auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; + + // Calculate position in a grid layout instead of horizontal concatenation + int row = current_block / max_blocks_per_row; + int col = current_block % max_blocks_per_row; + + int x = room_gfx_canvas_.zero_point().x + 2 + (col * block_width); + int y = room_gfx_canvas_.zero_point().y + 2 + (row * block_height); + + // Ensure we don't exceed canvas bounds + if (x + block_width <= room_gfx_canvas_.zero_point().x + room_gfx_canvas_.width() && + y + block_height <= room_gfx_canvas_.zero_point().y + room_gfx_canvas_.height()) { + + // Only draw if the texture is valid + if (gfx_sheet.texture() != 0) { + room_gfx_canvas_.draw_list()->AddImage( + (ImTextureID)(intptr_t)gfx_sheet.texture(), + ImVec2(x, y), + ImVec2(x + block_width, y + block_height)); + } + } + } + current_block += 1; + } + } + room_gfx_canvas_.DrawGrid(32.0f); + room_gfx_canvas_.DrawOverlay(); +} + +void DungeonObjectSelector::DrawIntegratedEditingPanels() { + if (!dungeon_editor_system_ || !object_editor_ || !*dungeon_editor_system_ || !*object_editor_) { + ImGui::Text("Editor systems not initialized"); + return; + } + + // Create a tabbed interface for different editing modes + if (ImGui::BeginTabBar("##EditingPanels")) { + // Object Editor Tab + if (ImGui::BeginTabItem("Objects")) { + DrawCompactObjectEditor(); + ImGui::EndTabItem(); + } + + // Sprite Editor Tab + if (ImGui::BeginTabItem("Sprites")) { + DrawCompactSpriteEditor(); + ImGui::EndTabItem(); + } + + // Item Editor Tab + if (ImGui::BeginTabItem("Items")) { + DrawCompactItemEditor(); + ImGui::EndTabItem(); + } + + // Entrance Editor Tab + if (ImGui::BeginTabItem("Entrances")) { + DrawCompactEntranceEditor(); + ImGui::EndTabItem(); + } + + // Door Editor Tab + if (ImGui::BeginTabItem("Doors")) { + DrawCompactDoorEditor(); + ImGui::EndTabItem(); + } + + // Chest Editor Tab + if (ImGui::BeginTabItem("Chests")) { + DrawCompactChestEditor(); + ImGui::EndTabItem(); + } + + // Properties Tab + if (ImGui::BeginTabItem("Properties")) { + DrawCompactPropertiesEditor(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } +} + +void DungeonObjectSelector::DrawCompactObjectEditor() { + if (!object_editor_ || !*object_editor_) { + ImGui::Text("Object editor not initialized"); + return; + } + + auto& editor = **object_editor_; + + ImGui::Text("Object Editor"); + Separator(); + + // Display current editing mode + auto mode = editor.GetMode(); + const char *mode_names[] = {"Select", "Insert", "Delete", "Edit", "Layer", "Preview"}; + ImGui::Text("Mode: %s", mode_names[static_cast(mode)]); + + // Compact mode selection + if (ImGui::Button("Select")) + editor.SetMode(zelda3::DungeonObjectEditor::Mode::kSelect); + ImGui::SameLine(); + if (ImGui::Button("Insert")) + editor.SetMode(zelda3::DungeonObjectEditor::Mode::kInsert); + ImGui::SameLine(); + if (ImGui::Button("Edit")) + editor.SetMode(zelda3::DungeonObjectEditor::Mode::kEdit); + + // Layer and object type selection + int current_layer = editor.GetCurrentLayer(); + if (ImGui::SliderInt("Layer", ¤t_layer, 0, 2)) { + editor.SetCurrentLayer(current_layer); + } + + int current_object_type = editor.GetCurrentObjectType(); + if (ImGui::InputInt("Object Type", ¤t_object_type, 1, 16)) { + if (current_object_type >= 0 && current_object_type <= 0x3FF) { + editor.SetCurrentObjectType(current_object_type); + } + } + + // Quick configuration checkboxes + auto config = editor.GetConfig(); + if (ImGui::Checkbox("Snap to Grid", &config.snap_to_grid)) { + editor.SetConfig(config); + } + ImGui::SameLine(); + if (ImGui::Checkbox("Show Grid", &config.show_grid)) { + editor.SetConfig(config); + } + + // Object count and selection info + Separator(); + ImGui::Text("Objects: %zu", editor.GetObjectCount()); + + auto selection = editor.GetSelection(); + if (!selection.selected_objects.empty()) { + ImGui::Text("Selected: %zu", selection.selected_objects.size()); + } + + // Undo/Redo buttons + Separator(); + if (ImGui::Button("Undo") && editor.CanUndo()) { + editor.Undo(); + } + ImGui::SameLine(); + if (ImGui::Button("Redo") && editor.CanRedo()) { + editor.Redo(); + } +} + +void DungeonObjectSelector::DrawCompactSpriteEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Sprite Editor"); + Separator(); + + // Display current room sprites + auto current_room = system.GetCurrentRoom(); + auto sprites_result = system.GetSpritesByRoom(current_room); + + if (sprites_result.ok()) { + auto sprites = sprites_result.value(); + ImGui::Text("Sprites in room %d: %zu", current_room, sprites.size()); + + // Show first few sprites in compact format + int display_count = std::min(3, static_cast(sprites.size())); + for (int i = 0; i < display_count; ++i) { + const auto &sprite = sprites[i]; + ImGui::Text("ID:%d Type:%d (%d,%d)", sprite.sprite_id, + static_cast(sprite.type), sprite.x, sprite.y); + } + if (sprites.size() > 3) { + ImGui::Text("... and %zu more", sprites.size() - 3); + } + } else { + ImGui::Text("Error loading sprites"); + } + + // Quick sprite placement + Separator(); + ImGui::Text("Quick Add Sprite"); + + static int new_sprite_id = 0; + static int new_sprite_x = 0; + static int new_sprite_y = 0; + + ImGui::InputInt("ID", &new_sprite_id); + ImGui::InputInt("X", &new_sprite_x); + ImGui::InputInt("Y", &new_sprite_y); + + if (ImGui::Button("Add Sprite")) { + zelda3::DungeonEditorSystem::SpriteData sprite_data; + sprite_data.sprite_id = new_sprite_id; + sprite_data.type = zelda3::DungeonEditorSystem::SpriteType::kEnemy; + sprite_data.x = new_sprite_x; + sprite_data.y = new_sprite_y; + sprite_data.layer = 0; + + auto status = system.AddSprite(sprite_data); + if (!status.ok()) { + ImGui::Text("Error adding sprite"); + } + } +} + +void DungeonObjectSelector::DrawCompactItemEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Item Editor"); + Separator(); + + // Display current room items + auto current_room = system.GetCurrentRoom(); + auto items_result = system.GetItemsByRoom(current_room); + + if (items_result.ok()) { + auto items = items_result.value(); + ImGui::Text("Items in room %d: %zu", current_room, items.size()); + + // Show first few items in compact format + int display_count = std::min(3, static_cast(items.size())); + for (int i = 0; i < display_count; ++i) { + const auto &item = items[i]; + ImGui::Text("ID:%d Type:%d (%d,%d)", item.item_id, + static_cast(item.type), item.x, item.y); + } + if (items.size() > 3) { + ImGui::Text("... and %zu more", items.size() - 3); + } + } else { + ImGui::Text("Error loading items"); + } + + // Quick item placement + Separator(); + ImGui::Text("Quick Add Item"); + + static int new_item_id = 0; + static int new_item_x = 0; + static int new_item_y = 0; + + ImGui::InputInt("ID", &new_item_id); + ImGui::InputInt("X", &new_item_x); + ImGui::InputInt("Y", &new_item_y); + + if (ImGui::Button("Add Item")) { + zelda3::DungeonEditorSystem::ItemData item_data; + item_data.item_id = new_item_id; + item_data.type = zelda3::DungeonEditorSystem::ItemType::kKey; + item_data.x = new_item_x; + item_data.y = new_item_y; + item_data.room_id = current_room; + item_data.is_hidden = false; + + auto status = system.AddItem(item_data); + if (!status.ok()) { + ImGui::Text("Error adding item"); + } + } +} + +void DungeonObjectSelector::DrawCompactEntranceEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Entrance Editor"); + Separator(); + + // Display current room entrances + auto current_room = system.GetCurrentRoom(); + auto entrances_result = system.GetEntrancesByRoom(current_room); + + if (entrances_result.ok()) { + auto entrances = entrances_result.value(); + ImGui::Text("Entrances: %zu", entrances.size()); + + for (const auto &entrance : entrances) { + ImGui::Text("ID:%d -> Room:%d (%d,%d)", entrance.entrance_id, + entrance.target_room_id, entrance.target_x, + entrance.target_y); + } + } else { + ImGui::Text("Error loading entrances"); + } + + // Quick room connection + Separator(); + ImGui::Text("Connect Rooms"); + + static int target_room_id = 0; + static int source_x = 0; + static int source_y = 0; + static int target_x = 0; + static int target_y = 0; + + ImGui::InputInt("Target Room", &target_room_id); + ImGui::InputInt("Source X", &source_x); + ImGui::InputInt("Source Y", &source_y); + ImGui::InputInt("Target X", &target_x); + ImGui::InputInt("Target Y", &target_y); + + if (ImGui::Button("Connect")) { + auto status = system.ConnectRooms(current_room, target_room_id, source_x, source_y, target_x, target_y); + if (!status.ok()) { + ImGui::Text("Error connecting rooms"); + } + } +} + +void DungeonObjectSelector::DrawCompactDoorEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Door Editor"); + Separator(); + + // Display current room doors + auto current_room = system.GetCurrentRoom(); + auto doors_result = system.GetDoorsByRoom(current_room); + + if (doors_result.ok()) { + auto doors = doors_result.value(); + ImGui::Text("Doors: %zu", doors.size()); + + for (const auto &door : doors) { + ImGui::Text("ID:%d (%d,%d) -> Room:%d", door.door_id, door.x, door.y, + door.target_room_id); + } + } else { + ImGui::Text("Error loading doors"); + } + + // Quick door creation + Separator(); + ImGui::Text("Add Door"); + + static int door_x = 0; + static int door_y = 0; + static int door_direction = 0; + static int door_target_room = 0; + + ImGui::InputInt("X", &door_x); + ImGui::InputInt("Y", &door_y); + ImGui::SliderInt("Dir", &door_direction, 0, 3); + ImGui::InputInt("Target", &door_target_room); + + if (ImGui::Button("Add Door")) { + zelda3::DungeonEditorSystem::DoorData door_data; + door_data.room_id = current_room; + door_data.x = door_x; + door_data.y = door_y; + door_data.direction = door_direction; + door_data.target_room_id = door_target_room; + door_data.target_x = door_x; + door_data.target_y = door_y; + door_data.is_locked = false; + door_data.requires_key = false; + door_data.key_type = 0; + + auto status = system.AddDoor(door_data); + if (!status.ok()) { + ImGui::Text("Error adding door"); + } + } +} + +void DungeonObjectSelector::DrawCompactChestEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Chest Editor"); + Separator(); + + // Display current room chests + auto current_room = system.GetCurrentRoom(); + auto chests_result = system.GetChestsByRoom(current_room); + + if (chests_result.ok()) { + auto chests = chests_result.value(); + ImGui::Text("Chests: %zu", chests.size()); + + for (const auto &chest : chests) { + ImGui::Text("ID:%d (%d,%d) Item:%d", chest.chest_id, chest.x, chest.y, + chest.item_id); + } + } else { + ImGui::Text("Error loading chests"); + } + + // Quick chest creation + Separator(); + ImGui::Text("Add Chest"); + + static int chest_x = 0; + static int chest_y = 0; + static int chest_item_id = 0; + static bool chest_big = false; + + ImGui::InputInt("X", &chest_x); + ImGui::InputInt("Y", &chest_y); + ImGui::InputInt("Item ID", &chest_item_id); + ImGui::Checkbox("Big", &chest_big); + + if (ImGui::Button("Add Chest")) { + zelda3::DungeonEditorSystem::ChestData chest_data; + chest_data.room_id = current_room; + chest_data.x = chest_x; + chest_data.y = chest_y; + chest_data.is_big_chest = chest_big; + chest_data.item_id = chest_item_id; + chest_data.item_quantity = 1; + + auto status = system.AddChest(chest_data); + if (!status.ok()) { + ImGui::Text("Error adding chest"); + } + } +} + +void DungeonObjectSelector::DrawCompactPropertiesEditor() { + if (!dungeon_editor_system_ || !*dungeon_editor_system_) { + ImGui::Text("Dungeon editor system not initialized"); + return; + } + + auto& system = **dungeon_editor_system_; + + ImGui::Text("Room Properties"); + Separator(); + + auto current_room = system.GetCurrentRoom(); + auto properties_result = system.GetRoomProperties(current_room); + + if (properties_result.ok()) { + auto properties = properties_result.value(); + + static char room_name[128] = {0}; + static int dungeon_id = 0; + static int floor_level = 0; + static bool is_boss_room = false; + static bool is_save_room = false; + static int music_id = 0; + + // Copy current values + strncpy(room_name, properties.name.c_str(), sizeof(room_name) - 1); + dungeon_id = properties.dungeon_id; + floor_level = properties.floor_level; + is_boss_room = properties.is_boss_room; + is_save_room = properties.is_save_room; + music_id = properties.music_id; + + ImGui::InputText("Name", room_name, sizeof(room_name)); + ImGui::InputInt("Dungeon ID", &dungeon_id); + ImGui::InputInt("Floor", &floor_level); + ImGui::InputInt("Music", &music_id); + ImGui::Checkbox("Boss Room", &is_boss_room); + ImGui::Checkbox("Save Room", &is_save_room); + + if (ImGui::Button("Save Properties")) { + zelda3::DungeonEditorSystem::RoomProperties new_properties; + new_properties.room_id = current_room; + new_properties.name = room_name; + new_properties.dungeon_id = dungeon_id; + new_properties.floor_level = floor_level; + new_properties.is_boss_room = is_boss_room; + new_properties.is_save_room = is_save_room; + new_properties.music_id = music_id; + + auto status = system.SetRoomProperties(current_room, new_properties); + if (!status.ok()) { + ImGui::Text("Error saving properties"); + } + } + } else { + ImGui::Text("Error loading properties"); + } + + // Dungeon settings summary + Separator(); + ImGui::Text("Dungeon Settings"); + + auto dungeon_settings_result = system.GetDungeonSettings(); + if (dungeon_settings_result.ok()) { + auto settings = dungeon_settings_result.value(); + ImGui::Text("Dungeon: %s", settings.name.c_str()); + ImGui::Text("Rooms: %d", settings.total_rooms); + ImGui::Text("Start: %d", settings.starting_room_id); + ImGui::Text("Boss: %d", settings.boss_room_id); + } +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_object_selector.h b/src/app/editor/dungeon/dungeon_object_selector.h new file mode 100644 index 00000000..a67ecc9b --- /dev/null +++ b/src/app/editor/dungeon/dungeon_object_selector.h @@ -0,0 +1,82 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_SELECTOR_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_SELECTOR_H + +#include "app/gui/canvas.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_renderer.h" +#include "app/zelda3/dungeon/dungeon_object_editor.h" +#include "app/zelda3/dungeon/dungeon_editor_system.h" +#include "app/gfx/snes_palette.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles object selection, preview, and editing UI + */ +class DungeonObjectSelector { + public: + explicit DungeonObjectSelector(Rom* rom = nullptr) : rom_(rom), object_renderer_(rom) {} + + void DrawTileSelector(); + void DrawObjectRenderer(); + void DrawIntegratedEditingPanels(); + + void set_rom(Rom* rom) { + rom_ = rom; + object_renderer_.SetROM(rom); + } + Rom* rom() const { return rom_; } + + // Editor system access + void set_dungeon_editor_system(std::unique_ptr* system) { + dungeon_editor_system_ = system; + } + void set_object_editor(std::shared_ptr* editor) { + object_editor_ = editor; + } + + // Room data access + void set_rooms(std::array* rooms) { rooms_ = rooms; } + void set_current_room_id(int room_id) { current_room_id_ = room_id; } + + // Palette access + void set_current_palette_group_id(uint64_t id) { current_palette_group_id_ = id; } + + private: + void DrawRoomGraphics(); + void DrawCompactObjectEditor(); + void DrawCompactSpriteEditor(); + void DrawCompactItemEditor(); + void DrawCompactEntranceEditor(); + void DrawCompactDoorEditor(); + void DrawCompactChestEditor(); + void DrawCompactPropertiesEditor(); + + Rom* rom_ = nullptr; + gui::Canvas room_gfx_canvas_{"##RoomGfxCanvas", ImVec2(0x100 + 1, 0x10 * 0x40 + 1)}; + gui::Canvas object_canvas_; + zelda3::ObjectRenderer object_renderer_; + + // Editor systems + std::unique_ptr* dungeon_editor_system_ = nullptr; + std::shared_ptr* object_editor_ = nullptr; + + // Room data + std::array* rooms_ = nullptr; + int current_room_id_ = 0; + + // Palette data + uint64_t current_palette_group_id_ = 0; + + // Object preview system + zelda3::RoomObject preview_object_{0, 0, 0, 0, 0}; + gfx::SnesPalette preview_palette_; + bool object_loaded_ = false; +}; + +} // namespace editor +} // namespace yaze + +#endif diff --git a/src/app/editor/dungeon/dungeon_room_selector.cc b/src/app/editor/dungeon/dungeon_room_selector.cc new file mode 100644 index 00000000..9578a339 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_room_selector.cc @@ -0,0 +1,124 @@ +#include "dungeon_room_selector.h" + +#include "absl/strings/str_format.h" +#include "app/gui/input.h" +#include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/room_entrance.h" +#include "imgui/imgui.h" +#include "util/hex.h" + +namespace yaze::editor { + +using ImGui::BeginChild; +using ImGui::EndChild; +using ImGui::Separator; +using ImGui::SameLine; + +void DungeonRoomSelector::DrawRoomSelector() { + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + return; + } + + gui::InputHexWord("Room ID", ¤t_room_id_, 50.f, true); + + if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)9); + BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + int i = 0; + for (const auto each_room_name : zelda3::kRoomNames) { + rom_->resource_label()->SelectableLabelWithNameEdit( + current_room_id_ == i, "Dungeon Room Names", util::HexByte(i), + each_room_name.data()); + if (ImGui::IsItemClicked()) { + current_room_id_ = i; + if (!active_rooms_.contains(i)) { + active_rooms_.push_back(i); + } + } + i += 1; + } + } + EndChild(); +} + +void DungeonRoomSelector::DrawEntranceSelector() { + if (!rom_ || !rom_->is_loaded()) { + ImGui::Text("ROM not loaded"); + return; + } + + if (!entrances_) { + ImGui::Text("Entrances not loaded"); + return; + } + + auto current_entrance = (*entrances_)[current_entrance_id_]; + gui::InputHexWord("Entrance ID", ¤t_entrance.entrance_id_); + gui::InputHexWord("Room ID", ¤t_entrance.room_); + SameLine(); + + gui::InputHexByte("Dungeon ID", ¤t_entrance.dungeon_id_, 50.f, true); + gui::InputHexByte("Blockset", ¤t_entrance.blockset_, 50.f, true); + SameLine(); + + gui::InputHexByte("Music", ¤t_entrance.music_, 50.f, true); + SameLine(); + gui::InputHexByte("Floor", ¤t_entrance.floor_); + Separator(); + + gui::InputHexWord("Player X ", ¤t_entrance.x_position_); + SameLine(); + gui::InputHexWord("Player Y ", ¤t_entrance.y_position_); + + gui::InputHexWord("Camera X", ¤t_entrance.camera_trigger_x_); + SameLine(); + gui::InputHexWord("Camera Y", ¤t_entrance.camera_trigger_y_); + + gui::InputHexWord("Scroll X ", ¤t_entrance.camera_x_); + SameLine(); + gui::InputHexWord("Scroll Y ", ¤t_entrance.camera_y_); + + gui::InputHexWord("Exit", ¤t_entrance.exit_, 50.f, true); + + Separator(); + ImGui::Text("Camera Boundaries"); + Separator(); + ImGui::Text("\t\t\t\t\tNorth East South West"); + gui::InputHexByte("Quadrant", ¤t_entrance.camera_boundary_qn_, 50.f, + true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_qe_, 50.f, true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_qs_, 50.f, true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_qw_, 50.f, true); + + gui::InputHexByte("Full room", ¤t_entrance.camera_boundary_fn_, 50.f, + true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_fe_, 50.f, true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_fs_, 50.f, true); + SameLine(); + gui::InputHexByte("", ¤t_entrance.camera_boundary_fw_, 50.f, true); + + if (BeginChild("EntranceSelector", ImVec2(0, 0), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + for (int i = 0; i < 0x85 + 7; i++) { + rom_->resource_label()->SelectableLabelWithNameEdit( + current_entrance_id_ == i, "Dungeon Entrance Names", + util::HexByte(i), zelda3::kEntranceNames[i].data()); + + if (ImGui::IsItemClicked()) { + current_entrance_id_ = i; + if (!active_rooms_.contains(i)) { + active_rooms_.push_back((*entrances_)[i].room_); + } + } + } + } + EndChild(); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_room_selector.h b/src/app/editor/dungeon/dungeon_room_selector.h new file mode 100644 index 00000000..4af32e71 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_room_selector.h @@ -0,0 +1,55 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_SELECTOR_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_SELECTOR_H + +#include "imgui/imgui.h" +#include "app/rom.h" +#include "app/gui/input.h" +#include "app/zelda3/dungeon/room_entrance.h" +#include "zelda3/dungeon/room.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles room and entrance selection UI + */ +class DungeonRoomSelector { + public: + explicit DungeonRoomSelector(Rom* rom = nullptr) : rom_(rom) {} + + void DrawRoomSelector(); + void DrawEntranceSelector(); + + void set_rom(Rom* rom) { rom_ = rom; } + Rom* rom() const { return rom_; } + + // Room selection + void set_current_room_id(uint16_t room_id) { current_room_id_ = room_id; } + int current_room_id() const { return current_room_id_; } + + void set_active_rooms(const ImVector& rooms) { active_rooms_ = rooms; } + const ImVector& active_rooms() const { return active_rooms_; } + ImVector& mutable_active_rooms() { return active_rooms_; } + + // Entrance selection + void set_current_entrance_id(int entrance_id) { current_entrance_id_ = entrance_id; } + int current_entrance_id() const { return current_entrance_id_; } + + // Room data access + void set_rooms(std::array* rooms) { rooms_ = rooms; } + void set_entrances(std::array* entrances) { entrances_ = entrances; } + + private: + Rom* rom_ = nullptr; + uint16_t current_room_id_ = 0; + int current_entrance_id_ = 0; + ImVector active_rooms_; + + std::array* rooms_ = nullptr; + std::array* entrances_ = nullptr; +}; + +} // namespace editor +} // namespace yaze + +#endif diff --git a/src/app/editor/editor.cmake b/src/app/editor/editor.cmake index 0b6bb9c8..122e2e65 100644 --- a/src/app/editor/editor.cmake +++ b/src/app/editor/editor.cmake @@ -2,6 +2,9 @@ set( YAZE_APP_EDITOR_SRC app/editor/editor_manager.cc app/editor/dungeon/dungeon_editor.cc + app/editor/dungeon/dungeon_room_selector.cc + app/editor/dungeon/dungeon_canvas_viewer.cc + app/editor/dungeon/dungeon_object_selector.cc app/editor/overworld/overworld_editor.cc app/editor/sprite/sprite_editor.cc app/editor/music/music_editor.cc diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 5325cb92..7a33e804 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -233,20 +233,47 @@ constexpr int kGfxBufferRoomSpriteStride = 2048; constexpr int kGfxBufferRoomSpriteLastLineOffset = 0x88; void Room::CopyRoomGraphicsToBuffer() { + if (!rom_ || !rom_->is_loaded()) { + return; + } + auto gfx_buffer_data = rom()->mutable_graphics_buffer(); + if (!gfx_buffer_data || gfx_buffer_data->empty()) { + return; + } // Copy room graphics to buffer int sheet_pos = 0; for (int i = 0; i < 16; i++) { + // Validate block index + if (blocks_[i] < 0 || blocks_[i] > 255) { + sheet_pos += kGfxBufferRoomOffset; + continue; + } + int data = 0; int block_offset = blocks_[i] * kGfxBufferRoomOffset; + + // Validate block_offset bounds + if (block_offset < 0 || block_offset >= static_cast(gfx_buffer_data->size())) { + sheet_pos += kGfxBufferRoomOffset; + continue; + } + while (data < kGfxBufferRoomOffset) { - uint8_t map_byte = (*gfx_buffer_data)[data + block_offset]; - if (i < 4) { - map_byte += kGfxBufferRoomSpriteLastLineOffset; - } + int buffer_index = data + block_offset; + if (buffer_index >= 0 && buffer_index < static_cast(gfx_buffer_data->size())) { + uint8_t map_byte = (*gfx_buffer_data)[buffer_index]; + if (i < 4) { + map_byte += kGfxBufferRoomSpriteLastLineOffset; + } - current_gfx16_[data + sheet_pos] = map_byte; + // Validate current_gfx16_ access + int gfx_index = data + sheet_pos; + if (gfx_index >= 0 && gfx_index < static_cast(sizeof(current_gfx16_))) { + current_gfx16_[gfx_index] = map_byte; + } + } data++; } @@ -285,21 +312,62 @@ void Room::RenderRoomGraphics() { } void Room::LoadAnimatedGraphics() { - int gfx_ptr = SnesToPc(rom()->version_constants().kGfxAnimatedPointer); - + if (!rom_ || !rom_->is_loaded()) { + return; + } + auto gfx_buffer_data = rom()->mutable_graphics_buffer(); + if (!gfx_buffer_data || gfx_buffer_data->empty()) { + return; + } + auto rom_data = rom()->vector(); + if (rom_data.empty()) { + return; + } + + // Validate animated_frame_ bounds + if (animated_frame_ < 0 || animated_frame_ > 10) { + return; + } + + // Validate background_tileset_ bounds + if (background_tileset_ < 0 || background_tileset_ > 255) { + return; + } + + int gfx_ptr = SnesToPc(rom()->version_constants().kGfxAnimatedPointer); + if (gfx_ptr < 0 || gfx_ptr >= static_cast(rom_data.size())) { + return; + } + int data = 0; while (data < 512) { - uint8_t map_byte = - (*gfx_buffer_data)[data + (92 * 2048) + (512 * animated_frame_)]; - current_gfx16_[data + (7 * 2048)] = map_byte; - - map_byte = - (*gfx_buffer_data)[data + - (rom_data[gfx_ptr + background_tileset_] * 2048) + - (512 * animated_frame_)]; - current_gfx16_[data + (7 * 2048) - 512] = map_byte; + // Validate buffer access for first operation + int first_offset = data + (92 * 2048) + (512 * animated_frame_); + if (first_offset >= 0 && first_offset < static_cast(gfx_buffer_data->size())) { + uint8_t map_byte = (*gfx_buffer_data)[first_offset]; + + // Validate current_gfx16_ access + int gfx_offset = data + (7 * 2048); + if (gfx_offset >= 0 && gfx_offset < static_cast(sizeof(current_gfx16_))) { + current_gfx16_[gfx_offset] = map_byte; + } + } + + // Validate buffer access for second operation + int tileset_index = rom_data[gfx_ptr + background_tileset_]; + int second_offset = data + (tileset_index * 2048) + (512 * animated_frame_); + if (second_offset >= 0 && second_offset < static_cast(gfx_buffer_data->size())) { + uint8_t map_byte = (*gfx_buffer_data)[second_offset]; + + // Validate current_gfx16_ access + int gfx_offset = data + (7 * 2048) - 512; + if (gfx_offset >= 0 && gfx_offset < static_cast(sizeof(current_gfx16_))) { + current_gfx16_[gfx_offset] = map_byte; + } + } + data++; } } diff --git a/src/app/zelda3/sprite/sprite.cc b/src/app/zelda3/sprite/sprite.cc index f2c40731..2d1e83c8 100644 --- a/src/app/zelda3/sprite/sprite.cc +++ b/src/app/zelda3/sprite/sprite.cc @@ -873,9 +873,24 @@ void Sprite::DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, return; } + // Validate input parameters + if (sizex <= 0 || sizey <= 0) { + return; + } + + if (srcx < 0 || srcy < 0 || pal < 0) { + return; + } + x += 16; y += 16; int drawid_ = (srcx + (srcy * 16)) + 512; + + // Validate drawid_ is within reasonable bounds + if (drawid_ < 0 || drawid_ > 4096) { + return; + } + for (auto yl = 0; yl < sizey * 8; yl++) { for (auto xl = 0; xl < (sizex * 8) / 2; xl++) { int mx = xl; @@ -893,12 +908,21 @@ void Sprite::DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, int tx = ((drawid_ / 0x10) * 0x400) + ((drawid_ - ((drawid_ / 0x10) * 0x10)) * 8); - auto pixel = current_gfx_[tx + (yl * 0x80) + xl]; + + // Validate graphics buffer access + int gfx_index = tx + (yl * 0x80) + xl; + if (gfx_index < 0 || gfx_index >= static_cast(current_gfx_.size())) { + continue; // Skip this pixel if out of bounds + } + + auto pixel = current_gfx_[gfx_index]; // nx,ny = object position, xx,yy = tile position, xl,yl = pixel // position int index = (x) + (y * 64) + (mx + (my * 0x80)); - if (index >= 0 && index <= 4096) { + // Validate preview buffer access + if (index >= 0 && index < static_cast(preview_gfx_.size()) && + index <= 4096) { preview_gfx_[index] = (uint8_t)((pixel & 0x0F) + 112 + (pal * 8)); } }