#include "dungeon_editor.h" #include "absl/container/flat_hash_map.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/object_renderer.h" #include "app/zelda3/dungeon/room.h" #include "imgui/imgui.h" #include "util/hex.h" namespace yaze::editor { using core::Renderer; using ImGui::BeginChild; using ImGui::BeginTabBar; using ImGui::BeginTabItem; using ImGui::BeginTable; using ImGui::Button; using ImGui::EndChild; 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; 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); } // 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(); // Initialize the new editor system if (dungeon_editor_system_) { auto status = dungeon_editor_system_->Initialize(); if (!status.ok()) { return status; } } is_loaded_ = true; return absl::OkStatus(); } absl::Status DungeonEditor::Update() { if (refresh_graphics_) { RETURN_IF_ERROR(RefreshGraphics()); refresh_graphics_ = false; } if (ImGui::BeginTabBar("##DungeonEditorTabBar")) { if (ImGui::BeginTabItem("Room Editor")) { status_ = UpdateDungeonRoomView(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Usage Statistics")) { if (is_loaded_) { DrawUsageStats(); } ImGui::EndTabItem(); } ImGui::EndTabBar(); } 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(); } 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; } } } } absl::Status DungeonEditor::UpdateDungeonRoomView() { DrawToolset(); 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(); } // Simplified 3-column layout: Room/Entrance Selector | Canvas | Object Selector/Editor if (BeginTable("#DungeonEditTable", 3, kDungeonTableFlags, ImVec2(0, 0))) { TableSetupColumn("Room/Entrance Selector", ImGuiTableColumnFlags_WidthFixed, 250); TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch, ImGui::GetContentRegionAvail().x); TableSetupColumn("Object Selector/Editor", ImGuiTableColumnFlags_WidthFixed, 300); TableHeadersRow(); TableNextRow(); // Column 1: Room and Entrance Selector TableNextColumn(); if (ImGui::BeginTabBar("##DungeonRoomTabBar")) { TAB_ITEM("Rooms"); DrawRoomSelector(); END_TAB_ITEM(); TAB_ITEM("Entrances"); DrawEntranceSelector(); END_TAB_ITEM(); ImGui::EndTabBar(); } // Column 2: Main Canvas TableNextColumn(); DrawDungeonTabView(); // Column 3: Object Selector and Editor TableNextColumn(); if (ImGui::BeginTabBar("##ObjectEditorTabBar")) { TAB_ITEM("Graphics"); DrawTileSelector(); END_TAB_ITEM(); TAB_ITEM("Editor"); DrawIntegratedEditingPanels(); END_TAB_ITEM(); ImGui::EndTabBar(); } ImGui::EndTable(); } return absl::OkStatus(); } 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(ICON_MD_FILTER_NONE, background_type_ == kBackgroundAny)) { background_type_ = kBackgroundAny; } TableNextColumn(); if (RadioButton(ICON_MD_FILTER_1, background_type_ == kBackground1)) { background_type_ = kBackground1; } TableNextColumn(); if (RadioButton(ICON_MD_FILTER_2, background_type_ == kBackground2)) { background_type_ = kBackground2; } TableNextColumn(); if (RadioButton(ICON_MD_FILTER_3, background_type_ == kBackground3)) { background_type_ = kBackground3; } 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(); } } void DungeonEditor::DrawRoomSelector() { if (rom()->is_loaded()) { gui::InputHexWord("Room ID", ¤t_room_id_); gui::InputHex("Palette ID", ¤t_palette_id_); if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)9); BeginChild(child_id, ImGui::GetContentRegionAvail(), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { int i = 0; for (const auto each_room_name : zelda3::kRoomNames) { rom()->resource_label()->SelectableLabelWithNameEdit( current_room_id_ == i, "Dungeon Room Names", util::HexByte(i), each_room_name.data()); if (ImGui::IsItemClicked()) { // TODO: Jump to tab if room is already open current_room_id_ = i; if (!active_rooms_.contains(i)) { active_rooms_.push_back(i); } } i += 1; } } EndChild(); } } using ImGui::Separator; void DungeonEditor::DrawEntranceSelector() { if (rom()->is_loaded()) { auto current_entrance = entrances_[current_entrance_id_]; gui::InputHexWord("Entrance ID", ¤t_entrance.entrance_id_); gui::InputHexWord("Room ID", ¤t_entrance.room_, 50.f, true); SameLine(); gui::InputHexByte("Dungeon ID", ¤t_entrance.dungeon_id_, 50.f, true); gui::InputHexByte("Blockset", ¤t_entrance.blockset_, 50.f, true); SameLine(); gui::InputHexByte("Music", ¤t_entrance.music_, 50.f, true); SameLine(); gui::InputHexByte("Floor", ¤t_entrance.floor_); Separator(); gui::InputHexWord("Player X ", ¤t_entrance.x_position_); SameLine(); gui::InputHexWord("Player Y ", ¤t_entrance.y_position_); gui::InputHexWord("Camera X", ¤t_entrance.camera_trigger_x_); SameLine(); gui::InputHexWord("Camera Y", ¤t_entrance.camera_trigger_y_); gui::InputHexWord("Scroll X ", ¤t_entrance.camera_x_); SameLine(); gui::InputHexWord("Scroll Y ", ¤t_entrance.camera_y_); gui::InputHexWord("Exit", ¤t_entrance.exit_, 50.f, true); Separator(); Text("Camera Boundaries"); Separator(); Text("\t\t\t\t\tNorth East South West"); gui::InputHexByte("Quadrant", ¤t_entrance.camera_boundary_qn_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_qe_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_qs_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_qw_, 50.f, true); gui::InputHexByte("Full room", ¤t_entrance.camera_boundary_fn_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_fe_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_fs_, 50.f, true); SameLine(); gui::InputHexByte("", ¤t_entrance.camera_boundary_fw_, 50.f, true); if (BeginChild("EntranceSelector", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { for (int i = 0; i < 0x85 + 7; i++) { rom()->resource_label()->SelectableLabelWithNameEdit( current_entrance_id_ == i, "Dungeon Entrance Names", util::HexByte(i), zelda3::kEntranceNames[i].data()); if (ImGui::IsItemClicked()) { current_entrance_id_ = i; if (!active_rooms_.contains(i)) { active_rooms_.push_back(entrances_[i].room_); } } } } EndChild(); } } 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(); } 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")) { object_render_cache_.clear(); } ImGui::SameLine(); ImGui::Text("Cache: %zu objects", object_render_cache_.size()); // Object statistics and metadata ImGui::Separator(); ImGui::Text("Room Statistics:"); ImGui::Text("Objects: %zu", rooms_[room_id].GetTileObjects().size()); 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("Palette Hash: %#016llx", last_palette_hash_); // 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(); 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 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); } } } 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)); } } void DungeonEditor::DrawRoomGraphics() { const auto height = 0x40; room_gfx_canvas_.DrawBackground(); room_gfx_canvas_.DrawContextMenu(); room_gfx_canvas_.DrawTileSelector(32); if (is_loaded_) { // Use the currently active room tab int active_room_id = 0; if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) { active_room_id = active_rooms_[current_active_room_tab_]; } auto blocks = rooms_[active_room_id].blocks(); // Load graphics for this room if not already loaded if (blocks.empty()) { (void)LoadAndRenderRoomGraphics(active_room_id); blocks = rooms_[active_room_id].blocks(); } int current_block = 0; const int max_blocks_per_row = 4; // Limit blocks per row to fit canvas const int block_width = 0x100; // 256 pixels per block const int block_height = 0x40; // 64 pixels per block for (int block : blocks) { if (current_block >= 16) break; // Only show first 16 blocks // Ensure the graphics sheet is loaded and has a valid texture if (block < gfx::Arena::Get().gfx_sheets().size()) { auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block]; // Calculate position in a grid layout instead of horizontal concatenation int row = current_block / max_blocks_per_row; int col = current_block % max_blocks_per_row; int x = room_gfx_canvas_.zero_point().x + 2 + (col * block_width); int y = room_gfx_canvas_.zero_point().y + 2 + (row * block_height); // Ensure we don't exceed canvas bounds if (x + block_width <= room_gfx_canvas_.zero_point().x + room_gfx_canvas_.width() && y + block_height <= room_gfx_canvas_.zero_point().y + room_gfx_canvas_.height()) { // Only draw if the texture is valid if (gfx_sheet.texture() != 0) { room_gfx_canvas_.draw_list()->AddImage( (ImTextureID)(intptr_t)gfx_sheet.texture(), ImVec2(x, y), ImVec2(x + block_width, y + block_height)); } } } current_block += 1; } } room_gfx_canvas_.DrawGrid(32.0f); room_gfx_canvas_.DrawOverlay(); } void DungeonEditor::DrawTileSelector() { if (BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) { if (BeginTabItem("Room Graphics")) { if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)3); BeginChild(child_id, ImGui::GetContentRegionAvail(), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { DrawRoomGraphics(); } EndChild(); EndTabItem(); } if (BeginTabItem("Object Renderer")) { DrawObjectRenderer(); EndTabItem(); } EndTabBar(); } } void DungeonEditor::DrawObjectRenderer() { if (BeginTable("DungeonObjectEditorTable", 2, kDungeonObjectTableFlags, ImVec2(0, 0))) { TableSetupColumn("Dungeon Objects", ImGuiTableColumnFlags_WidthStretch, ImGui::GetContentRegionAvail().x); TableSetupColumn("Canvas"); TableNextColumn(); BeginChild("DungeonObjectButtons", ImVec2(250, 0), true); static int selected_object = 0; int i = 0; for (const auto object_name : zelda3::Type1RoomObjectNames) { if (ImGui::Selectable(object_name.data(), selected_object == i)) { selected_object = i; // Create a test object and render it auto test_object = zelda3::RoomObject(i, 8, 8, 0x12, 0); // Center in canvas test_object.set_rom(rom_); test_object.EnsureTilesLoaded(); // Get current palette auto palette = rom()->palette_group().dungeon_main[current_palette_group_id_]; // Render object preview auto result = object_renderer_.GetObjectPreview(test_object, palette); if (result.ok()) { object_loaded_ = true; preview_object_ = test_object; // Store for rendering preview_palette_ = palette; } } i += 1; } EndChild(); // Right side of the table - Canvas TableNextColumn(); BeginChild("DungeonObjectCanvas", ImVec2(276, 0x10 * 0x40 + 1), true); object_canvas_.DrawBackground(ImVec2(256 + 1, 0x10 * 0x40 + 1)); object_canvas_.DrawContextMenu(); object_canvas_.DrawTileSelector(32); object_canvas_.DrawGrid(32.0f); // Render object preview if available if (object_loaded_ && preview_object_.id_ >= 0) { // Render preview object at center of canvas (object_canvas_ is 256x256) int preview_x = 128 - 16; // Center horizontally (256/2 - 16 for 32x32 object) int preview_y = 128 - 16; // Center vertically auto preview_result = object_renderer_.RenderObject(preview_object_, preview_palette_); if (preview_result.ok()) { auto preview_bitmap = std::move(preview_result.value()); preview_bitmap.SetPalette(preview_palette_); core::Renderer::Get().RenderBitmap(&preview_bitmap); object_canvas_.DrawBitmap(preview_bitmap, preview_x, preview_y, 1.0f, 255); } } object_canvas_.DrawOverlay(); EndChild(); ImGui::EndTable(); } if (object_loaded_) { ImGui::Begin("Object Preview", &object_loaded_, 0); ImGui::Text("Object ID: 0x%02X", preview_object_.id_); ImGui::Text("Position: (%d, %d)", preview_object_.x_, preview_object_.y_); ImGui::Text("Size: 0x%02X", preview_object_.size_); ImGui::Text("Layer: %d", static_cast(preview_object_.layer_)); ImGui::End(); } } void 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::RenderSetUsage( const absl::flat_hash_map &usage_map, uint16_t &selected_set, int spriteset_offset) { // Sort the usage map by set number std::vector> sorted_usage(usage_map.begin(), usage_map.end()); std::sort(sorted_usage.begin(), sorted_usage.end(), [](const auto &a, const auto &b) { return a.first < b.first; }); for (const auto &[set, count] : sorted_usage) { std::string display_str; if (spriteset_offset != 0x00) { display_str = absl::StrFormat("%#02x, %#02x: %d", set, (set + spriteset_offset), count); } else { display_str = absl::StrFormat("%#02x: %d", (set + spriteset_offset), count); } if (ImGui::Selectable(display_str.c_str(), selected_set == set)) { selected_set = set; // Update the selected set when clicked } } } namespace { // Calculate the unused sets in a usage map // Range for blocksets 0-0x24 // Range for spritesets 0-0x8F // Range for palettes 0-0x47 template void RenderUnusedSets(const absl::flat_hash_map &usage_map, int max_set, int spriteset_offset = 0x00) { std::vector unused_sets; for (int i = 0; i < max_set; i++) { if (usage_map.find(i) == usage_map.end()) { unused_sets.push_back(i); } } for (const auto &set : unused_sets) { if (spriteset_offset != 0x00) { Text("%#02x, %#02x", set, (set + spriteset_offset)); } else { Text("%#02x", set); } } } } // namespace void DungeonEditor::DrawUsageStats() { if (Button("Refresh")) { selected_blockset_ = 0xFFFF; selected_spriteset_ = 0xFFFF; selected_palette_ = 0xFFFF; spriteset_usage_.clear(); blockset_usage_.clear(); palette_usage_.clear(); CalculateUsageStats(); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (BeginTable("DungeonUsageStatsTable", 8, kDungeonTableFlags | ImGuiTableFlags_SizingFixedFit, ImGui::GetContentRegionAvail())) { TableSetupColumn("Blockset Usage"); TableSetupColumn("Unused Blockset"); TableSetupColumn("Palette Usage"); TableSetupColumn("Unused Palette"); TableSetupColumn("Spriteset Usage"); TableSetupColumn("Unused Spriteset"); TableSetupColumn("Usage Grid"); TableSetupColumn("Group Preview"); TableHeadersRow(); ImGui::PopStyleVar(2); TableNextColumn(); BeginChild("BlocksetUsageScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderSetUsage(blockset_usage_, selected_blockset_); EndChild(); TableNextColumn(); BeginChild("UnusedBlocksetScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderUnusedSets(blockset_usage_, 0x25); EndChild(); TableNextColumn(); BeginChild("PaletteUsageScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderSetUsage(palette_usage_, selected_palette_); EndChild(); TableNextColumn(); BeginChild("UnusedPaletteScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderUnusedSets(palette_usage_, 0x48); EndChild(); TableNextColumn(); BeginChild("SpritesetUsageScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderSetUsage(spriteset_usage_, selected_spriteset_, 0x40); EndChild(); TableNextColumn(); BeginChild("UnusedSpritesetScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); RenderUnusedSets(spriteset_usage_, 0x90, 0x40); EndChild(); TableNextColumn(); BeginChild("UsageGrid", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar); Text("%s", absl::StrFormat("Total size of all rooms: %d hex format: %#06x", total_room_size_, total_room_size_) .c_str()); DrawUsageGrid(); EndChild(); TableNextColumn(); if (selected_blockset_ < 0x25) { gfx_group_editor_.SetSelectedBlockset(selected_blockset_); gfx_group_editor_.DrawBlocksetViewer(true); } else if (selected_spriteset_ < 0x90) { gfx_group_editor_.SetSelectedSpriteset(selected_spriteset_ + 0x40); gfx_group_editor_.DrawSpritesetViewer(true); } } ImGui::EndTable(); } void DungeonEditor::DrawUsageGrid() { int totalSquares = 296; int squaresWide = 16; int squaresTall = (totalSquares + squaresWide - 1) / squaresWide; // Ceiling of totalSquares/squaresWide for (int row = 0; row < squaresTall; ++row) { ImGui::NewLine(); for (int col = 0; col < squaresWide; ++col) { // Check if we have reached 295 squares if (row * squaresWide + col >= totalSquares) { break; } // Determine if this square should be highlighted const auto &room = rooms_[row * squaresWide + col]; // Create a button or selectable for each square ImGui::BeginGroup(); ImVec4 color = room_palette_[room.palette]; color.x = color.x / 255; color.y = color.y / 255; color.z = color.z / 255; color.w = 1.0f; if (room_sizes_[row * squaresWide + col] > 0xFFFF) { color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Or any highlight color } if (room_sizes_[row * squaresWide + col] == 0) { color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Or any highlight color } ImGui::PushStyleColor(ImGuiCol_Button, color); // Make the button text darker ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); bool highlight = room.blockset == selected_blockset_ || room.spriteset == selected_spriteset_ || room.palette == selected_palette_; // Set highlight color if needed if (highlight) { ImGui::PushStyleColor( ImGuiCol_Button, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Or any highlight color } if (Button(absl::StrFormat("%#llx", static_cast(room_sizes_[row * squaresWide + col])) .c_str(), ImVec2(55, 30))) { // Switch over to the room editor tab // and add a room tab by the ID of the square // that was clicked } if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { ImGui::OpenPopup( absl::StrFormat("RoomContextMenu%d", row * squaresWide + col) .c_str()); } ImGui::PopStyleColor(2); ImGui::EndGroup(); // Reset style if it was highlighted if (highlight) { ImGui::PopStyleColor(); } // Check if the square is hovered if (ImGui::IsItemHovered()) { // Display a tooltip with all the room properties ImGui::BeginTooltip(); Text("Room ID: %d", row * squaresWide + col); Text("Blockset: %#02x", room.blockset); Text("Spriteset: %#02x", room.spriteset); Text("Palette: %#02x", room.palette); Text("Floor1: %#02x", room.floor1); Text("Floor2: %#02x", room.floor2); Text("Message ID: %#04x", room.message_id_); Text("Size: %#016llx", static_cast( room_sizes_[row * squaresWide + col])); Text("Size Pointer: %#016llx", room_size_pointers_[row * squaresWide + col]); ImGui::EndTooltip(); } // Keep squares in the same line SameLine(); } } } // New editor method implementations void DungeonEditor::DrawObjectEditor() { if (!object_editor_) { ImGui::Text("Object editor not initialized"); return; } ImGui::Text("Object Editor"); ImGui::Separator(); // Display current editing mode auto mode = object_editor_->GetMode(); const char *mode_names[] = {"Select", "Insert", "Delete", "Edit", "Layer", "Preview"}; ImGui::Text("Mode: %s", mode_names[static_cast(mode)]); // Mode selection if (ImGui::Button("Select Mode")) { object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kSelect); } ImGui::SameLine(); if (ImGui::Button("Insert Mode")) { object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kInsert); } ImGui::SameLine(); if (ImGui::Button("Edit Mode")) { object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kEdit); } // Layer selection int current_layer = object_editor_->GetCurrentLayer(); if (ImGui::SliderInt("Layer", ¤t_layer, 0, 2)) { object_editor_->SetCurrentLayer(current_layer); } // Object type selection int current_object_type = object_editor_->GetCurrentObjectType(); if (ImGui::InputInt("Object Type", ¤t_object_type, 1, 16)) { if (current_object_type >= 0 && current_object_type <= 0x3FF) { object_editor_->SetCurrentObjectType(current_object_type); } } // Editor configuration auto config = object_editor_->GetConfig(); if (ImGui::Checkbox("Snap to Grid", &config.snap_to_grid)) { (void)object_editor_->SetConfig(config); } if (ImGui::Checkbox("Show Grid", &config.show_grid)) { (void)object_editor_->SetConfig(config); } if (ImGui::Checkbox("Show Preview", &config.show_preview)) { (void)object_editor_->SetConfig(config); } // Object count and selection info ImGui::Separator(); ImGui::Text("Objects: %zu", object_editor_->GetObjectCount()); auto selection = object_editor_->GetSelection(); if (!selection.selected_objects.empty()) { ImGui::Text("Selected: %zu objects", selection.selected_objects.size()); } // Undo/Redo status ImGui::Separator(); ImGui::Text("Undo: %s", object_editor_->CanUndo() ? "Available" : "Not Available"); ImGui::Text("Redo: %s", object_editor_->CanRedo() ? "Available" : "Not Available"); } void DungeonEditor::DrawSpriteEditor() { if (!dungeon_editor_system_) { ImGui::Text("Dungeon editor system not initialized"); return; } ImGui::Text("Sprite Editor"); ImGui::Separator(); // Display current room sprites auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(current_room); if (sprites_result.ok()) { auto sprites = sprites_result.value(); ImGui::Text("Sprites in room %d: %zu", current_room, sprites.size()); for (const auto &sprite : sprites) { ImGui::Text("ID: %d, Type: %d, Position: (%d, %d)", sprite.sprite_id, static_cast(sprite.type), sprite.x, sprite.y); } } else { ImGui::Text("Error loading sprites: %s", sprites_result.status().message().data()); } // Sprite placement controls static int new_sprite_id = 0; static int new_sprite_x = 0; static int new_sprite_y = 0; static int new_sprite_layer = 0; ImGui::Separator(); ImGui::Text("Add New Sprite"); ImGui::InputInt("Sprite ID", &new_sprite_id); ImGui::InputInt("X Position", &new_sprite_x); ImGui::InputInt("Y Position", &new_sprite_y); ImGui::SliderInt("Layer", &new_sprite_layer, 0, 2); if (ImGui::Button("Add Sprite")) { zelda3::DungeonEditorSystem::SpriteData sprite_data; sprite_data.sprite_id = new_sprite_id; sprite_data.type = zelda3::DungeonEditorSystem::SpriteType::kEnemy; sprite_data.x = new_sprite_x; sprite_data.y = new_sprite_y; sprite_data.layer = new_sprite_layer; auto status = dungeon_editor_system_->AddSprite(sprite_data); if (!status.ok()) { ImGui::Text("Error adding sprite: %s", status.message().data()); } } } void DungeonEditor::DrawItemEditor() { if (!dungeon_editor_system_) { ImGui::Text("Dungeon editor system not initialized"); return; } ImGui::Text("Item Editor"); ImGui::Separator(); // Display current room items auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto items_result = dungeon_editor_system_->GetItemsByRoom(current_room); if (items_result.ok()) { auto items = items_result.value(); ImGui::Text("Items in room %d: %zu", current_room, items.size()); for (const auto &item : items) { ImGui::Text("ID: %d, Type: %d, Position: (%d, %d), Hidden: %s", item.item_id, static_cast(item.type), item.x, item.y, item.is_hidden ? "Yes" : "No"); } } else { ImGui::Text("Error loading items: %s", items_result.status().message().data()); } // Item placement controls static int new_item_id = 0; static int new_item_x = 0; static int new_item_y = 0; static bool new_item_hidden = false; ImGui::Separator(); ImGui::Text("Add New Item"); ImGui::InputInt("Item ID", &new_item_id); ImGui::InputInt("X Position", &new_item_x); ImGui::InputInt("Y Position", &new_item_y); ImGui::Checkbox("Hidden", &new_item_hidden); if (ImGui::Button("Add Item")) { zelda3::DungeonEditorSystem::ItemData item_data; item_data.item_id = new_item_id; item_data.type = zelda3::DungeonEditorSystem::ItemType::kKey; item_data.x = new_item_x; item_data.y = new_item_y; item_data.room_id = current_room; item_data.is_hidden = new_item_hidden; auto status = dungeon_editor_system_->AddItem(item_data); if (!status.ok()) { ImGui::Text("Error adding item: %s", status.message().data()); } } } void DungeonEditor::DrawEntranceEditor() { if (!dungeon_editor_system_) { ImGui::Text("Dungeon editor system not initialized"); return; } ImGui::Text("Entrance Editor"); ImGui::Separator(); // Display current room entrances auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(current_room); if (entrances_result.ok()) { auto entrances = entrances_result.value(); ImGui::Text("Entrances in room %d: %zu", current_room, entrances.size()); for (const auto &entrance : entrances) { ImGui::Text( "ID: %d, Type: %d, Target Room: %d, Target Position: (%d, %d)", entrance.entrance_id, static_cast(entrance.type), entrance.target_room_id, entrance.target_x, entrance.target_y); } } else { ImGui::Text("Error loading entrances: %s", entrances_result.status().message().data()); } // Entrance creation controls static int target_room_id = 0; static int target_x = 0; static int target_y = 0; static int source_x = 0; static int source_y = 0; ImGui::Separator(); ImGui::Text("Create New Entrance"); ImGui::InputInt("Target Room ID", &target_room_id); ImGui::InputInt("Source X", &source_x); ImGui::InputInt("Source Y", &source_y); ImGui::InputInt("Target X", &target_x); ImGui::InputInt("Target Y", &target_y); if (ImGui::Button("Connect Rooms")) { auto status = dungeon_editor_system_->ConnectRooms( current_room, target_room_id, source_x, source_y, target_x, target_y); if (!status.ok()) { ImGui::Text("Error connecting rooms: %s", status.message().data()); } } } void DungeonEditor::DrawDoorEditor() { if (!dungeon_editor_system_) { ImGui::Text("Dungeon editor system not initialized"); return; } ImGui::Text("Door Editor"); ImGui::Separator(); // Display current room doors auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto doors_result = dungeon_editor_system_->GetDoorsByRoom(current_room); if (doors_result.ok()) { auto doors = doors_result.value(); ImGui::Text("Doors in room %d: %zu", current_room, doors.size()); for (const auto &door : doors) { ImGui::Text("ID: %d, Position: (%d, %d), Direction: %d, Target Room: %d", door.door_id, door.x, door.y, door.direction, door.target_room_id); } } else { ImGui::Text("Error loading doors: %s", doors_result.status().message().data()); } // Door creation controls static int door_x = 0; static int door_y = 0; static int door_direction = 0; static int door_target_room = 0; static int door_target_x = 0; static int door_target_y = 0; static bool door_locked = false; static bool door_requires_key = false; static int door_key_type = 0; ImGui::Separator(); ImGui::Text("Create New Door"); ImGui::InputInt("Door X", &door_x); ImGui::InputInt("Door Y", &door_y); ImGui::SliderInt("Direction", &door_direction, 0, 3); ImGui::InputInt("Target Room", &door_target_room); ImGui::InputInt("Target X", &door_target_x); ImGui::InputInt("Target Y", &door_target_y); ImGui::Checkbox("Locked", &door_locked); ImGui::Checkbox("Requires Key", &door_requires_key); ImGui::InputInt("Key Type", &door_key_type); if (ImGui::Button("Add Door")) { zelda3::DungeonEditorSystem::DoorData door_data; door_data.room_id = current_room; door_data.x = door_x; door_data.y = door_y; door_data.direction = door_direction; door_data.target_room_id = door_target_room; door_data.target_x = door_target_x; door_data.target_y = door_target_y; door_data.is_locked = door_locked; door_data.requires_key = door_requires_key; door_data.key_type = door_key_type; auto status = dungeon_editor_system_->AddDoor(door_data); if (!status.ok()) { ImGui::Text("Error adding door: %s", status.message().data()); } } } void DungeonEditor::DrawChestEditor() { if (!dungeon_editor_system_) { ImGui::Text("Dungeon editor system not initialized"); return; } ImGui::Text("Chest Editor"); ImGui::Separator(); // Display current room chests auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto chests_result = dungeon_editor_system_->GetChestsByRoom(current_room); if (chests_result.ok()) { auto chests = chests_result.value(); ImGui::Text("Chests in room %d: %zu", current_room, chests.size()); for (const auto &chest : chests) { ImGui::Text("ID: %d, Position: (%d, %d), Big: %s, Item: %d, Quantity: %d", chest.chest_id, chest.x, chest.y, chest.is_big_chest ? "Yes" : "No", chest.item_id, chest.item_quantity); } } else { ImGui::Text("Error loading chests: %s", chests_result.status().message().data()); } // Chest creation controls static int chest_x = 0; static int chest_y = 0; static bool chest_big = false; static int chest_item_id = 0; static int chest_item_quantity = 1; ImGui::Separator(); ImGui::Text("Create New Chest"); ImGui::InputInt("Chest X", &chest_x); ImGui::InputInt("Chest Y", &chest_y); ImGui::Checkbox("Big Chest", &chest_big); ImGui::InputInt("Item ID", &chest_item_id); ImGui::InputInt("Item Quantity", &chest_item_quantity); if (ImGui::Button("Add Chest")) { zelda3::DungeonEditorSystem::ChestData chest_data; chest_data.room_id = current_room; chest_data.x = chest_x; chest_data.y = chest_y; chest_data.is_big_chest = chest_big; chest_data.item_id = chest_item_id; chest_data.item_quantity = chest_item_quantity; auto status = dungeon_editor_system_->AddChest(chest_data); if (!status.ok()) { ImGui::Text("Error adding chest: %s", status.message().data()); } } } void DungeonEditor::DrawIntegratedEditingPanels() { if (!dungeon_editor_system_ || !object_editor_) { ImGui::Text("Editor systems not initialized"); return; } // Create a tabbed interface for different editing modes if (ImGui::BeginTabBar("##EditingPanels")) { // Object Editor Tab if (ImGui::BeginTabItem("Objects")) { DrawCompactObjectEditor(); ImGui::EndTabItem(); } // Sprite Editor Tab if (ImGui::BeginTabItem("Sprites")) { DrawCompactSpriteEditor(); ImGui::EndTabItem(); } // Item Editor Tab if (ImGui::BeginTabItem("Items")) { DrawCompactItemEditor(); ImGui::EndTabItem(); } // Entrance Editor Tab if (ImGui::BeginTabItem("Entrances")) { DrawCompactEntranceEditor(); ImGui::EndTabItem(); } // Door Editor Tab if (ImGui::BeginTabItem("Doors")) { DrawCompactDoorEditor(); ImGui::EndTabItem(); } // Chest Editor Tab if (ImGui::BeginTabItem("Chests")) { DrawCompactChestEditor(); ImGui::EndTabItem(); } // Properties Tab if (ImGui::BeginTabItem("Properties")) { DrawCompactPropertiesEditor(); ImGui::EndTabItem(); } ImGui::EndTabBar(); } } void DungeonEditor::DrawCompactObjectEditor() { ImGui::Text("Object Editor"); ImGui::Separator(); // Display current editing mode auto mode = object_editor_->GetMode(); const char *mode_names[] = {"Select", "Insert", "Delete", "Edit", "Layer", "Preview"}; ImGui::Text("Mode: %s", mode_names[static_cast(mode)]); // Compact mode selection if (ImGui::Button("Select")) object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kSelect); ImGui::SameLine(); if (ImGui::Button("Insert")) object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kInsert); ImGui::SameLine(); if (ImGui::Button("Edit")) object_editor_->SetMode(zelda3::DungeonObjectEditor::Mode::kEdit); // Layer and object type selection int current_layer = object_editor_->GetCurrentLayer(); if (ImGui::SliderInt("Layer", ¤t_layer, 0, 2)) { object_editor_->SetCurrentLayer(current_layer); } int current_object_type = object_editor_->GetCurrentObjectType(); if (ImGui::InputInt("Object Type", ¤t_object_type, 1, 16)) { if (current_object_type >= 0 && current_object_type <= 0x3FF) { object_editor_->SetCurrentObjectType(current_object_type); } } // Quick configuration checkboxes auto config = object_editor_->GetConfig(); if (ImGui::Checkbox("Snap to Grid", &config.snap_to_grid)) { (void)object_editor_->SetConfig(config); } ImGui::SameLine(); if (ImGui::Checkbox("Show Grid", &config.show_grid)) { (void)object_editor_->SetConfig(config); } // Object count and selection info ImGui::Separator(); ImGui::Text("Objects: %zu", object_editor_->GetObjectCount()); auto selection = object_editor_->GetSelection(); if (!selection.selected_objects.empty()) { ImGui::Text("Selected: %zu", selection.selected_objects.size()); } // Undo/Redo buttons ImGui::Separator(); if (ImGui::Button("Undo") && object_editor_->CanUndo()) { (void)object_editor_->Undo(); } ImGui::SameLine(); if (ImGui::Button("Redo") && object_editor_->CanRedo()) { (void)object_editor_->Redo(); } } void DungeonEditor::DrawCompactSpriteEditor() { ImGui::Text("Sprite Editor"); ImGui::Separator(); // Display current room sprites auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(current_room); if (sprites_result.ok()) { auto sprites = sprites_result.value(); ImGui::Text("Sprites in room %d: %zu", current_room, sprites.size()); // Show first few sprites in compact format int display_count = std::min(3, static_cast(sprites.size())); for (int i = 0; i < display_count; ++i) { const auto &sprite = sprites[i]; ImGui::Text("ID:%d Type:%d (%d,%d)", sprite.sprite_id, static_cast(sprite.type), sprite.x, sprite.y); } if (sprites.size() > 3) { ImGui::Text("... and %zu more", sprites.size() - 3); } } else { ImGui::Text("Error loading sprites"); } // Quick sprite placement ImGui::Separator(); ImGui::Text("Quick Add Sprite"); static int new_sprite_id = 0; static int new_sprite_x = 0; static int new_sprite_y = 0; ImGui::InputInt("ID", &new_sprite_id); ImGui::InputInt("X", &new_sprite_x); ImGui::InputInt("Y", &new_sprite_y); if (ImGui::Button("Add Sprite")) { zelda3::DungeonEditorSystem::SpriteData sprite_data; sprite_data.sprite_id = new_sprite_id; sprite_data.type = zelda3::DungeonEditorSystem::SpriteType::kEnemy; sprite_data.x = new_sprite_x; sprite_data.y = new_sprite_y; sprite_data.layer = 0; auto status = dungeon_editor_system_->AddSprite(sprite_data); if (!status.ok()) { ImGui::Text("Error adding sprite"); } } } void DungeonEditor::DrawCompactItemEditor() { ImGui::Text("Item Editor"); ImGui::Separator(); // Display current room items auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto items_result = dungeon_editor_system_->GetItemsByRoom(current_room); if (items_result.ok()) { auto items = items_result.value(); ImGui::Text("Items in room %d: %zu", current_room, items.size()); // Show first few items in compact format int display_count = std::min(3, static_cast(items.size())); for (int i = 0; i < display_count; ++i) { const auto &item = items[i]; ImGui::Text("ID:%d Type:%d (%d,%d)", item.item_id, static_cast(item.type), item.x, item.y); } if (items.size() > 3) { ImGui::Text("... and %zu more", items.size() - 3); } } else { ImGui::Text("Error loading items"); } // Quick item placement ImGui::Separator(); ImGui::Text("Quick Add Item"); static int new_item_id = 0; static int new_item_x = 0; static int new_item_y = 0; ImGui::InputInt("ID", &new_item_id); ImGui::InputInt("X", &new_item_x); ImGui::InputInt("Y", &new_item_y); if (ImGui::Button("Add Item")) { zelda3::DungeonEditorSystem::ItemData item_data; item_data.item_id = new_item_id; item_data.type = zelda3::DungeonEditorSystem::ItemType::kKey; item_data.x = new_item_x; item_data.y = new_item_y; item_data.room_id = current_room; item_data.is_hidden = false; auto status = dungeon_editor_system_->AddItem(item_data); if (!status.ok()) { ImGui::Text("Error adding item"); } } } void DungeonEditor::DrawCompactEntranceEditor() { ImGui::Text("Entrance Editor"); ImGui::Separator(); // Display current room entrances auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(current_room); if (entrances_result.ok()) { auto entrances = entrances_result.value(); ImGui::Text("Entrances: %zu", entrances.size()); for (const auto &entrance : entrances) { ImGui::Text("ID:%d -> Room:%d (%d,%d)", entrance.entrance_id, entrance.target_room_id, entrance.target_x, entrance.target_y); } } else { ImGui::Text("Error loading entrances"); } // Quick room connection ImGui::Separator(); ImGui::Text("Connect Rooms"); static int target_room_id = 0; static int source_x = 0; static int source_y = 0; static int target_x = 0; static int target_y = 0; ImGui::InputInt("Target Room", &target_room_id); ImGui::InputInt("Source X", &source_x); ImGui::InputInt("Source Y", &source_y); ImGui::InputInt("Target X", &target_x); ImGui::InputInt("Target Y", &target_y); if (ImGui::Button("Connect")) { auto status = dungeon_editor_system_->ConnectRooms( current_room, target_room_id, source_x, source_y, target_x, target_y); if (!status.ok()) { ImGui::Text("Error connecting rooms"); } } } void DungeonEditor::DrawCompactDoorEditor() { ImGui::Text("Door Editor"); ImGui::Separator(); // Display current room doors auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto doors_result = dungeon_editor_system_->GetDoorsByRoom(current_room); if (doors_result.ok()) { auto doors = doors_result.value(); ImGui::Text("Doors: %zu", doors.size()); for (const auto &door : doors) { ImGui::Text("ID:%d (%d,%d) -> Room:%d", door.door_id, door.x, door.y, door.target_room_id); } } else { ImGui::Text("Error loading doors"); } // Quick door creation ImGui::Separator(); ImGui::Text("Add Door"); static int door_x = 0; static int door_y = 0; static int door_direction = 0; static int door_target_room = 0; ImGui::InputInt("X", &door_x); ImGui::InputInt("Y", &door_y); ImGui::SliderInt("Dir", &door_direction, 0, 3); ImGui::InputInt("Target", &door_target_room); if (ImGui::Button("Add Door")) { zelda3::DungeonEditorSystem::DoorData door_data; door_data.room_id = current_room; door_data.x = door_x; door_data.y = door_y; door_data.direction = door_direction; door_data.target_room_id = door_target_room; door_data.target_x = door_x; door_data.target_y = door_y; door_data.is_locked = false; door_data.requires_key = false; door_data.key_type = 0; auto status = dungeon_editor_system_->AddDoor(door_data); if (!status.ok()) { ImGui::Text("Error adding door"); } } } void DungeonEditor::DrawCompactChestEditor() { ImGui::Text("Chest Editor"); ImGui::Separator(); // Display current room chests auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto chests_result = dungeon_editor_system_->GetChestsByRoom(current_room); if (chests_result.ok()) { auto chests = chests_result.value(); ImGui::Text("Chests: %zu", chests.size()); for (const auto &chest : chests) { ImGui::Text("ID:%d (%d,%d) Item:%d", chest.chest_id, chest.x, chest.y, chest.item_id); } } else { ImGui::Text("Error loading chests"); } // Quick chest creation ImGui::Separator(); ImGui::Text("Add Chest"); static int chest_x = 0; static int chest_y = 0; static int chest_item_id = 0; static bool chest_big = false; ImGui::InputInt("X", &chest_x); ImGui::InputInt("Y", &chest_y); ImGui::InputInt("Item ID", &chest_item_id); ImGui::Checkbox("Big", &chest_big); if (ImGui::Button("Add Chest")) { zelda3::DungeonEditorSystem::ChestData chest_data; chest_data.room_id = current_room; chest_data.x = chest_x; chest_data.y = chest_y; chest_data.is_big_chest = chest_big; chest_data.item_id = chest_item_id; chest_data.item_quantity = 1; auto status = dungeon_editor_system_->AddChest(chest_data); if (!status.ok()) { ImGui::Text("Error adding chest"); } } } void DungeonEditor::DrawCompactPropertiesEditor() { ImGui::Text("Room Properties"); ImGui::Separator(); auto current_room = dungeon_editor_system_->GetCurrentRoom(); auto properties_result = dungeon_editor_system_->GetRoomProperties(current_room); if (properties_result.ok()) { auto properties = properties_result.value(); static char room_name[128] = {0}; static int dungeon_id = 0; static int floor_level = 0; static bool is_boss_room = false; static bool is_save_room = false; static int music_id = 0; // Copy current values strncpy(room_name, properties.name.c_str(), sizeof(room_name) - 1); dungeon_id = properties.dungeon_id; floor_level = properties.floor_level; is_boss_room = properties.is_boss_room; is_save_room = properties.is_save_room; music_id = properties.music_id; ImGui::InputText("Name", room_name, sizeof(room_name)); ImGui::InputInt("Dungeon ID", &dungeon_id); ImGui::InputInt("Floor", &floor_level); ImGui::InputInt("Music", &music_id); ImGui::Checkbox("Boss Room", &is_boss_room); ImGui::Checkbox("Save Room", &is_save_room); if (ImGui::Button("Save Properties")) { zelda3::DungeonEditorSystem::RoomProperties new_properties; new_properties.room_id = current_room; new_properties.name = room_name; new_properties.dungeon_id = dungeon_id; new_properties.floor_level = floor_level; new_properties.is_boss_room = is_boss_room; new_properties.is_save_room = is_save_room; new_properties.music_id = music_id; auto status = dungeon_editor_system_->SetRoomProperties(current_room, new_properties); if (!status.ok()) { ImGui::Text("Error saving properties"); } } } else { ImGui::Text("Error loading properties"); } // Dungeon settings summary ImGui::Separator(); ImGui::Text("Dungeon Settings"); auto dungeon_settings_result = dungeon_editor_system_->GetDungeonSettings(); if (dungeon_settings_result.ok()) { auto settings = dungeon_settings_result.value(); ImGui::Text("Dungeon: %s", settings.name.c_str()); ImGui::Text("Rooms: %d", settings.total_rooms); ImGui::Text("Start: %d", settings.starting_room_id); ImGui::Text("Boss: %d", settings.boss_room_id); } } // 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) 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) 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 auto canvas_width = canvas_.width(); auto canvas_height = canvas_.height(); return (canvas_x >= -margin && canvas_y >= -margin && canvas_x <= canvas_width + margin && canvas_y <= canvas_height + margin); } // Room graphics management methods absl::Status 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(); // Update the background layers with proper palette RETURN_IF_ERROR(UpdateRoomBackgroundLayers(room_id)); // Refresh the graphics system refresh_graphics_ = true; return absl::OkStatus(); } absl::Status DungeonEditor::ReloadAllRoomGraphics() { // Reload graphics for all currently loaded rooms for (int room_id = 0; room_id < rooms_.size(); ++room_id) { RETURN_IF_ERROR(LoadAndRenderRoomGraphics(room_id)); } return absl::OkStatus(); } absl::Status DungeonEditor::UpdateRoomBackgroundLayers(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]; // Validate palette group access if (current_palette_group_id_ >= rom()->palette_group().dungeon_main.size()) { return absl::FailedPreconditionError("Invalid palette group ID"); } // Get the current room's palette auto current_palette = rom()->palette_group().dungeon_main[current_palette_group_id_]; // Update BG1 (background layer 1) with proper palette if (room.blocks().size() >= 8) { for (int i = 0; i < 8; i++) { int block = room.blocks()[i]; if (block >= 0 && block < gfx::Arena::Get().gfx_sheets().size()) { if (current_palette_id_ < current_palette_group_.size()) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( current_palette_group_[current_palette_id_], 0); core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); } } } } // Update BG2 (background layer 2) with sprite auxiliary palette if (room.blocks().size() >= 16) { auto sprites_aux1_pal_group = rom()->palette_group().sprites_aux1; if (current_palette_id_ < sprites_aux1_pal_group.size()) { for (int i = 8; i < 16; i++) { int block = room.blocks()[i]; if (block >= 0 && block < gfx::Arena::Get().gfx_sheets().size()) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( sprites_aux1_pal_group[current_palette_id_], 0); core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); } } } } return absl::OkStatus(); } void DungeonEditor::RenderRoomBackgroundLayers(int room_id) { if (room_id < 0 || room_id >= rooms_.size()) { return; } if (!rom_ || !rom_->is_loaded()) { return; } auto& room = rooms_[room_id]; // Get canvas dimensions to limit rendering int canvas_width = canvas_.width(); int canvas_height = canvas_.height(); // Validate canvas dimensions if (canvas_width <= 0 || canvas_height <= 0) { return; } // Render the room's background layers using the graphics arena // BG1 (background layer 1) - main room graphics auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap(); if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) { // Scale the background to fit the canvas float scale_x = static_cast(canvas_width) / bg1_bitmap.width(); float scale_y = static_cast(canvas_height) / bg1_bitmap.height(); float scale = std::min(scale_x, scale_y); int scaled_width = static_cast(bg1_bitmap.width() * scale); int scaled_height = static_cast(bg1_bitmap.height() * scale); int offset_x = (canvas_width - scaled_width) / 2; int offset_y = (canvas_height - scaled_height) / 2; canvas_.DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255); } // BG2 (background layer 2) - sprite graphics (overlay) auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap(); if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) { // Scale the background to fit the canvas float scale_x = static_cast(canvas_width) / bg2_bitmap.width(); float scale_y = static_cast(canvas_height) / bg2_bitmap.height(); float scale = std::min(scale_x, scale_y); int scaled_width = static_cast(bg2_bitmap.width() * scale); int scaled_height = static_cast(bg2_bitmap.height() * scale); int offset_x = (canvas_width - scaled_width) / 2; int offset_y = (canvas_height - scaled_height) / 2; canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200); // Semi-transparent overlay } } } // namespace yaze::editor