#include "dungeon_editor.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/dungeon_editor_system.h" #include "app/zelda3/dungeon/dungeon_object_editor.h" #include "app/zelda3/dungeon/room.h" #include "imgui/imgui.h" namespace yaze::editor { using core::Renderer; using ImGui::BeginTabBar; using ImGui::BeginTabItem; using ImGui::BeginTable; using ImGui::Button; using ImGui::EndTabBar; using ImGui::EndTabItem; using ImGui::RadioButton; using ImGui::SameLine; using ImGui::TableHeadersRow; using ImGui::TableNextColumn; using ImGui::TableNextRow; using ImGui::TableSetupColumn; using ImGui::Text; constexpr ImGuiTableFlags kDungeonObjectTableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV; void DungeonEditor::Initialize() { if (rom_ && !dungeon_editor_system_) { dungeon_editor_system_ = std::make_unique(rom_); object_editor_ = std::make_shared(rom_); } } absl::Status DungeonEditor::Load() { auto dungeon_man_pal_group = rom()->palette_group().dungeon_main; // Use room loader component for loading rooms RETURN_IF_ERROR(room_loader_.LoadAllRooms(rooms_)); RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(entrances_)); // Load the palette group and palette for the dungeon full_palette_ = dungeon_man_pal_group[current_palette_group_id_]; ASSIGN_OR_RETURN(current_palette_group_, gfx::CreatePaletteGroupFromLargePalette(full_palette_)); // Calculate usage statistics usage_tracker_.CalculateUsageStats(rooms_); // Initialize the new editor system if (dungeon_editor_system_) { auto status = dungeon_editor_system_->Initialize(); if (!status.ok()) { return status; } } // Initialize the new UI components with loaded data room_selector_.set_rom(rom_); room_selector_.set_rooms(&rooms_); room_selector_.set_entrances(&entrances_); room_selector_.set_active_rooms(active_rooms_); room_selector_.set_room_selected_callback( [this](int room_id) { OnRoomSelected(room_id); }); canvas_viewer_.SetRom(rom_); canvas_viewer_.SetRooms(&rooms_); canvas_viewer_.SetCurrentPaletteGroup(current_palette_group_); canvas_viewer_.SetCurrentPaletteId(current_palette_id_); object_selector_.SetRom(rom_); object_selector_.SetCurrentPaletteGroup(current_palette_group_); object_selector_.SetCurrentPaletteId(current_palette_id_); object_selector_.set_dungeon_editor_system(&dungeon_editor_system_); object_selector_.set_object_editor(&object_editor_); object_selector_.set_rooms(&rooms_); // Set up object selection callback object_selector_.SetObjectSelectedCallback( [this](const zelda3::RoomObject& object) { preview_object_ = object; object_loaded_ = true; toolset_.set_placement_type(DungeonToolset::kObject); object_interaction_.SetPreviewObject(object, true); }); // Set up component callbacks object_interaction_.SetCurrentRoom(&rooms_, current_room_id_); object_interaction_.SetObjectPlacedCallback([this](const zelda3::RoomObject& object) { renderer_.ClearObjectCache(); }); object_interaction_.SetCacheInvalidationCallback([this]() { renderer_.ClearObjectCache(); }); // Set up toolset callbacks toolset_.SetUndoCallback([this]() { PRINT_IF_ERROR(Undo()); }); toolset_.SetRedoCallback([this]() { PRINT_IF_ERROR(Redo()); }); toolset_.SetPaletteToggleCallback([this]() { palette_showing_ = !palette_showing_; }); is_loaded_ = true; return absl::OkStatus(); } absl::Status DungeonEditor::Update() { if (refresh_graphics_) { RETURN_IF_ERROR(RefreshGraphics()); refresh_graphics_ = false; } status_ = UpdateDungeonRoomView(); return absl::OkStatus(); } absl::Status DungeonEditor::Undo() { if (dungeon_editor_system_) { return dungeon_editor_system_->Undo(); } return absl::UnimplementedError("Undo not available"); } absl::Status DungeonEditor::Redo() { if (dungeon_editor_system_) { return dungeon_editor_system_->Redo(); } return absl::UnimplementedError("Redo not available"); } absl::Status DungeonEditor::Save() { if (dungeon_editor_system_) { return dungeon_editor_system_->SaveDungeon(); } return absl::UnimplementedError("Save not available"); } absl::Status DungeonEditor::RefreshGraphics() { std::for_each_n( rooms_[current_room_id_].blocks().begin(), 8, [this](int block) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( current_palette_group_[current_palette_id_], 0); Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); }); auto sprites_aux1_pal_group = rom()->palette_group().sprites_aux1; std::for_each_n( rooms_[current_room_id_].blocks().begin() + 8, 8, [this, &sprites_aux1_pal_group](int block) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( sprites_aux1_pal_group[current_palette_id_], 0); Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); }); return absl::OkStatus(); } // LoadDungeonRoomSize moved to DungeonRoomLoader component absl::Status DungeonEditor::UpdateDungeonRoomView() { toolset_.Draw(); if (palette_showing_) { ImGui::Begin("Palette Editor", &palette_showing_, 0); auto dungeon_main_pal_group = rom()->palette_group().dungeon_main; current_palette_ = dungeon_main_pal_group[current_palette_group_id_]; gui::SelectablePalettePipeline(current_palette_id_, refresh_graphics_, current_palette_); ImGui::End(); } // Correct 3-column layout as specified if (BeginTable("#DungeonEditTable", 3, kDungeonTableFlags, ImVec2(0, 0))) { TableSetupColumn("Room/Entrance Selector", ImGuiTableColumnFlags_WidthFixed, 250); TableSetupColumn("Canvas & Properties", ImGuiTableColumnFlags_WidthStretch); TableSetupColumn("Object Selector/Editor", ImGuiTableColumnFlags_WidthFixed, 300); TableHeadersRow(); TableNextRow(); // Column 1: Room and Entrance Selector (unchanged) TableNextColumn(); room_selector_.Draw(); // Column 2: Canvas and room properties with tabs TableNextColumn(); DrawCanvasAndPropertiesPanel(); // Column 3: Object selector, room graphics, and object editor TableNextColumn(); object_selector_.Draw(); ImGui::EndTable(); } return absl::OkStatus(); } void DungeonEditor::OnRoomSelected(int room_id) { // Update current room ID current_room_id_ = room_id; // Check if room is already open in a tab int existing_tab_index = -1; for (int i = 0; i < active_rooms_.Size; i++) { if (active_rooms_[i] == room_id) { existing_tab_index = i; break; } } if (existing_tab_index >= 0) { // Room is already open, switch to that tab current_active_room_tab_ = existing_tab_index; } else { // Room is not open, add it as a new tab active_rooms_.push_back(room_id); current_active_room_tab_ = active_rooms_.Size - 1; } // Update the room selector's active rooms list room_selector_.set_active_rooms(active_rooms_); } // DrawToolset() method moved to DungeonToolset component void DungeonEditor::DrawCanvasAndPropertiesPanel() { if (ImGui::BeginTabBar("CanvasPropertiesTabBar")) { // Canvas tab - main editing view if (ImGui::BeginTabItem("Canvas")) { DrawDungeonTabView(); ImGui::EndTabItem(); } // Room Properties tab - debug and editing controls if (ImGui::BeginTabItem("Room Properties")) { if (ImGui::Button("Room Debug Info")) { ImGui::OpenPopup("RoomDebugPopup"); } // Room properties popup if (ImGui::BeginPopup("RoomDebugPopup")) { DrawRoomPropertiesDebugPopup(); ImGui::EndPopup(); } // Quick room info display int current_room = current_room_id_; if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) { current_room = active_rooms_[current_active_room_tab_]; } if (current_room >= 0 && current_room < rooms_.size()) { auto& room = rooms_[current_room]; ImGui::Text("Current Room: %03X (%d)", current_room, current_room); ImGui::Text("Objects: %zu", room.GetTileObjects().size()); ImGui::Text("Sprites: %zu", room.GetSprites().size()); ImGui::Text("Chests: %zu", room.GetChests().size()); // Selection info const auto& selected_indices = object_interaction_.GetSelectedObjectIndices(); if (!selected_indices.empty()) { ImGui::Separator(); ImGui::Text("Selected Objects: %zu", selected_indices.size()); if (ImGui::Button("Clear Selection")) { object_interaction_.ClearSelection(); } } ImGui::Separator(); // Quick edit controls gui::InputHexByte("Layout", &room.layout); gui::InputHexByte("Blockset", &room.blockset); gui::InputHexByte("Spriteset", &room.spriteset); gui::InputHexByte("Palette", &room.palette); if (ImGui::Button("Reload Room Graphics")) { (void)LoadAndRenderRoomGraphics(current_room); } } ImGui::EndTabItem(); } ImGui::EndTabBar(); } } void DungeonEditor::DrawRoomPropertiesDebugPopup() { int current_room = current_room_id_; if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) { current_room = active_rooms_[current_active_room_tab_]; } if (current_room < 0 || current_room >= rooms_.size()) { ImGui::Text("Invalid room"); return; } auto& room = rooms_[current_room]; ImGui::Text("Room %03X Debug Information", current_room); ImGui::Separator(); // Room properties table if (ImGui::BeginTable("RoomPropertiesPopup", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 120); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Room ID"); ImGui::TableNextColumn(); ImGui::Text("%03X (%d)", current_room, current_room); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Layout"); ImGui::TableNextColumn(); gui::InputHexByte("##layout", &room.layout); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Blockset"); ImGui::TableNextColumn(); gui::InputHexByte("##blockset", &room.blockset); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Spriteset"); ImGui::TableNextColumn(); gui::InputHexByte("##spriteset", &room.spriteset); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Palette"); ImGui::TableNextColumn(); gui::InputHexByte("##palette", &room.palette); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Floor 1"); ImGui::TableNextColumn(); gui::InputHexByte("##floor1", &room.floor1); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Floor 2"); ImGui::TableNextColumn(); gui::InputHexByte("##floor2", &room.floor2); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Message ID"); ImGui::TableNextColumn(); gui::InputHexWord("##message_id", &room.message_id_); ImGui::EndTable(); } ImGui::Separator(); // Object statistics ImGui::Text("Object Statistics:"); ImGui::Text("Total Objects: %zu", room.GetTileObjects().size()); ImGui::Text("Layout Objects: %zu", room.GetLayout().GetObjects().size()); ImGui::Text("Sprites: %zu", room.GetSprites().size()); ImGui::Text("Chests: %zu", room.GetChests().size()); ImGui::Separator(); if (ImGui::Button("Reload Objects")) { room.LoadObjects(); } ImGui::SameLine(); if (ImGui::Button("Clear Cache")) { renderer_.ClearObjectCache(); } ImGui::SameLine(); if (ImGui::Button("Close")) { ImGui::CloseCurrentPopup(); } } void DungeonEditor::DrawDungeonTabView() { static int next_tab_id = 0; if (BeginTabBar("MyTabBar", kDungeonTabBarFlags)) { if (ImGui::TabItemButton(ICON_MD_ADD, kDungeonTabFlags)) { if (std::find(active_rooms_.begin(), active_rooms_.end(), current_room_id_) != active_rooms_.end()) { // Room is already open next_tab_id++; } active_rooms_.push_back(next_tab_id++); // Add new tab } // 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 (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(); } if (!open) active_rooms_.erase(active_rooms_.Data + n); else n++; } EndTabBar(); } ImGui::Separator(); } 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); SameLine(); gui::InputHexByte("Blockset", &rooms_[room_id].blockset); SameLine(); gui::InputHexByte("Spriteset", &rooms_[room_id].spriteset); SameLine(); gui::InputHexByte("Palette", &rooms_[room_id].palette); gui::InputHexByte("Floor1", &rooms_[room_id].floor1); SameLine(); gui::InputHexByte("Floor2", &rooms_[room_id].floor2); SameLine(); gui::InputHexWord("Message ID", &rooms_[room_id].message_id_); SameLine(); if (Button("Load Room Graphics")) { (void)LoadAndRenderRoomGraphics(room_id); } ImGui::SameLine(); if (ImGui::Button("Reload All Graphics")) { (void)ReloadAllRoomGraphics(); } // Debug and control popup static bool show_debug_popup = false; if (ImGui::Button("Room Debug Info")) { show_debug_popup = true; } if (show_debug_popup) { ImGui::OpenPopup("Room Debug Info"); show_debug_popup = false; } 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")) { renderer_.ClearObjectCache(); } ImGui::SameLine(); ImGui::Text("Cache: %zu objects", renderer_.GetCacheSize()); // Object statistics and metadata ImGui::Separator(); ImGui::Text("Room Statistics:"); ImGui::Text("Objects: %zu", rooms_[room_id].GetTileObjects().size()); ImGui::Text("Layout Objects: %zu", rooms_[room_id].GetLayout().GetObjects().size()); ImGui::Text("Sprites: %llu", static_cast( rooms_[room_id].GetSprites().size())); ImGui::Text("Chests: %zu", rooms_[room_id].GetChests().size()); // Palette information ImGui::Text("Current Palette Group: %llu", static_cast(current_palette_group_id_)); ImGui::Text("Cache Size: %zu objects", renderer_.GetCacheSize()); // Object type breakdown ImGui::Separator(); ImGui::Text("Object Type Breakdown:"); std::map object_type_counts; for (const auto& obj : rooms_[room_id].GetTileObjects()) { object_type_counts[obj.id_]++; } for (const auto& [type, count] : object_type_counts) { ImGui::Text("Type 0x%02X: %d objects", type, count); } // Layout object type breakdown ImGui::Separator(); ImGui::Text("Layout Object Types:"); auto walls = rooms_[room_id].GetLayout().GetObjectsByType( zelda3::RoomLayoutObject::Type::kWall); auto floors = rooms_[room_id].GetLayout().GetObjectsByType( zelda3::RoomLayoutObject::Type::kFloor); auto doors = rooms_[room_id].GetLayout().GetObjectsByType( zelda3::RoomLayoutObject::Type::kDoor); 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(); } } 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 } 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(); canvas_.DrawBackground(); canvas_.DrawContextMenu(); // Handle object selection and placement using component object_interaction_.CheckForObjectSelection(); // Handle mouse input for drag and select functionality object_interaction_.HandleCanvasMouseInput(); // Update preview object position based on mouse cursor if (object_loaded_ && preview_object_.id_ >= 0 && canvas_.IsMouseHovering()) { const ImGuiIO& io = ImGui::GetIO(); ImVec2 mouse_pos = io.MousePos; ImVec2 canvas_pos = canvas_.zero_point(); ImVec2 canvas_mouse_pos = ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y); auto [room_x, room_y] = object_interaction_.CanvasToRoomCoordinates(static_cast(canvas_mouse_pos.x), static_cast(canvas_mouse_pos.y)); preview_object_.x_ = room_x; preview_object_.y_ = room_y; } if (is_loaded_) { // Automatically load room graphics if not already loaded if (rooms_[room_id].blocks().empty()) { (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 renderer_.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 : rooms_[room_id].GetTileObjects()) { renderer_.RenderObjectInCanvas(object, room_palette); } } } // Draw selection box and drag preview using component object_interaction_.DrawSelectBox(); object_interaction_.DrawDragPreview(); canvas_.DrawGrid(); canvas_.DrawOverlay(); } // Legacy method implementations that delegate to components absl::Status DungeonEditor::LoadAndRenderRoomGraphics(int room_id) { if (room_id < 0 || room_id >= rooms_.size()) { return absl::InvalidArgumentError("Invalid room ID"); } return room_loader_.LoadAndRenderRoomGraphics(room_id, rooms_[room_id]); } absl::Status DungeonEditor::ReloadAllRoomGraphics() { return room_loader_.ReloadAllRoomGraphics(rooms_); } absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int room_id) { // This method is deprecated - rendering is handled by DungeonRenderer component return absl::OkStatus(); } } // namespace yaze::editor