From bfcf54e27135fa82963e887401823ad5de041fbe Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 25 Sep 2025 19:56:39 -0400 Subject: [PATCH] Refactor DungeonEditor and introduce new components for enhanced dungeon editing - Integrated new components: DungeonToolset, DungeonObjectInteraction, DungeonRenderer, DungeonRoomLoader, and DungeonUsageTracker to streamline dungeon editing functionalities. - Updated DungeonEditor to utilize the new components for room loading, object interaction, and rendering, improving code organization and maintainability. - Enhanced object selection and placement features, including drag-and-drop functionality and improved UI interactions. - Removed legacy methods and refactored existing code to delegate responsibilities to the new components, ensuring a cleaner architecture. - Added support for usage tracking of blocksets, spritesets, and palettes across dungeon rooms, providing insights for optimization. --- src/app/editor/dungeon/dungeon_editor.cc | 878 +----------------- src/app/editor/dungeon/dungeon_editor.h | 130 +-- .../dungeon/dungeon_object_interaction.cc | 308 ++++++ .../dungeon/dungeon_object_interaction.h | 94 ++ src/app/editor/dungeon/dungeon_renderer.cc | 206 ++++ src/app/editor/dungeon/dungeon_renderer.h | 76 ++ src/app/editor/dungeon/dungeon_room_loader.cc | 127 +++ src/app/editor/dungeon/dungeon_room_loader.h | 56 ++ src/app/editor/dungeon/dungeon_toolset.cc | 151 +++ src/app/editor/dungeon/dungeon_toolset.h | 69 ++ .../editor/dungeon/dungeon_usage_tracker.cc | 87 ++ .../editor/dungeon/dungeon_usage_tracker.h | 57 ++ src/app/editor/editor.cmake | 5 + 13 files changed, 1303 insertions(+), 941 deletions(-) create mode 100644 src/app/editor/dungeon/dungeon_object_interaction.cc create mode 100644 src/app/editor/dungeon/dungeon_object_interaction.h create mode 100644 src/app/editor/dungeon/dungeon_renderer.cc create mode 100644 src/app/editor/dungeon/dungeon_renderer.h create mode 100644 src/app/editor/dungeon/dungeon_room_loader.cc create mode 100644 src/app/editor/dungeon/dungeon_room_loader.h create mode 100644 src/app/editor/dungeon/dungeon_toolset.cc create mode 100644 src/app/editor/dungeon/dungeon_toolset.h create mode 100644 src/app/editor/dungeon/dungeon_usage_tracker.cc create mode 100644 src/app/editor/dungeon/dungeon_usage_tracker.h diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index ef513cfc..f0bfff8d 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -48,44 +48,17 @@ void DungeonEditor::Initialize() { absl::Status DungeonEditor::Load() { auto dungeon_man_pal_group = rom()->palette_group().dungeon_main; - for (int i = 0; i < 0x100 + 40; i++) { - rooms_[i] = zelda3::LoadRoomFromRom(rom_, i); - - auto room_size = zelda3::CalculateRoomSize(rom_, i); - room_size_pointers_.push_back(room_size.room_size_pointer); - room_sizes_.push_back(room_size.room_size); - if (room_size.room_size_pointer != 0x0A8000) { - room_size_addresses_[i] = room_size.room_size_pointer; - } - - rooms_[i].LoadObjects(); - - auto dungeon_palette_ptr = rom()->paletteset_ids[rooms_[i].palette][0]; - auto palette_id = rom()->ReadWord(0xDEC4B + dungeon_palette_ptr); - if (palette_id.status() != absl::OkStatus()) { - continue; - } - int p_id = palette_id.value() / 180; - auto color = dungeon_man_pal_group[p_id][3]; - room_palette_[rooms_[i].palette] = color.rgb(); - } - - LoadDungeonRoomSize(); - // LoadRoomEntrances - for (int i = 0; i < 0x07; ++i) { - entrances_[i] = zelda3::RoomEntrance(rom(), i, true); - } - - for (int i = 0; i < 0x85; ++i) { - entrances_[i + 0x07] = zelda3::RoomEntrance(rom(), i, false); - } + // 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_)); - CalculateUsageStats(); + // Calculate usage statistics + usage_tracker_.CalculateUsageStats(rooms_); // Initialize the new editor system if (dungeon_editor_system_) { @@ -120,9 +93,23 @@ absl::Status DungeonEditor::Load() { [this](const zelda3::RoomObject& object) { preview_object_ = object; object_loaded_ = true; - placement_type_ = - kObject; // Automatically switch to object placement mode + 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(); @@ -179,49 +166,10 @@ absl::Status DungeonEditor::RefreshGraphics() { return absl::OkStatus(); } -void DungeonEditor::LoadDungeonRoomSize() { - std::map> rooms_by_bank; - for (const auto& room : room_size_addresses_) { - int bank = room.second >> 16; - rooms_by_bank[bank].push_back(room.second); - } - - // Process and calculate room sizes within each bank - for (auto& bank_rooms : rooms_by_bank) { - // Sort the rooms within this bank - std::ranges::sort(bank_rooms.second); - - for (size_t i = 0; i < bank_rooms.second.size(); ++i) { - int room_ptr = bank_rooms.second[i]; - - // Identify the room ID for the current room pointer - int room_id = - std::ranges::find_if(room_size_addresses_, [room_ptr]( - const auto& entry) { - return entry.second == room_ptr; - })->first; - - if (room_ptr != 0x0A8000) { - if (i < bank_rooms.second.size() - 1) { - // Calculate size as difference between current room and next room - // in the same bank - room_sizes_[room_id] = bank_rooms.second[i + 1] - room_ptr; - } else { - // Calculate size for the last room in this bank - int bank_end_address = (bank_rooms.first << 16) | 0xFFFF; - room_sizes_[room_id] = bank_end_address - room_ptr + 1; - } - total_room_size_ += room_sizes_[room_id]; - } else { - // Room with address 0x0A8000 - room_sizes_[room_id] = 0x00; - } - } - } -} +// LoadDungeonRoomSize moved to DungeonRoomLoader component absl::Status DungeonEditor::UpdateDungeonRoomView() { - DrawToolset(); + toolset_.Draw(); if (palette_showing_) { ImGui::Begin("Palette Editor", &palette_showing_, 0); @@ -285,150 +233,7 @@ void DungeonEditor::OnRoomSelected(int room_id) { room_selector_.set_active_rooms(active_rooms_); } -void DungeonEditor::DrawToolset() { - if (BeginTable("DWToolset", 16, ImGuiTableFlags_SizingFixedFit, - ImVec2(0, 0))) { - static std::array tool_names = { - "Undo", "Redo", "Separator", "Any", "BG1", "BG2", - "BG3", "Separator", "Object", "Sprite", "Item", "Entrance", - "Door", "Chest", "Block", "Palette"}; - std::ranges::for_each(tool_names, - [](const char* name) { TableSetupColumn(name); }); - - TableNextColumn(); - if (Button(ICON_MD_UNDO)) { - PRINT_IF_ERROR(Undo()); - } - - TableNextColumn(); - if (Button(ICON_MD_REDO)) { - PRINT_IF_ERROR(Redo()); - } - - TableNextColumn(); - Text(ICON_MD_MORE_VERT); - - TableNextColumn(); - if (RadioButton("All", background_type_ == kBackgroundAny)) { - background_type_ = kBackgroundAny; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Show all background layers"); - } - - TableNextColumn(); - if (RadioButton("BG1", background_type_ == kBackground1)) { - background_type_ = kBackground1; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Show background layer 1 only"); - } - - TableNextColumn(); - if (RadioButton("BG2", background_type_ == kBackground2)) { - background_type_ = kBackground2; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Show background layer 2 only"); - } - - TableNextColumn(); - if (RadioButton("BG3", background_type_ == kBackground3)) { - background_type_ = kBackground3; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Show background layer 3 only"); - } - - TableNextColumn(); - Text(ICON_MD_MORE_VERT); - - TableNextColumn(); - if (RadioButton(ICON_MD_SQUARE, placement_type_ == kObject)) { - placement_type_ = kObject; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Objects"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_PEST_CONTROL, placement_type_ == kSprite)) { - placement_type_ = kSprite; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Sprites"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_GRASS, placement_type_ == kItem)) { - placement_type_ = kItem; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Items"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_NAVIGATION, placement_type_ == kEntrance)) { - placement_type_ = kEntrance; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Entrances"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_SENSOR_DOOR, placement_type_ == kDoor)) { - placement_type_ = kDoor; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Doors"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_INVENTORY, placement_type_ == kChest)) { - placement_type_ = kChest; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Chests"); - } - - TableNextColumn(); - if (RadioButton(ICON_MD_VIEW_MODULE, placement_type_ == kBlock)) { - placement_type_ = kBlock; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Blocks"); - } - - TableNextColumn(); - if (Button(ICON_MD_PALETTE)) { - palette_showing_ = !palette_showing_; - } - - ImGui::EndTable(); - } - - ImGui::Separator(); - ImGui::Text( - "Instructions: Click to place objects, Ctrl+Click to select, drag to " - "move"); - - ImGui::SameLine(); - // Usage statistics as a popup to save space - if (ImGui::Button("Usage Statistics")) { - ImGui::OpenPopup("UsageStatsPopup"); - } - - if (ImGui::BeginPopupModal("UsageStatsPopup", nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (is_loaded_) { - DrawUsageStats(); - } - if (ImGui::Button("Close")) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } -} +// DrawToolset() method moved to DungeonToolset component void DungeonEditor::DrawCanvasAndPropertiesPanel() { if (ImGui::BeginTabBar("CanvasPropertiesTabBar")) { @@ -466,12 +271,12 @@ void DungeonEditor::DrawCanvasAndPropertiesPanel() { ImGui::Text("Chests: %zu", room.GetChests().size()); // Selection info - if (!selected_object_indices_.empty()) { + const auto& selected_indices = object_interaction_.GetSelectedObjectIndices(); + if (!selected_indices.empty()) { ImGui::Separator(); - ImGui::Text("Selected Objects: %zu", selected_object_indices_.size()); + ImGui::Text("Selected Objects: %zu", selected_indices.size()); if (ImGui::Button("Clear Selection")) { - selected_object_indices_.clear(); - object_select_active_ = false; + object_interaction_.ClearSelection(); } } @@ -584,9 +389,9 @@ void DungeonEditor::DrawRoomPropertiesDebugPopup() { room.LoadObjects(); } ImGui::SameLine(); - if (ImGui::Button("Clear Cache")) { - object_render_cache_.clear(); - } + if (ImGui::Button("Clear Cache")) { + renderer_.ClearObjectCache(); + } ImGui::SameLine(); if (ImGui::Button("Close")) { ImGui::CloseCurrentPopup(); @@ -702,11 +507,11 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { ImGui::Checkbox("Show Layout Objects", &show_layout_objects); if (ImGui::Button("Clear Object Cache")) { - object_render_cache_.clear(); + renderer_.ClearObjectCache(); } ImGui::SameLine(); - ImGui::Text("Cache: %zu objects", object_render_cache_.size()); + ImGui::Text("Cache: %zu objects", renderer_.GetCacheSize()); // Object statistics and metadata ImGui::Separator(); @@ -721,7 +526,7 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { // Palette information ImGui::Text("Current Palette Group: %llu", static_cast(current_palette_group_id_)); - ImGui::Text("Palette Hash: %#016llx", last_palette_hash_); + ImGui::Text("Cache Size: %zu objects", renderer_.GetCacheSize()); // Object type breakdown ImGui::Separator(); @@ -791,11 +596,11 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { canvas_.DrawBackground(); canvas_.DrawContextMenu(); - // Handle object selection and placement - CheckForObjectSelection(); + // Handle object selection and placement using component + object_interaction_.CheckForObjectSelection(); - // Handle mouse input for drag and select functionality - HandleCanvasMouseInput(); + // 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()) { @@ -805,8 +610,8 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { ImVec2 canvas_mouse_pos = ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y); auto [room_x, room_y] = - CanvasToRoomCoordinates(static_cast(canvas_mouse_pos.x), - static_cast(canvas_mouse_pos.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; } @@ -823,621 +628,40 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) { } // Render background layers with proper positioning - RenderRoomBackgroundLayers(room_id); + 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()) { - RenderObjectInCanvas(object, room_palette); + renderer_.RenderObjectInCanvas(object, room_palette); } } } - // Draw selection box and drag preview - DrawSelectBox(); - DrawDragPreview(); + // Draw selection box and drag preview using component + object_interaction_.DrawSelectBox(); + object_interaction_.DrawDragPreview(); canvas_.DrawGrid(); canvas_.DrawOverlay(); } -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_); - mutable_object.EnsureTilesLoaded(); - - // Check if tiles were loaded successfully - if (mutable_object.tiles().empty()) { - return; // Skip objects without tiles - } - - // Calculate palette hash for caching - uint64_t palette_hash = 0; - for (size_t i = 0; i < palette.size() && i < 16; ++i) { - palette_hash ^= std::hash{}(palette[i].snes()) + 0x9e3779b9 + - (palette_hash << 6) + (palette_hash >> 2); - } - - // 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 - } - - // Check cache first - for (auto& cached : object_render_cache_) { - if (cached.object_id == object.id_ && cached.object_x == object.x_ && - cached.object_y == object.y_ && cached.object_size == object.size_ && - cached.palette_hash == palette_hash && cached.is_valid) { - // Use cached bitmap - canvas handles scrolling internally - canvas_.DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255); - return; - } - } - - // 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 will handle scrolling and coordinate transformation - canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); - - // Cache the rendered bitmap (create a copy for caching) - ObjectRenderCache cache_entry; - cache_entry.object_id = object.id_; - cache_entry.object_x = object.x_; - cache_entry.object_y = object.y_; - cache_entry.object_size = object.size_; - cache_entry.palette_hash = palette_hash; - cache_entry.rendered_bitmap = object_bitmap; // Copy instead of move - cache_entry.is_valid = true; - - // Add to cache (limit cache size) - if (object_render_cache_.size() >= 100) { - object_render_cache_.erase(object_render_cache_.begin()); - } - object_render_cache_.push_back(std::move(cache_entry)); -} - -void DungeonEditor::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 DungeonEditor::RenderLayoutObjects(const zelda3::RoomLayout& layout, - const gfx::SnesPalette& palette) { - // Render layout objects (walls, floors, etc.) as simple colored rectangles - // This provides a visual representation of the room's structure - - 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 - // This is a placeholder - in a real implementation, you'd render the actual - // tile - canvas_.DrawRect(canvas_x, canvas_y, 16, 16, - gui::ConvertSnesColorToImVec4(color)); - } -} - -// Coordinate conversion helper functions -std::pair DungeonEditor::RoomToCanvasCoordinates(int room_x, - int room_y) const { - // Convert room coordinates (16x16 tile units) to canvas coordinates (pixels) - // Note: The canvas applies global_scale_ and scrolling internally, so we - // return the base coordinates and let the canvas handle the transformation - return {room_x * 16, room_y * 16}; -} - -std::pair DungeonEditor::CanvasToRoomCoordinates(int canvas_x, - int canvas_y) const { - // Convert canvas coordinates (pixels) to room coordinates (16x16 tile units) - // Note: This assumes the canvas coordinates are already adjusted for scaling - // and scrolling - return {canvas_x / 16, canvas_y / 16}; -} - -bool DungeonEditor::IsWithinCanvasBounds(int canvas_x, int canvas_y, - int margin) const { - // Check if coordinates are within canvas bounds with optional margin - // Get the actual canvas size (accounting for scaling) - auto canvas_size = canvas_.canvas_size(); - auto global_scale = canvas_.global_scale(); - int scaled_width = static_cast(canvas_size.x * global_scale); - int scaled_height = static_cast(canvas_size.y * global_scale); - - return (canvas_x >= -margin && canvas_y >= -margin && - canvas_x <= scaled_width + margin && - canvas_y <= scaled_height + margin); -} - -// Room graphics management methods +// 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"); } - - 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(); - - return absl::OkStatus(); + return room_loader_.LoadAndRenderRoomGraphics(room_id, rooms_[room_id]); } absl::Status DungeonEditor::ReloadAllRoomGraphics() { - if (!rom_ || !rom_->is_loaded()) { - return absl::FailedPreconditionError("ROM not loaded"); - } - - // Clear existing graphics cache - object_render_cache_.clear(); - - // Reload graphics for all rooms - for (int i = 0; i < rooms_.size(); i++) { - auto status = LoadAndRenderRoomGraphics(i); - if (!status.ok()) { - // Log error but continue with other rooms - continue; - } - } + 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(); } -void DungeonEditor::RenderRoomBackgroundLayers(int room_id) { - if (room_id < 0 || room_id >= rooms_.size()) { - return; - } - - auto& room = rooms_[room_id]; - - // Get canvas dimensions - int canvas_width = canvas_.width(); - int canvas_height = canvas_.height(); - - // BG1 (background layer 1) - 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 - } -} - -void DungeonEditor::CalculateUsageStats() { - for (const auto& room : rooms_) { - if (blockset_usage_.find(room.blockset) == blockset_usage_.end()) { - blockset_usage_[room.blockset] = 1; - } else { - blockset_usage_[room.blockset] += 1; - } - - if (spriteset_usage_.find(room.spriteset) == spriteset_usage_.end()) { - spriteset_usage_[room.spriteset] = 1; - } else { - spriteset_usage_[room.spriteset] += 1; - } - - if (palette_usage_.find(room.palette) == palette_usage_.end()) { - palette_usage_[room.palette] = 1; - } else { - palette_usage_[room.palette] += 1; - } - } -} - -void DungeonEditor::DrawUsageStats() { - if (ImGui::Button("Refresh")) { - selected_blockset_ = 0xFFFF; - selected_spriteset_ = 0xFFFF; - selected_palette_ = 0xFFFF; - spriteset_usage_.clear(); - blockset_usage_.clear(); - palette_usage_.clear(); - CalculateUsageStats(); - } - - ImGui::Text("Usage Statistics"); - ImGui::Separator(); - - ImGui::Text("Blocksets: %zu used", blockset_usage_.size()); - ImGui::Text("Spritesets: %zu used", spriteset_usage_.size()); - ImGui::Text("Palettes: %zu used", palette_usage_.size()); -} - -// Drag and select box functionality -void DungeonEditor::HandleCanvasMouseInput() { - const ImGuiIO& io = ImGui::GetIO(); - - // Check if mouse is over the canvas - if (!canvas_.IsMouseHovering()) { - return; - } - - // Get mouse position relative to canvas - ImVec2 mouse_pos = io.MousePos; - ImVec2 canvas_pos = canvas_.zero_point(); - ImVec2 canvas_size = canvas_.canvas_size(); - - // Convert to canvas coordinates - ImVec2 canvas_mouse_pos = - ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y); - - // Handle mouse clicks - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || - ImGui::IsKeyDown(ImGuiKey_RightCtrl)) { - // Start selection box - is_selecting_ = true; - select_start_pos_ = canvas_mouse_pos; - select_current_pos_ = canvas_mouse_pos; - selected_objects_.clear(); - } else { - // Start dragging or place object - if (object_loaded_) { - // Convert canvas coordinates to room coordinates - auto [room_x, room_y] = - CanvasToRoomCoordinates(static_cast(canvas_mouse_pos.x), - static_cast(canvas_mouse_pos.y)); - PlaceObjectAtPosition(room_x, room_y); - } else { - // Start dragging existing objects - is_dragging_ = true; - drag_start_pos_ = canvas_mouse_pos; - drag_current_pos_ = canvas_mouse_pos; - } - } - } - - // Handle mouse drag - if (is_selecting_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - select_current_pos_ = canvas_mouse_pos; - UpdateSelectedObjects(); - } - - if (is_dragging_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - drag_current_pos_ = canvas_mouse_pos; - DrawDragPreview(); - } - - // Handle mouse release - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - if (is_selecting_) { - is_selecting_ = false; - UpdateSelectedObjects(); - } - if (is_dragging_) { - is_dragging_ = false; - // TODO: Apply drag transformation to selected objects - } - } -} - -void DungeonEditor::DrawSelectBox() { - if (!is_selecting_) return; - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 canvas_pos = canvas_.zero_point(); - - // Calculate select box bounds - ImVec2 start = ImVec2( - canvas_pos.x + std::min(select_start_pos_.x, select_current_pos_.x), - canvas_pos.y + std::min(select_start_pos_.y, select_current_pos_.y)); - ImVec2 end = ImVec2( - canvas_pos.x + std::max(select_start_pos_.x, select_current_pos_.x), - canvas_pos.y + std::max(select_start_pos_.y, select_current_pos_.y)); - - // Draw selection box - draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); - draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32)); -} - -void DungeonEditor::DrawDragPreview() { - if (!is_dragging_) return; - - // Draw drag preview for selected objects - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 canvas_pos = canvas_.zero_point(); - ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x, - drag_current_pos_.y - drag_start_pos_.y); - - // Draw preview of where objects would be moved - for (int obj_id : selected_objects_) { - // TODO: Draw preview of object at new position - // This would require getting the object's current position and drawing it - // offset by drag_delta - } -} - -void DungeonEditor::UpdateSelectedObjects() { - if (!is_selecting_) return; - - selected_objects_.clear(); - - // Get current room - 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()) return; - - auto& room = rooms_[current_room]; - - // Check each object in the room - for (const auto& object : room.GetTileObjects()) { - if (IsObjectInSelectBox(object)) { - selected_objects_.push_back(object.id_); - } - } -} - -bool DungeonEditor::IsObjectInSelectBox( - const zelda3::RoomObject& object) const { - if (!is_selecting_) return false; - - // Convert object position to canvas coordinates - auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_); - - // Calculate select box bounds - float min_x = std::min(select_start_pos_.x, select_current_pos_.x); - float max_x = std::max(select_start_pos_.x, select_current_pos_.x); - float min_y = std::min(select_start_pos_.y, select_current_pos_.y); - float max_y = std::max(select_start_pos_.y, select_current_pos_.y); - - // Check if object is within select box - return (canvas_x >= min_x && canvas_x <= max_x && canvas_y >= min_y && - canvas_y <= max_y); -} - -void DungeonEditor::PlaceObjectAtPosition(int room_x, int room_y) { - if (!object_loaded_ || preview_object_.id_ < 0) return; - - // Get current room - 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()) return; - - // Create new object at the specified position - auto new_object = preview_object_; - new_object.x_ = room_x; - new_object.y_ = room_y; - new_object.set_rom(rom_); - new_object.EnsureTilesLoaded(); - - // Add object to room - auto& room = rooms_[current_room]; - room.AddTileObject(new_object); - - // Clear the object render cache to force redraw - object_render_cache_.clear(); - - // Update the object selector with the new object count - object_selector_.set_current_room_id(current_room); -} - -void DungeonEditor::CheckForObjectSelection() { - // Draw object selection rectangle similar to OverworldEditor - DrawObjectSelectRect(); - - // Handle object selection when rectangle is active - if (object_select_active_) { - SelectObjectsInRect(); - } -} - -void DungeonEditor::DrawObjectSelectRect() { - if (!canvas_.IsMouseHovering()) return; - - const ImGuiIO& io = ImGui::GetIO(); - const ImVec2 canvas_pos = canvas_.zero_point(); - const ImVec2 mouse_pos = - ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y); - - static bool dragging = false; - static ImVec2 drag_start_pos; - - // Right click to start object selection - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !object_loaded_) { - drag_start_pos = mouse_pos; - object_select_start_ = mouse_pos; - selected_object_indices_.clear(); - object_select_active_ = false; - dragging = false; - } - - // Right drag to create selection rectangle - if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && !object_loaded_) { - object_select_end_ = mouse_pos; - dragging = true; - - // Draw selection rectangle - ImVec2 start = - ImVec2(canvas_pos.x + std::min(drag_start_pos.x, mouse_pos.x), - canvas_pos.y + std::min(drag_start_pos.y, mouse_pos.y)); - ImVec2 end = ImVec2(canvas_pos.x + std::max(drag_start_pos.x, mouse_pos.x), - canvas_pos.y + std::max(drag_start_pos.y, mouse_pos.y)); - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); - draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32)); - } - - // Complete selection on mouse release - if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { - dragging = false; - object_select_active_ = true; - SelectObjectsInRect(); - } -} - -void DungeonEditor::SelectObjectsInRect() { - 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()) return; - - auto& room = rooms_[current_room]; - selected_object_indices_.clear(); - - // Calculate selection bounds in room coordinates - auto [start_room_x, start_room_y] = CanvasToRoomCoordinates( - static_cast(std::min(object_select_start_.x, object_select_end_.x)), - static_cast(std::min(object_select_start_.y, object_select_end_.y))); - auto [end_room_x, end_room_y] = CanvasToRoomCoordinates( - static_cast(std::max(object_select_start_.x, object_select_end_.x)), - static_cast(std::max(object_select_start_.y, object_select_end_.y))); - - // Find objects within selection rectangle - const auto& objects = room.GetTileObjects(); - for (size_t i = 0; i < objects.size(); ++i) { - const auto& object = objects[i]; - if (object.x_ >= start_room_x && object.x_ <= end_room_x && - object.y_ >= start_room_y && object.y_ <= end_room_y) { - selected_object_indices_.push_back(i); - } - } - - // Highlight selected objects - if (!selected_object_indices_.empty()) { - for (size_t index : selected_object_indices_) { - if (index < objects.size()) { - const auto& object = objects[index]; - auto [canvas_x, canvas_y] = - RoomToCanvasCoordinates(object.x_, object.y_); - - // Draw selection highlight - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 canvas_pos = canvas_.zero_point(); - ImVec2 obj_start(canvas_pos.x + canvas_x - 2, - canvas_pos.y + canvas_y - 2); - ImVec2 obj_end(canvas_pos.x + canvas_x + 18, - canvas_pos.y + canvas_y + 18); - draw_list->AddRect(obj_start, obj_end, IM_COL32(0, 255, 255, 255), 0.0f, - 0, 2.0f); - } - } - } -} - } // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index d86fa888..58b0075f 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -17,6 +17,11 @@ #include "dungeon_room_selector.h" #include "dungeon_canvas_viewer.h" #include "dungeon_object_selector.h" +#include "dungeon_toolset.h" +#include "dungeon_object_interaction.h" +#include "dungeon_renderer.h" +#include "dungeon_room_loader.h" +#include "dungeon_usage_tracker.h" namespace yaze { namespace editor { @@ -46,7 +51,8 @@ class DungeonEditor : public Editor { public: explicit DungeonEditor(Rom* rom = nullptr) : rom_(rom), object_renderer_(rom), preview_object_(0, 0, 0, 0, 0), - room_selector_(rom), canvas_viewer_(rom), object_selector_(rom) { + room_selector_(rom), canvas_viewer_(rom), object_selector_(rom), + object_interaction_(&canvas_), renderer_(&canvas_, rom), room_loader_(rom) { type_ = EditorType::kDungeon; // Initialize the new dungeon editor system if (rom) { @@ -84,8 +90,6 @@ class DungeonEditor : public Editor { absl::Status UpdateDungeonRoomView(); - void DrawToolset(); - void DrawDungeonTabView(); void DrawDungeonCanvas(int room_id); @@ -100,113 +104,20 @@ class DungeonEditor : public Editor { void DrawTileSelector(); void DrawObjectRenderer(); - // Object rendering methods - 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); - - // New editing mode interfaces - void DrawObjectEditor(); - void DrawSpriteEditor(); - void DrawItemEditor(); - void DrawEntranceEditor(); - void DrawDoorEditor(); - void DrawChestEditor(); - void DrawPropertiesEditor(); - - // 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; - - // Drag and select box functionality - void HandleCanvasMouseInput(); - void DrawSelectBox(); - void DrawDragPreview(); - void UpdateSelectedObjects(); - bool IsObjectInSelectBox(const zelda3::RoomObject& object) const; - void PlaceObjectAtPosition(int room_x, int room_y); - - // Object selection rectangle (like OverworldEditor) - void CheckForObjectSelection(); - void DrawObjectSelectRect(); - void SelectObjectsInRect(); - - // Room graphics management + // Legacy methods (delegated to components) absl::Status LoadAndRenderRoomGraphics(int room_id); absl::Status ReloadAllRoomGraphics(); absl::Status UpdateRoomBackgroundLayers(int room_id); - void RenderRoomBackgroundLayers(int room_id); - // Object rendering cache to avoid re-rendering the same objects - 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; - // Object preview system zelda3::RoomObject preview_object_; gfx::SnesPalette preview_palette_; - void CalculateUsageStats(); - void DrawUsageStats(); - void DrawUsageGrid(); - void RenderSetUsage(const absl::flat_hash_map& usage_map, - uint16_t& selected_set, int spriteset_offset = 0x00); - - enum BackgroundType { - kNoBackground, - kBackground1, - kBackground2, - kBackground3, - kBackgroundAny, - }; - - // Updated placement types to match new editor system - enum PlacementType { - kNoType, - kObject, // Object editing mode - kSprite, // Sprite editing mode - kItem, // Item placement mode - kEntrance, // Entrance/exit editing mode - kDoor, // Door configuration mode - kChest, // Chest management mode - kBlock // Legacy block mode - }; - - int background_type_ = kNoBackground; - int placement_type_ = kNoType; - bool is_loaded_ = false; bool object_loaded_ = false; bool palette_showing_ = false; bool refresh_graphics_ = false; - // Drag and select box infrastructure - bool is_dragging_ = false; - bool is_selecting_ = false; - ImVec2 drag_start_pos_; - ImVec2 drag_current_pos_; - ImVec2 select_start_pos_; - ImVec2 select_current_pos_; - std::vector selected_objects_; - int current_layer_ = 0; // 0 = BG1, 1 = BG2, 2 = Both - - // Object selection rectangle (like OverworldEditor) - bool object_select_active_ = false; - ImVec2 object_select_start_; - ImVec2 object_select_end_; - std::vector selected_object_indices_; - // New editor system integration std::unique_ptr dungeon_editor_system_; std::shared_ptr object_editor_; @@ -243,26 +154,17 @@ class DungeonEditor : public Editor { std::array entrances_ = {}; zelda3::ObjectRenderer object_renderer_; - // New UI components + // UI components DungeonRoomSelector room_selector_; DungeonCanvasViewer canvas_viewer_; DungeonObjectSelector object_selector_; - - absl::flat_hash_map spriteset_usage_; - absl::flat_hash_map blockset_usage_; - absl::flat_hash_map palette_usage_; - - std::vector room_size_pointers_; - std::vector room_sizes_; - - uint16_t selected_blockset_ = 0xFFFF; // 0xFFFF indicates no selection - uint16_t selected_spriteset_ = 0xFFFF; - uint16_t selected_palette_ = 0xFFFF; - - uint64_t total_room_size_ = 0; - - std::unordered_map room_size_addresses_; - std::unordered_map room_palette_; + + // Refactored components + DungeonToolset toolset_; + DungeonObjectInteraction object_interaction_; + DungeonRenderer renderer_; + DungeonRoomLoader room_loader_; + DungeonUsageTracker usage_tracker_; absl::Status status_; diff --git a/src/app/editor/dungeon/dungeon_object_interaction.cc b/src/app/editor/dungeon/dungeon_object_interaction.cc new file mode 100644 index 00000000..31c2f0ae --- /dev/null +++ b/src/app/editor/dungeon/dungeon_object_interaction.cc @@ -0,0 +1,308 @@ +#include "dungeon_object_interaction.h" + +#include "app/gui/color.h" +#include "imgui/imgui.h" + +namespace yaze::editor { + +void DungeonObjectInteraction::HandleCanvasMouseInput() { + const ImGuiIO& io = ImGui::GetIO(); + + // Check if mouse is over the canvas + if (!canvas_->IsMouseHovering()) { + return; + } + + // Get mouse position relative to canvas + ImVec2 mouse_pos = io.MousePos; + ImVec2 canvas_pos = canvas_->zero_point(); + ImVec2 canvas_size = canvas_->canvas_size(); + + // Convert to canvas coordinates + ImVec2 canvas_mouse_pos = + ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y); + + // Handle mouse clicks + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || + ImGui::IsKeyDown(ImGuiKey_RightCtrl)) { + // Start selection box + is_selecting_ = true; + select_start_pos_ = canvas_mouse_pos; + select_current_pos_ = canvas_mouse_pos; + selected_objects_.clear(); + } else { + // Start dragging or place object + if (object_loaded_) { + // Convert canvas coordinates to room coordinates + auto [room_x, room_y] = + CanvasToRoomCoordinates(static_cast(canvas_mouse_pos.x), + static_cast(canvas_mouse_pos.y)); + PlaceObjectAtPosition(room_x, room_y); + } else { + // Start dragging existing objects + is_dragging_ = true; + drag_start_pos_ = canvas_mouse_pos; + drag_current_pos_ = canvas_mouse_pos; + } + } + } + + // Handle mouse drag + if (is_selecting_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + select_current_pos_ = canvas_mouse_pos; + UpdateSelectedObjects(); + } + + if (is_dragging_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + drag_current_pos_ = canvas_mouse_pos; + DrawDragPreview(); + } + + // Handle mouse release + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + if (is_selecting_) { + is_selecting_ = false; + UpdateSelectedObjects(); + } + if (is_dragging_) { + is_dragging_ = false; + // TODO: Apply drag transformation to selected objects + } + } +} + +void DungeonObjectInteraction::CheckForObjectSelection() { + // Draw object selection rectangle similar to OverworldEditor + DrawObjectSelectRect(); + + // Handle object selection when rectangle is active + if (object_select_active_) { + SelectObjectsInRect(); + } +} + +void DungeonObjectInteraction::DrawObjectSelectRect() { + if (!canvas_->IsMouseHovering()) return; + + const ImGuiIO& io = ImGui::GetIO(); + const ImVec2 canvas_pos = canvas_->zero_point(); + const ImVec2 mouse_pos = + ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y); + + static bool dragging = false; + static ImVec2 drag_start_pos; + + // Right click to start object selection + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !object_loaded_) { + drag_start_pos = mouse_pos; + object_select_start_ = mouse_pos; + selected_object_indices_.clear(); + object_select_active_ = false; + dragging = false; + } + + // Right drag to create selection rectangle + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && !object_loaded_) { + object_select_end_ = mouse_pos; + dragging = true; + + // Draw selection rectangle + ImVec2 start = + ImVec2(canvas_pos.x + std::min(drag_start_pos.x, mouse_pos.x), + canvas_pos.y + std::min(drag_start_pos.y, mouse_pos.y)); + ImVec2 end = ImVec2(canvas_pos.x + std::max(drag_start_pos.x, mouse_pos.x), + canvas_pos.y + std::max(drag_start_pos.y, mouse_pos.y)); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32)); + } + + // Complete selection on mouse release + if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + dragging = false; + object_select_active_ = true; + SelectObjectsInRect(); + } +} + +void DungeonObjectInteraction::SelectObjectsInRect() { + if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296) return; + + auto& room = (*rooms_)[current_room_id_]; + selected_object_indices_.clear(); + + // Calculate selection bounds in room coordinates + auto [start_room_x, start_room_y] = CanvasToRoomCoordinates( + static_cast(std::min(object_select_start_.x, object_select_end_.x)), + static_cast(std::min(object_select_start_.y, object_select_end_.y))); + auto [end_room_x, end_room_y] = CanvasToRoomCoordinates( + static_cast(std::max(object_select_start_.x, object_select_end_.x)), + static_cast(std::max(object_select_start_.y, object_select_end_.y))); + + // Find objects within selection rectangle + const auto& objects = room.GetTileObjects(); + for (size_t i = 0; i < objects.size(); ++i) { + const auto& object = objects[i]; + if (object.x_ >= start_room_x && object.x_ <= end_room_x && + object.y_ >= start_room_y && object.y_ <= end_room_y) { + selected_object_indices_.push_back(i); + } + } + + // Highlight selected objects + if (!selected_object_indices_.empty()) { + for (size_t index : selected_object_indices_) { + if (index < objects.size()) { + const auto& object = objects[index]; + auto [canvas_x, canvas_y] = + RoomToCanvasCoordinates(object.x_, object.y_); + + // Draw selection highlight + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 canvas_pos = canvas_->zero_point(); + ImVec2 obj_start(canvas_pos.x + canvas_x - 2, + canvas_pos.y + canvas_y - 2); + ImVec2 obj_end(canvas_pos.x + canvas_x + 18, + canvas_pos.y + canvas_y + 18); + draw_list->AddRect(obj_start, obj_end, IM_COL32(0, 255, 255, 255), 0.0f, + 0, 2.0f); + } + } + } +} + +void DungeonObjectInteraction::PlaceObjectAtPosition(int room_x, int room_y) { + if (!object_loaded_ || preview_object_.id_ < 0 || !rooms_) return; + + if (current_room_id_ < 0 || current_room_id_ >= 296) return; + + // Create new object at the specified position + auto new_object = preview_object_; + new_object.x_ = room_x; + new_object.y_ = room_y; + + // Add object to room + auto& room = (*rooms_)[current_room_id_]; + room.AddTileObject(new_object); + + // Notify callback if set + if (object_placed_callback_) { + object_placed_callback_(new_object); + } + + // Trigger cache invalidation + if (cache_invalidation_callback_) { + cache_invalidation_callback_(); + } +} + +void DungeonObjectInteraction::DrawSelectBox() { + if (!is_selecting_) return; + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 canvas_pos = canvas_->zero_point(); + + // Calculate select box bounds + ImVec2 start = ImVec2( + canvas_pos.x + std::min(select_start_pos_.x, select_current_pos_.x), + canvas_pos.y + std::min(select_start_pos_.y, select_current_pos_.y)); + ImVec2 end = ImVec2( + canvas_pos.x + std::max(select_start_pos_.x, select_current_pos_.x), + canvas_pos.y + std::max(select_start_pos_.y, select_current_pos_.y)); + + // Draw selection box + draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32)); +} + +void DungeonObjectInteraction::DrawDragPreview() { + if (!is_dragging_) return; + + // Draw drag preview for selected objects + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 canvas_pos = canvas_->zero_point(); + ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x, + drag_current_pos_.y - drag_start_pos_.y); + + // Draw preview of where objects would be moved + for (int obj_id : selected_objects_) { + // TODO: Draw preview of object at new position + // This would require getting the object's current position and drawing it + // offset by drag_delta + } +} + +void DungeonObjectInteraction::UpdateSelectedObjects() { + if (!is_selecting_ || !rooms_) return; + + selected_objects_.clear(); + + if (current_room_id_ < 0 || current_room_id_ >= 296) return; + + auto& room = (*rooms_)[current_room_id_]; + + // Check each object in the room + for (const auto& object : room.GetTileObjects()) { + if (IsObjectInSelectBox(object)) { + selected_objects_.push_back(object.id_); + } + } +} + +bool DungeonObjectInteraction::IsObjectInSelectBox( + const zelda3::RoomObject& object) const { + if (!is_selecting_) return false; + + // Convert object position to canvas coordinates + auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_); + + // Calculate select box bounds + float min_x = std::min(select_start_pos_.x, select_current_pos_.x); + float max_x = std::max(select_start_pos_.x, select_current_pos_.x); + float min_y = std::min(select_start_pos_.y, select_current_pos_.y); + float max_y = std::max(select_start_pos_.y, select_current_pos_.y); + + // Check if object is within select box + return (canvas_x >= min_x && canvas_x <= max_x && canvas_y >= min_y && + canvas_y <= max_y); +} + +std::pair DungeonObjectInteraction::RoomToCanvasCoordinates(int room_x, int room_y) const { + return {room_x * 16, room_y * 16}; +} + +std::pair DungeonObjectInteraction::CanvasToRoomCoordinates(int canvas_x, int canvas_y) const { + return {canvas_x / 16, canvas_y / 16}; +} + +bool DungeonObjectInteraction::IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin) const { + auto canvas_size = canvas_->canvas_size(); + auto global_scale = canvas_->global_scale(); + int scaled_width = static_cast(canvas_size.x * global_scale); + int scaled_height = static_cast(canvas_size.y * global_scale); + + return (canvas_x >= -margin && canvas_y >= -margin && + canvas_x <= scaled_width + margin && + canvas_y <= scaled_height + margin); +} + +void DungeonObjectInteraction::SetCurrentRoom(std::array* rooms, int room_id) { + rooms_ = rooms; + current_room_id_ = room_id; +} + +void DungeonObjectInteraction::SetPreviewObject(const zelda3::RoomObject& object, bool loaded) { + preview_object_ = object; + object_loaded_ = loaded; +} + +void DungeonObjectInteraction::ClearSelection() { + selected_object_indices_.clear(); + object_select_active_ = false; + is_selecting_ = false; + is_dragging_ = false; +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_object_interaction.h b/src/app/editor/dungeon/dungeon_object_interaction.h new file mode 100644 index 00000000..0500801c --- /dev/null +++ b/src/app/editor/dungeon/dungeon_object_interaction.h @@ -0,0 +1,94 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H + +#include +#include + +#include "imgui/imgui.h" +#include "app/gui/canvas.h" +#include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/room_object.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles object selection, placement, and interaction within the dungeon canvas + * + * This component manages mouse interactions for object selection (similar to OverworldEditor), + * object placement, drag operations, and multi-object selection. + */ +class DungeonObjectInteraction { + public: + explicit DungeonObjectInteraction(gui::Canvas* canvas) : canvas_(canvas) {} + + // Main interaction handling + void HandleCanvasMouseInput(); + void CheckForObjectSelection(); + void PlaceObjectAtPosition(int room_x, int room_y); + + // Selection rectangle (like OverworldEditor) + void DrawObjectSelectRect(); + void SelectObjectsInRect(); + + // Drag and select box functionality + void DrawSelectBox(); + void DrawDragPreview(); + void UpdateSelectedObjects(); + bool IsObjectInSelectBox(const zelda3::RoomObject& object) const; + + // Coordinate conversion + 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; + + // State management + void SetCurrentRoom(std::array* rooms, int room_id); + void SetPreviewObject(const zelda3::RoomObject& object, bool loaded); + + // Selection state + const std::vector& GetSelectedObjectIndices() const { return selected_object_indices_; } + bool IsObjectSelectActive() const { return object_select_active_; } + void ClearSelection(); + + // Callbacks + void SetObjectPlacedCallback(std::function callback) { + object_placed_callback_ = callback; + } + void SetCacheInvalidationCallback(std::function callback) { + cache_invalidation_callback_ = callback; + } + + private: + gui::Canvas* canvas_; + std::array* rooms_ = nullptr; + int current_room_id_ = 0; + + // Preview object state + zelda3::RoomObject preview_object_{0, 0, 0, 0, 0}; + bool object_loaded_ = false; + + // Drag and select infrastructure + bool is_dragging_ = false; + bool is_selecting_ = false; + ImVec2 drag_start_pos_; + ImVec2 drag_current_pos_; + ImVec2 select_start_pos_; + ImVec2 select_current_pos_; + std::vector selected_objects_; + + // Object selection rectangle (like OverworldEditor) + bool object_select_active_ = false; + ImVec2 object_select_start_; + ImVec2 object_select_end_; + std::vector selected_object_indices_; + + // Callbacks + std::function object_placed_callback_; + std::function cache_invalidation_callback_; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H diff --git a/src/app/editor/dungeon/dungeon_renderer.cc b/src/app/editor/dungeon/dungeon_renderer.cc new file mode 100644 index 00000000..fc063e3b --- /dev/null +++ b/src/app/editor/dungeon/dungeon_renderer.cc @@ -0,0 +1,206 @@ +#include "dungeon_renderer.h" + +#include "absl/strings/str_format.h" +#include "app/core/window.h" +#include "app/gfx/arena.h" +#include "app/gui/color.h" + +namespace yaze::editor { + +using core::Renderer; + +void DungeonRenderer::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 + } + + // Calculate palette hash for caching + uint64_t palette_hash = 0; + for (size_t i = 0; i < palette.size() && i < 16; ++i) { + palette_hash ^= std::hash{}(palette[i].snes()) + 0x9e3779b9 + + (palette_hash << 6) + (palette_hash >> 2); + } + + // Convert room coordinates to canvas coordinates + auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_); + + // Check if object is within canvas bounds + if (!IsWithinCanvasBounds(canvas_x, canvas_y, 32)) { + return; // Skip objects outside visible area + } + + // Check cache first + for (auto& cached : object_render_cache_) { + if (cached.object_id == object.id_ && cached.object_x == object.x_ && + cached.object_y == object.y_ && cached.object_size == object.size_ && + cached.palette_hash == palette_hash && cached.is_valid) { + canvas_->DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255); + return; + } + } + + // 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()); + object_bitmap.SetPalette(palette); + core::Renderer::Get().RenderBitmap(&object_bitmap); + + // Draw the object bitmap to the canvas + canvas_->DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); + + // Cache the rendered bitmap + ObjectRenderCache cache_entry; + cache_entry.object_id = object.id_; + cache_entry.object_x = object.x_; + cache_entry.object_y = object.y_; + cache_entry.object_size = object.size_; + cache_entry.palette_hash = palette_hash; + cache_entry.rendered_bitmap = object_bitmap; + cache_entry.is_valid = true; + + // Add to cache (limit cache size) + if (object_render_cache_.size() >= 100) { + object_render_cache_.erase(object_render_cache_.begin()); + } + object_render_cache_.push_back(std::move(cache_entry)); +} + +void DungeonRenderer::DisplayObjectInfo(const zelda3::RoomObject& object, + int canvas_x, int canvas_y) { + std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_, + object.x_, object.y_, object.size_); + canvas_->DrawText(info_text, canvas_x, canvas_y - 12); +} + +void DungeonRenderer::RenderLayoutObjects(const zelda3::RoomLayout& layout, + const gfx::SnesPalette& palette) { + for (const auto& layout_obj : layout.GetObjects()) { + auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(layout_obj.x(), layout_obj.y()); + + if (!IsWithinCanvasBounds(canvas_x, canvas_y, 16)) { + continue; + } + + // 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; + } + + canvas_->DrawRect(canvas_x, canvas_y, 16, 16, + gui::ConvertSnesColorToImVec4(color)); + } +} + +void DungeonRenderer::RenderRoomBackgroundLayers(int 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; + } + + // 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) { + 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) { + 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); + } +} + +absl::Status DungeonRenderer::RefreshGraphics(int room_id, uint64_t palette_id, + const gfx::PaletteGroup& palette_group) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // This would need access to room data - will be called from main editor + return absl::OkStatus(); +} + +std::pair DungeonRenderer::RoomToCanvasCoordinates(int room_x, int room_y) const { + return {room_x * 16, room_y * 16}; +} + +std::pair DungeonRenderer::CanvasToRoomCoordinates(int canvas_x, int canvas_y) const { + return {canvas_x / 16, canvas_y / 16}; +} + +bool DungeonRenderer::IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin) const { + auto canvas_size = canvas_->canvas_size(); + auto global_scale = canvas_->global_scale(); + int scaled_width = static_cast(canvas_size.x * global_scale); + int scaled_height = static_cast(canvas_size.y * global_scale); + + return (canvas_x >= -margin && canvas_y >= -margin && + canvas_x <= scaled_width + margin && + canvas_y <= scaled_height + margin); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_renderer.h b/src/app/editor/dungeon/dungeon_renderer.h new file mode 100644 index 00000000..7ea196fa --- /dev/null +++ b/src/app/editor/dungeon/dungeon_renderer.h @@ -0,0 +1,76 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H + +#include + +#include "absl/status/status.h" +#include "app/gfx/snes_palette.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/zelda3/dungeon/room_layout.h" +#include "app/zelda3/dungeon/room_object.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles rendering of dungeon objects, layouts, and backgrounds + * + * This component manages all rendering operations for the dungeon editor, + * including object caching, background layers, and layout visualization. + */ +class DungeonRenderer { + public: + explicit DungeonRenderer(gui::Canvas* canvas, Rom* rom) + : canvas_(canvas), rom_(rom), object_renderer_(rom) {} + + // Object rendering + 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); + + // Background rendering + void RenderRoomBackgroundLayers(int room_id); + absl::Status RefreshGraphics(int room_id, uint64_t palette_id, + const gfx::PaletteGroup& palette_group); + + // Graphics management + absl::Status LoadAndRenderRoomGraphics(int room_id, + std::array& rooms); + absl::Status ReloadAllRoomGraphics(std::array& rooms); + + // Cache management + void ClearObjectCache() { object_render_cache_.clear(); } + size_t GetCacheSize() const { return object_render_cache_.size(); } + + // 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; + + private: + gui::Canvas* canvas_; + Rom* rom_; + zelda3::ObjectRenderer object_renderer_; + + // 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 // YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H diff --git a/src/app/editor/dungeon/dungeon_room_loader.cc b/src/app/editor/dungeon/dungeon_room_loader.cc new file mode 100644 index 00000000..b366ed05 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_room_loader.cc @@ -0,0 +1,127 @@ +#include "dungeon_room_loader.h" + +#include +#include + +#include "app/gfx/snes_palette.h" +#include "app/zelda3/dungeon/room.h" + +namespace yaze::editor { + +absl::Status DungeonRoomLoader::LoadAllRooms(std::array& rooms) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + auto dungeon_man_pal_group = rom_->palette_group().dungeon_main; + + for (int i = 0; i < 0x100 + 40; i++) { + rooms[i] = zelda3::LoadRoomFromRom(rom_, i); + + auto room_size = zelda3::CalculateRoomSize(rom_, i); + room_size_pointers_.push_back(room_size.room_size_pointer); + room_sizes_.push_back(room_size.room_size); + if (room_size.room_size_pointer != 0x0A8000) { + room_size_addresses_[i] = room_size.room_size_pointer; + } + + rooms[i].LoadObjects(); + + auto dungeon_palette_ptr = rom_->paletteset_ids[rooms[i].palette][0]; + auto palette_id = rom_->ReadWord(0xDEC4B + dungeon_palette_ptr); + if (palette_id.status() != absl::OkStatus()) { + continue; + } + int p_id = palette_id.value() / 180; + auto color = dungeon_man_pal_group[p_id][3]; + room_palette_[rooms[i].palette] = color.rgb(); + } + + LoadDungeonRoomSize(); + return absl::OkStatus(); +} + +absl::Status DungeonRoomLoader::LoadRoomEntrances(std::array& entrances) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Load entrances + for (int i = 0; i < 0x07; ++i) { + entrances[i] = zelda3::RoomEntrance(rom_, i, true); + } + + for (int i = 0; i < 0x85; ++i) { + entrances[i + 0x07] = zelda3::RoomEntrance(rom_, i, false); + } + + return absl::OkStatus(); +} + +void DungeonRoomLoader::LoadDungeonRoomSize() { + std::map> rooms_by_bank; + for (const auto& room : room_size_addresses_) { + int bank = room.second >> 16; + rooms_by_bank[bank].push_back(room.second); + } + + // Process and calculate room sizes within each bank + for (auto& bank_rooms : rooms_by_bank) { + std::ranges::sort(bank_rooms.second); + + for (size_t i = 0; i < bank_rooms.second.size(); ++i) { + int room_ptr = bank_rooms.second[i]; + + // Identify the room ID for the current room pointer + int room_id = + std::ranges::find_if(room_size_addresses_, [room_ptr]( + const auto& entry) { + return entry.second == room_ptr; + })->first; + + if (room_ptr != 0x0A8000) { + if (i < bank_rooms.second.size() - 1) { + room_sizes_[room_id] = bank_rooms.second[i + 1] - room_ptr; + } else { + int bank_end_address = (bank_rooms.first << 16) | 0xFFFF; + room_sizes_[room_id] = bank_end_address - room_ptr + 1; + } + total_room_size_ += room_sizes_[room_id]; + } else { + room_sizes_[room_id] = 0x00; + } + } + } +} + +absl::Status DungeonRoomLoader::LoadAndRenderRoomGraphics(int room_id, zelda3::Room& room) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Load room graphics with proper blockset + room.LoadRoomGraphics(room.blockset); + + // Render the room graphics to the graphics arena + room.RenderRoomGraphics(); + + return absl::OkStatus(); +} + +absl::Status DungeonRoomLoader::ReloadAllRoomGraphics(std::array& rooms) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Reload graphics for all rooms + for (size_t i = 0; i < rooms.size(); ++i) { + auto status = LoadAndRenderRoomGraphics(static_cast(i), rooms[i]); + if (!status.ok()) { + continue; // Log error but continue with other rooms + } + } + + return absl::OkStatus(); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_room_loader.h b/src/app/editor/dungeon/dungeon_room_loader.h new file mode 100644 index 00000000..36b5eec8 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_room_loader.h @@ -0,0 +1,56 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H + +#include +#include + +#include "absl/status/status.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/room_entrance.h" + +namespace yaze { +namespace editor { + +/** + * @brief Manages loading and saving of dungeon room data + * + * This component handles all ROM-related operations for loading room data, + * calculating room sizes, and managing room graphics. + */ +class DungeonRoomLoader { + public: + explicit DungeonRoomLoader(Rom* rom) : rom_(rom) {} + + // Room loading + absl::Status LoadAllRooms(std::array& rooms); + absl::Status LoadRoomEntrances(std::array& entrances); + + // Room size management + void LoadDungeonRoomSize(); + uint64_t GetTotalRoomSize() const { return total_room_size_; } + + // Room graphics + absl::Status LoadAndRenderRoomGraphics(int room_id, zelda3::Room& room); + absl::Status ReloadAllRoomGraphics(std::array& rooms); + + // Data access + const std::vector& GetRoomSizePointers() const { return room_size_pointers_; } + const std::vector& GetRoomSizes() const { return room_sizes_; } + const std::unordered_map& GetRoomSizeAddresses() const { return room_size_addresses_; } + const std::unordered_map& GetRoomPalette() const { return room_palette_; } + + private: + Rom* rom_; + + std::vector room_size_pointers_; + std::vector room_sizes_; + std::unordered_map room_size_addresses_; + std::unordered_map room_palette_; + uint64_t total_room_size_ = 0; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H diff --git a/src/app/editor/dungeon/dungeon_toolset.cc b/src/app/editor/dungeon/dungeon_toolset.cc new file mode 100644 index 00000000..15b9b52a --- /dev/null +++ b/src/app/editor/dungeon/dungeon_toolset.cc @@ -0,0 +1,151 @@ +#include "dungeon_toolset.h" + +#include +#include + +#include "app/gui/icons.h" +#include "imgui/imgui.h" + +namespace yaze::editor { + +using ImGui::BeginTable; +using ImGui::Button; +using ImGui::EndTable; +using ImGui::RadioButton; +using ImGui::TableNextColumn; +using ImGui::TableSetupColumn; +using ImGui::Text; + +void DungeonToolset::Draw() { + if (BeginTable("DWToolset", 16, ImGuiTableFlags_SizingFixedFit, ImVec2(0, 0))) { + static std::array tool_names = { + "Undo", "Redo", "Separator", "All", "BG1", "BG2", + "BG3", "Separator", "Object", "Sprite", "Item", "Entrance", + "Door", "Chest", "Block", "Palette"}; + std::ranges::for_each(tool_names, + [](const char* name) { TableSetupColumn(name); }); + + // Undo button + TableNextColumn(); + if (Button(ICON_MD_UNDO)) { + if (undo_callback_) undo_callback_(); + } + + // Redo button + TableNextColumn(); + if (Button(ICON_MD_REDO)) { + if (redo_callback_) redo_callback_(); + } + + // Separator + TableNextColumn(); + Text(ICON_MD_MORE_VERT); + + // Background layer selection + TableNextColumn(); + if (RadioButton("All", background_type_ == kBackgroundAny)) { + background_type_ = kBackgroundAny; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show all background layers"); + } + + TableNextColumn(); + if (RadioButton("BG1", background_type_ == kBackground1)) { + background_type_ = kBackground1; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show background layer 1 only"); + } + + TableNextColumn(); + if (RadioButton("BG2", background_type_ == kBackground2)) { + background_type_ = kBackground2; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show background layer 2 only"); + } + + TableNextColumn(); + if (RadioButton("BG3", background_type_ == kBackground3)) { + background_type_ = kBackground3; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show background layer 3 only"); + } + + // Separator + TableNextColumn(); + Text(ICON_MD_MORE_VERT); + + // Placement mode selection + TableNextColumn(); + if (RadioButton(ICON_MD_SQUARE, placement_type_ == kObject)) { + placement_type_ = kObject; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Objects"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_PEST_CONTROL, placement_type_ == kSprite)) { + placement_type_ = kSprite; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Sprites"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_GRASS, placement_type_ == kItem)) { + placement_type_ = kItem; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Items"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_NAVIGATION, placement_type_ == kEntrance)) { + placement_type_ = kEntrance; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Entrances"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_SENSOR_DOOR, placement_type_ == kDoor)) { + placement_type_ = kDoor; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Doors"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_INVENTORY, placement_type_ == kChest)) { + placement_type_ = kChest; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Chests"); + } + + TableNextColumn(); + if (RadioButton(ICON_MD_VIEW_MODULE, placement_type_ == kBlock)) { + placement_type_ = kBlock; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Blocks"); + } + + // Palette button + TableNextColumn(); + if (Button(ICON_MD_PALETTE)) { + if (palette_toggle_callback_) palette_toggle_callback_(); + } + + ImGui::EndTable(); + } + + ImGui::Separator(); + ImGui::Text("Instructions: Click to place objects, Ctrl+Click to select, drag to move"); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_toolset.h b/src/app/editor/dungeon/dungeon_toolset.h new file mode 100644 index 00000000..63bc1d73 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_toolset.h @@ -0,0 +1,69 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H + +#include +#include + +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +/** + * @brief Handles the dungeon editor toolset UI + * + * This component manages the toolbar with placement modes, background layer + * selection, and other editing tools. + */ +class DungeonToolset { + public: + enum BackgroundType { + kNoBackground, + kBackground1, + kBackground2, + kBackground3, + kBackgroundAny, + }; + + enum PlacementType { + kNoType, + kObject, // Object editing mode + kSprite, // Sprite editing mode + kItem, // Item placement mode + kEntrance, // Entrance/exit editing mode + kDoor, // Door configuration mode + kChest, // Chest management mode + kBlock // Legacy block mode + }; + + DungeonToolset() = default; + + void Draw(); + + // Getters + BackgroundType background_type() const { return background_type_; } + PlacementType placement_type() const { return placement_type_; } + + // Setters + void set_background_type(BackgroundType type) { background_type_ = type; } + void set_placement_type(PlacementType type) { placement_type_ = type; } + + // Callbacks + void SetUndoCallback(std::function callback) { undo_callback_ = callback; } + void SetRedoCallback(std::function callback) { redo_callback_ = callback; } + void SetPaletteToggleCallback(std::function callback) { palette_toggle_callback_ = callback; } + + private: + BackgroundType background_type_ = kBackgroundAny; + PlacementType placement_type_ = kNoType; + + // Callbacks for editor actions + std::function undo_callback_; + std::function redo_callback_; + std::function palette_toggle_callback_; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H diff --git a/src/app/editor/dungeon/dungeon_usage_tracker.cc b/src/app/editor/dungeon/dungeon_usage_tracker.cc new file mode 100644 index 00000000..9b774cc0 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_usage_tracker.cc @@ -0,0 +1,87 @@ +#include "dungeon_usage_tracker.h" + +#include "imgui/imgui.h" + +namespace yaze::editor { + +void DungeonUsageTracker::CalculateUsageStats(const std::array& rooms) { + blockset_usage_.clear(); + spriteset_usage_.clear(); + palette_usage_.clear(); + + for (const auto& room : rooms) { + if (blockset_usage_.find(room.blockset) == blockset_usage_.end()) { + blockset_usage_[room.blockset] = 1; + } else { + blockset_usage_[room.blockset] += 1; + } + + if (spriteset_usage_.find(room.spriteset) == spriteset_usage_.end()) { + spriteset_usage_[room.spriteset] = 1; + } else { + spriteset_usage_[room.spriteset] += 1; + } + + if (palette_usage_.find(room.palette) == palette_usage_.end()) { + palette_usage_[room.palette] = 1; + } else { + palette_usage_[room.palette] += 1; + } + } +} + +void DungeonUsageTracker::DrawUsageStats() { + if (ImGui::Button("Refresh")) { + ClearUsageStats(); + } + + ImGui::Text("Usage Statistics"); + ImGui::Separator(); + + ImGui::Text("Blocksets: %zu used", blockset_usage_.size()); + ImGui::Text("Spritesets: %zu used", spriteset_usage_.size()); + ImGui::Text("Palettes: %zu used", palette_usage_.size()); + + ImGui::Separator(); + + // Detailed usage breakdown + if (ImGui::CollapsingHeader("Blockset Usage")) { + for (const auto& [blockset, count] : blockset_usage_) { + ImGui::Text("Blockset 0x%02X: %d rooms", blockset, count); + } + } + + if (ImGui::CollapsingHeader("Spriteset Usage")) { + for (const auto& [spriteset, count] : spriteset_usage_) { + ImGui::Text("Spriteset 0x%02X: %d rooms", spriteset, count); + } + } + + if (ImGui::CollapsingHeader("Palette Usage")) { + for (const auto& [palette, count] : palette_usage_) { + ImGui::Text("Palette 0x%02X: %d rooms", palette, count); + } + } +} + +void DungeonUsageTracker::DrawUsageGrid() { + // TODO: Implement usage grid visualization + ImGui::Text("Usage grid visualization not yet implemented"); +} + +void DungeonUsageTracker::RenderSetUsage(const absl::flat_hash_map& usage_map, + uint16_t& selected_set, int spriteset_offset) { + // TODO: Implement set usage rendering + ImGui::Text("Set usage rendering not yet implemented"); +} + +void DungeonUsageTracker::ClearUsageStats() { + selected_blockset_ = 0xFFFF; + selected_spriteset_ = 0xFFFF; + selected_palette_ = 0xFFFF; + spriteset_usage_.clear(); + blockset_usage_.clear(); + palette_usage_.clear(); +} + +} // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_usage_tracker.h b/src/app/editor/dungeon/dungeon_usage_tracker.h new file mode 100644 index 00000000..902f5b05 --- /dev/null +++ b/src/app/editor/dungeon/dungeon_usage_tracker.h @@ -0,0 +1,57 @@ +#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H +#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H + +#include "absl/container/flat_hash_map.h" +#include "app/zelda3/dungeon/room.h" + +namespace yaze { +namespace editor { + +/** + * @brief Tracks and analyzes usage statistics for dungeon resources + * + * This component manages blockset, spriteset, and palette usage statistics + * across all dungeon rooms, providing insights for optimization. + */ +class DungeonUsageTracker { + public: + DungeonUsageTracker() = default; + + // Statistics calculation + void CalculateUsageStats(const std::array& rooms); + void DrawUsageStats(); + void DrawUsageGrid(); + void RenderSetUsage(const absl::flat_hash_map& usage_map, + uint16_t& selected_set, int spriteset_offset = 0x00); + + // Data access + const absl::flat_hash_map& GetBlocksetUsage() const { return blockset_usage_; } + const absl::flat_hash_map& GetSpritesetUsage() const { return spriteset_usage_; } + const absl::flat_hash_map& GetPaletteUsage() const { return palette_usage_; } + + // Selection state + uint16_t GetSelectedBlockset() const { return selected_blockset_; } + uint16_t GetSelectedSpriteset() const { return selected_spriteset_; } + uint16_t GetSelectedPalette() const { return selected_palette_; } + + void SetSelectedBlockset(uint16_t blockset) { selected_blockset_ = blockset; } + void SetSelectedSpriteset(uint16_t spriteset) { selected_spriteset_ = spriteset; } + void SetSelectedPalette(uint16_t palette) { selected_palette_ = palette; } + + // Clear data + void ClearUsageStats(); + + private: + absl::flat_hash_map spriteset_usage_; + absl::flat_hash_map blockset_usage_; + absl::flat_hash_map palette_usage_; + + uint16_t selected_blockset_ = 0xFFFF; // 0xFFFF indicates no selection + uint16_t selected_spriteset_ = 0xFFFF; + uint16_t selected_palette_ = 0xFFFF; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H diff --git a/src/app/editor/editor.cmake b/src/app/editor/editor.cmake index eeb9f7d5..89435b12 100644 --- a/src/app/editor/editor.cmake +++ b/src/app/editor/editor.cmake @@ -5,6 +5,11 @@ set( app/editor/dungeon/dungeon_room_selector.cc app/editor/dungeon/dungeon_canvas_viewer.cc app/editor/dungeon/dungeon_object_selector.cc + app/editor/dungeon/dungeon_toolset.cc + app/editor/dungeon/dungeon_object_interaction.cc + app/editor/dungeon/dungeon_renderer.cc + app/editor/dungeon/dungeon_room_loader.cc + app/editor/dungeon/dungeon_usage_tracker.cc app/editor/overworld/overworld_editor.cc app/editor/sprite/sprite_editor.cc app/editor/music/music_editor.cc