yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_selector.cc
Go to the documentation of this file.
1// Related header
3#include "absl/strings/str_format.h"
4
5// C system headers
6#include <cstring>
7#include <filesystem>
8
9// C++ standard library headers
10#include <algorithm>
11#include <cctype>
12#include <iterator>
13
14// Third-party library headers
15#include "imgui/imgui.h"
16
17// Project headers
20#include "app/gui/core/icons.h"
27#include "app/platform/window.h"
28#include "core/features.h"
29#include "rom/rom.h"
30#include "zelda3/dungeon/custom_object.h" // For CustomObjectManager
38#include "zelda3/dungeon/room.h"
39#include "zelda3/dungeon/room_object.h" // For GetObjectName()
40
41namespace yaze::editor {
42
43using ImGui::BeginChild;
44using ImGui::EndChild;
45using ImGui::EndTabItem;
46using ImGui::Separator;
47
50 if (gui::BeginThemedTabBar("##TabBar",
51 ImGuiTabBarFlags_FittingPolicyScroll)) {
52 if (ImGui::BeginTabItem("Room Graphics")) {
53 const bool room_graphics_tab_open = gui::LayoutHelpers::BeginContentChild(
54 "##RoomGraphicsTab",
55 ImVec2(0.0f, gui::UIConfig::kContentMinHeightList), true,
56 ImGuiWindowFlags_AlwaysVerticalScrollbar);
57 if (room_graphics_tab_open) {
59 }
61 EndTabItem();
62 }
63
64 if (ImGui::BeginTabItem("Object Renderer")) {
66 EndTabItem();
67 }
69 }
70}
71
74 // Use AssetBrowser for better object selection
75 if (ImGui::BeginTable(
76 "DungeonObjectEditorTable", 2,
77 ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
78 ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter |
79 ImGuiTableFlags_BordersV,
80 ImVec2(0, 0))) {
81 ImGui::TableSetupColumn("Object Browser", ImGuiTableColumnFlags_WidthFixed,
82 400);
83 ImGui::TableSetupColumn("Preview Canvas",
84 ImGuiTableColumnFlags_WidthStretch);
85 ImGui::TableHeadersRow();
86
87 // Left column: AssetBrowser for object selection (minimum size so list never collapses)
88 ImGui::TableNextColumn();
89 const bool asset_browser_open = gui::LayoutHelpers::BeginContentChild(
90 "AssetBrowser",
93 true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
94 if (asset_browser_open) {
96 }
98
99 // Right column: Preview and placement controls
100 ImGui::TableNextColumn();
101 const bool preview_canvas_open = gui::LayoutHelpers::BeginContentChild(
102 "PreviewCanvas", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas),
103 true);
104 if (preview_canvas_open) {
105 // Object placement controls
106 ImGui::SeparatorText("Object Placement");
107 ImGui::InputInt("X Position", &place_x_);
108 ImGui::InputInt("Y Position", &place_y_);
109
110 if (ImGui::Button("Place Object") && object_loaded_) {
112 }
113
114 ImGui::Separator();
115
116 // Preview canvas
117 gui::CanvasFrameOptions frame_opts;
118 frame_opts.canvas_size = ImVec2(256 + 1, 0x10 * 0x40 + 1);
119 frame_opts.draw_grid = true;
120 frame_opts.grid_step = 32.0f;
121 frame_opts.render_popups = true;
122 gui::CanvasFrame frame(object_canvas_, frame_opts);
123
124 // Render selected object preview with graphical rendering
125 if (object_loaded_ && preview_object_.id_ >= 0) {
126 int preview_x = 128 - 24; // Center horizontally
127 int preview_y = 128 - 24; // Center vertically
128
129 if (!DrawObjectPreview(preview_object_, ImVec2(preview_x, preview_y),
130 48.0f)) {
131 RenderObjectPrimitive(preview_object_, preview_x, preview_y);
132 }
133 }
134 }
136 ImGui::EndTable();
137 }
138
139 // Object details window
140 if (object_loaded_) {
141 ImGui::Begin("Object Details", &object_loaded_, 0);
142 ImGui::Text("Object ID: 0x%02X", preview_object_.id_);
143 ImGui::Text("Position: (%d, %d)", preview_object_.x_, preview_object_.y_);
144 ImGui::Text("Size: 0x%02X", preview_object_.size_);
145 ImGui::Text("Layer: %d", static_cast<int>(preview_object_.layer_));
146
147 // Add object placement controls
148 ImGui::Separator();
149 ImGui::Text("Placement Controls:");
150 ImGui::InputInt("X Position", &place_x_);
151 ImGui::InputInt("Y Position", &place_y_);
152
153 if (ImGui::Button("Place Object")) {
155 }
156
157 ImGui::End();
158 }
159}
160
162 if (gui::BeginThemedTabBar("##ObjectSelectorTabBar")) {
163 // Object Selector tab - for placing objects with new AssetBrowser
164 if (ImGui::BeginTabItem("Object Selector")) {
166 ImGui::EndTabItem();
167 }
168
169 // Room Graphics tab - 8 bitmaps viewer
170 if (ImGui::BeginTabItem("Room Graphics")) {
172 ImGui::EndTabItem();
173 }
174
175 // Object Editor tab - experimental editor
176 if (ImGui::BeginTabItem("Object Editor")) {
178 ImGui::EndTabItem();
179 }
180
182 }
183}
184
186 const auto height = 0x40;
187 gui::CanvasFrameOptions frame_opts;
188 frame_opts.draw_grid = true;
189 frame_opts.grid_step = 32.0f;
190 frame_opts.render_popups = true;
191 gui::CanvasFrame frame(room_gfx_canvas_, frame_opts);
193
194 if (rom_ && rom_->is_loaded() && rooms_) {
195 int active_room_id = current_room_id_;
196 auto& room = (*rooms_)[active_room_id];
197 // Keep room-sheet assignments in sync with the active room header.
198 room.LoadRoomGraphics(room.blockset());
199 auto blocks = room.blocks();
200
201 int current_block = 0;
202 const int max_blocks_per_row = 2; // 2 blocks per row for 300px column
203 const int block_width = 128; // Reduced size to fit column
204 const int block_height = 32; // Reduced height
205
206 for (int block : blocks) {
207 if (current_block >= 16)
208 break; // Only show first 16 blocks
209
210 // Ensure the graphics sheet is loaded and has a valid texture
211 if (block < gfx::Arena::Get().gfx_sheets().size()) {
212 auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block];
213
214 // Calculate position in a grid layout instead of horizontal
215 // concatenation
216 int row = current_block / max_blocks_per_row;
217 int col = current_block % max_blocks_per_row;
218
219 ImVec2 local_pos(2 + (col * block_width), 2 + (row * block_height));
220
221 // Ensure we don't exceed canvas bounds
222 if (local_pos.x + block_width <= room_gfx_canvas_.width() &&
223 local_pos.y + block_height <= room_gfx_canvas_.height()) {
224 if (gfx_sheet.texture() != 0) {
226 (ImTextureID)(intptr_t)gfx_sheet.texture(), local_pos,
227 ImVec2(block_width, block_height));
228 }
229 }
230 }
231 current_block += 1;
232 }
233 }
234}
235
238 ImGui::Text("Editor systems not initialized");
239 return;
240 }
241
242 // Create a tabbed interface for different editing modes
243 if (gui::BeginThemedTabBar("##EditingPanels")) {
244 // Object Editor Tab
245 if (ImGui::BeginTabItem("Objects")) {
247 ImGui::EndTabItem();
248 }
249
250 // Sprite Editor Tab
251 if (ImGui::BeginTabItem("Sprites")) {
253 ImGui::EndTabItem();
254 }
255
256 // Item Editor Tab
257 if (ImGui::BeginTabItem("Items")) {
259 ImGui::EndTabItem();
260 }
261
262 // Entrance Editor Tab
263 if (ImGui::BeginTabItem("Entrances")) {
265 ImGui::EndTabItem();
266 }
267
268 // Door Editor Tab
269 if (ImGui::BeginTabItem("Doors")) {
271 ImGui::EndTabItem();
272 }
273
274 // Chest Editor Tab
275 if (ImGui::BeginTabItem("Chests")) {
277 ImGui::EndTabItem();
278 }
279
280 // Properties Tab
281 if (ImGui::BeginTabItem("Properties")) {
283 ImGui::EndTabItem();
284 }
285
286 // Minecart Editor Tab
288 }
289}
290
292 if (!object_editor_) {
293 ImGui::Text("Object editor not initialized");
294 return;
295 }
296
297 auto& editor = *object_editor_;
298
299 ImGui::Text("Object Editor");
300 Separator();
301
302 // Display current editing mode
303 auto mode = editor.GetMode();
304 const char* mode_names[] = {"Select", "Insert", "Delete",
305 "Edit", "Layer", "Preview"};
306 ImGui::Text("Mode: %s", mode_names[static_cast<int>(mode)]);
307
308 // Compact mode selection
309 if (ImGui::Button("Select"))
311 ImGui::SameLine();
312 if (ImGui::Button("Insert"))
314 ImGui::SameLine();
315 if (ImGui::Button("Edit"))
317
318 // Layer and object type selection
319 int current_layer = editor.GetCurrentLayer();
320 if (ImGui::SliderInt("Layer", &current_layer, 0, 2)) {
321 editor.SetCurrentLayer(current_layer);
322 }
323
324 int current_object_type = editor.GetCurrentObjectType();
325 if (ImGui::InputInt("Object Type", &current_object_type, 1, 16)) {
326 if (current_object_type >= 0 && current_object_type <= 0x3FF) {
327 editor.SetCurrentObjectType(current_object_type);
328 }
329 }
330
331 // Quick configuration checkboxes
332 auto config = editor.GetConfig();
333 if (ImGui::Checkbox("Snap to Grid", &config.snap_to_grid)) {
334 editor.SetConfig(config);
335 }
336 ImGui::SameLine();
337 if (ImGui::Checkbox("Show Grid", &config.show_grid)) {
338 editor.SetConfig(config);
339 }
340
341 // Object count and selection info
342 Separator();
343 ImGui::Text("Objects: %zu", editor.GetObjectCount());
344
345 auto selection = editor.GetSelection();
346 if (!selection.selected_objects.empty()) {
347 ImGui::Text("Selected: %zu", selection.selected_objects.size());
348 }
349
350 // Undo/Redo buttons
351 Separator();
352 if (ImGui::Button("Undo") && editor.CanUndo()) {
353 (void)editor.Undo();
354 }
355 ImGui::SameLine();
356 if (ImGui::Button("Redo") && editor.CanRedo()) {
357 (void)editor.Redo();
358 }
359}
360
362 const auto& theme = AgentUI::GetTheme();
363
364 // Type 3 objects (0xF80-0xFFF) - Special room features
365 if (object_id >= 0xF80) {
366 if (object_id >= 0xF80 && object_id <= 0xF8F) {
367 return ImGui::ColorConvertFloat4ToU32(
368 theme.selection_secondary); // Light blue for layer indicators
369 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
370 return ImGui::ColorConvertFloat4ToU32(
371 theme.transport_color); // Orange/Purple for door indicators
372 } else {
373 return ImGui::ColorConvertFloat4ToU32(
374 theme.music_zone_color); // Purple for misc Type 3
375 }
376 }
377
378 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
379 if (object_id >= 0x100 && object_id < 0x200) {
380 if (object_id >= 0x100 && object_id <= 0x10F) {
381 return IM_COL32(255, 150, 50, 255); // Orange for torches
382 } else if (object_id >= 0x110 && object_id <= 0x11F) {
383 return IM_COL32(150, 150, 200, 255); // Blue-gray for blocks
384 } else if (object_id >= 0x120 && object_id <= 0x12F) {
385 return ImGui::ColorConvertFloat4ToU32(
386 theme.status_success); // Green for switches
387 } else if (object_id >= 0x130 && object_id <= 0x13F) {
388 return ImGui::GetColorU32(theme.selection_primary); // Yellow for stairs
389 } else {
390 return IM_COL32(180, 180, 180, 255); // Gray for other Type 2
391 }
392 }
393
394 // Type 1 objects (0x00-0xFF) - Base room objects
395 if (object_id >= 0x10 && object_id <= 0x1F) {
396 return ImGui::GetColorU32(theme.dungeon_object_wall); // Gray for walls
397 } else if (object_id >= 0x20 && object_id <= 0x2F) {
398 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for floors
399 } else if (object_id == 0xF9 || object_id == 0xFA) {
400 return ImGui::GetColorU32(theme.item_color); // Gold for chests
401 } else if (object_id >= 0x17 && object_id <= 0x1E) {
402 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for doors
403 } else if (object_id == 0x2F || object_id == 0x2B) {
404 return ImGui::GetColorU32(
405 theme.dungeon_object_pot); // Saddle brown for pots
406 } else if (object_id >= 0x30 && object_id <= 0x3F) {
407 return ImGui::GetColorU32(
408 theme.dungeon_object_decoration); // Dim gray for decorations
409 } else if (object_id >= 0x00 && object_id <= 0x0F) {
410 return IM_COL32(120, 120, 180, 255); // Blue-gray for corners
411 } else {
412 return ImGui::GetColorU32(theme.dungeon_object_default); // Default gray
413 }
414}
415
417 // Type 3 objects (0xF80-0xFFF) - Special room features
418 if (object_id >= 0xF80) {
419 if (object_id >= 0xF80 && object_id <= 0xF8F) {
420 return "L"; // Layer
421 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
422 return "D"; // Door indicator
423 } else {
424 return "S"; // Special
425 }
426 }
427
428 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
429 if (object_id >= 0x100 && object_id < 0x200) {
430 if (object_id >= 0x100 && object_id <= 0x10F) {
431 return "*"; // Torch (flame)
432 } else if (object_id >= 0x110 && object_id <= 0x11F) {
433 return "#"; // Block
434 } else if (object_id >= 0x120 && object_id <= 0x12F) {
435 return "o"; // Switch
436 } else if (object_id >= 0x130 && object_id <= 0x13F) {
437 return "^"; // Stairs
438 } else {
439 return "2"; // Type 2
440 }
441 }
442
443 // Type 1 objects (0x00-0xFF) - Base room objects
444 if (object_id >= 0x10 && object_id <= 0x1F) {
445 return "|"; // Wall
446 } else if (object_id >= 0x20 && object_id <= 0x2F) {
447 return "_"; // Floor
448 } else if (object_id == 0xF9 || object_id == 0xFA) {
449 return "C"; // Chest
450 } else if (object_id >= 0x17 && object_id <= 0x1E) {
451 return "+"; // Door
452 } else if (object_id == 0x2F || object_id == 0x2B) {
453 return "o"; // Pot
454 } else if (object_id >= 0x30 && object_id <= 0x3F) {
455 return "~"; // Decoration
456 } else if (object_id >= 0x00 && object_id <= 0x0F) {
457 return "/"; // Corner
458 } else {
459 return "?"; // Unknown
460 }
461}
462
464 const zelda3::RoomObject& object, int x, int y) {
465 const auto& theme = AgentUI::GetTheme();
466 // Render object as primitive shape on canvas
467 ImU32 color = GetObjectTypeColor(object.id_);
468
469 // Calculate object size with proper wall length handling
470 int obj_width, obj_height;
471 CalculateObjectDimensions(object, obj_width, obj_height);
472
473 // Draw object rectangle
474 ImVec4 color_vec = ImGui::ColorConvertU32ToFloat4(color);
475 object_canvas_.DrawRect(x, y, obj_width, obj_height, color_vec);
476 object_canvas_.DrawRect(x, y, obj_width, obj_height, theme.panel_bg_darker);
477
478 // Draw object ID as text
479 std::string obj_text = absl::StrFormat("0x%X", object.id_);
480 object_canvas_.DrawText(obj_text, x + obj_width + 2, y + 4);
481}
482
483void DungeonObjectSelector::SelectObject(int obj_id, int subtype) {
484 selected_object_id_ = obj_id;
485
486 // Create and update preview object
487 uint8_t size = 0x12;
488 if (subtype >= 0) {
489 size = static_cast<uint8_t>(subtype & 0x1F);
490 }
491 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, size, 0);
493 if (game_data_) {
494 auto palette =
496 preview_palette_ = palette;
497 }
498 object_loaded_ = true;
499
500 // Notify callback
503 }
504}
505
507 const auto& theme = AgentUI::GetTheme();
508
509 // Object ranges: Type 1 (0x00-0xFF), Type 2 (0x100-0x141), Type 3 (0xF80-0xFFF)
510 struct ObjectRange {
511 int start;
512 int end;
513 const char* label;
514 ImU32 header_color;
515 };
516 static const ObjectRange ranges[] = {
517 {0x00, 0xFF, "Type 1", IM_COL32(80, 120, 180, 255)},
518 {0x100, 0x141, "Type 2", IM_COL32(120, 80, 180, 255)},
519 {0xF80, 0xFFF, "Type 3", IM_COL32(180, 120, 80, 255)},
520 };
521
522 // Total object count
523 int total_objects =
524 (0xFF - 0x00 + 1) + (0x141 - 0x100 + 1) + (0xFFF - 0xF80 + 1);
525
526 // Preview toggle (disabled by default for performance)
527 ImGui::Checkbox(ICON_MD_IMAGE " Previews", &enable_object_previews_);
528 if (ImGui::IsItemHovered()) {
529 ImGui::SetTooltip(
530 "Enable to show actual object graphics.\n"
531 "Requires a room to be loaded.\n"
532 "May impact performance.");
533 }
534 ImGui::SameLine();
535 ImGui::TextDisabled("(%d objects)", total_objects);
536
537 // Search + category filter
538 ImGui::SetNextItemWidth(-1.0f);
539 ImGui::InputTextWithHint(
540 "##ObjectSearch", ICON_MD_SEARCH " Filter by name or hex...",
542 static const char* kFilterLabels[] = {"All", "Walls", "Floors", "Chests",
543 "Doors", "Decor", "Stairs"};
544 ImGui::SetNextItemWidth(160.0f);
545 ImGui::Combo("##ObjectFilterType", &object_type_filter_, kFilterLabels,
546 IM_ARRAYSIZE(kFilterLabels));
547 ImGui::SameLine();
548 if (gui::ThemedButton(ICON_MD_CLEAR " Clear")) {
549 object_search_buffer_[0] = '\0';
551 }
552 if (ImGui::IsItemHovered()) {
553 gui::ThemedTooltip("Clear search and category filter");
554 }
555
556 // Create asset browser-style grid
557 const float item_size = 72.0f;
558 const float item_spacing = 6.0f;
559 const int columns = std::max(
560 1, static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
561 (item_size + item_spacing)));
562
563 // Scrollable child region for grid - use all available space
564 float child_height = ImGui::GetContentRegionAvail().y;
565 if (ImGui::BeginChild("##ObjectGrid", ImVec2(0, child_height), false,
566 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
567
568 // Iterate through all object ranges
569 for (const auto& range : ranges) {
570 // Section header for each type
571 gui::StyleColorGuard section_guard(
572 {{ImGuiCol_Header,
573 ImGui::ColorConvertU32ToFloat4(range.header_color)},
574 {ImGuiCol_HeaderHovered,
575 ImGui::ColorConvertU32ToFloat4(
576 IM_COL32((range.header_color & 0xFF) + 30,
577 ((range.header_color >> 8) & 0xFF) + 30,
578 ((range.header_color >> 16) & 0xFF) + 30, 255))}});
579 bool section_open = ImGui::CollapsingHeader(
580 absl::StrFormat("%s (0x%03X-0x%03X)", range.label, range.start,
581 range.end)
582 .c_str(),
583 ImGuiTreeNodeFlags_DefaultOpen);
584
585 if (!section_open)
586 continue;
587
588 int current_column = 0;
589
590 for (int obj_id = range.start; obj_id <= range.end; ++obj_id) {
592 continue;
593 }
594
595 std::string full_name = zelda3::GetObjectName(obj_id);
596 if (!MatchesObjectSearch(obj_id, full_name)) {
597 continue;
598 }
599
600 if (current_column > 0) {
601 ImGui::SameLine();
602 }
603
604 ImGui::PushID(obj_id);
605
606 // Create selectable button for object
607 bool is_selected = (selected_object_id_ == obj_id);
608 ImVec2 button_size(item_size, item_size);
609
610 if (ImGui::Selectable("", is_selected,
611 ImGuiSelectableFlags_AllowDoubleClick,
612 button_size)) {
613 selected_object_id_ = obj_id;
614
615 // Create and update preview object
616 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, 0x12, 0);
618 if (game_data_ &&
621 auto palette = game_data_->palette_groups
623 preview_palette_ = palette;
624 }
625 object_loaded_ = true;
626
627 // Notify callbacks
630 }
631
632 // Handle double-click to open static object editor
633 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
636 }
637 }
638 }
639
640 // Draw object preview on the button; fall back to styled placeholder
641 ImVec2 button_pos = ImGui::GetItemRectMin();
642 ImDrawList* draw_list = ImGui::GetWindowDrawList();
643
644 // Only attempt graphical preview if enabled (performance optimization)
645 bool rendered = false;
647 rendered = DrawObjectPreview(MakePreviewObject(obj_id), button_pos,
648 item_size);
649 }
650
651 if (!rendered) {
652 // Draw a styled fallback with gradient background
653 ImU32 obj_color = GetObjectTypeColor(obj_id);
654 ImU32 darker_color = IM_COL32((obj_color & 0xFF) * 0.6f,
655 ((obj_color >> 8) & 0xFF) * 0.6f,
656 ((obj_color >> 16) & 0xFF) * 0.6f, 255);
657
658 // Gradient background
659 draw_list->AddRectFilledMultiColor(
660 button_pos,
661 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
662 darker_color, darker_color, obj_color, obj_color);
663
664 // Draw object type symbol in center
665 std::string symbol = GetObjectTypeSymbol(obj_id);
666 ImVec2 symbol_size = ImGui::CalcTextSize(symbol.c_str());
667 ImVec2 symbol_pos(
668 button_pos.x + (item_size - symbol_size.x) / 2,
669 button_pos.y + (item_size - symbol_size.y) / 2 - 10);
670 draw_list->AddText(symbol_pos, IM_COL32(255, 255, 255, 180),
671 symbol.c_str());
672 }
673
674 // Draw border with special highlight for static editor object
675 bool is_static_editor_obj = (obj_id == static_editor_object_id_);
676 ImU32 border_color;
677 float border_thickness;
678
679 if (is_static_editor_obj) {
680 border_color = IM_COL32(0, 200, 255, 255);
681 border_thickness = 3.0f;
682 } else if (is_selected) {
683 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
684 border_thickness = 3.0f;
685 } else {
686 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
687 border_thickness = 1.0f;
688 }
689
690 draw_list->AddRect(
691 button_pos,
692 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
693 border_color, 0.0f, 0, border_thickness);
694
695 // Static editor indicator icon
696 if (is_static_editor_obj) {
697 ImVec2 icon_pos(button_pos.x + item_size - 14, button_pos.y + 2);
698 draw_list->AddCircleFilled(ImVec2(icon_pos.x + 6, icon_pos.y + 6), 6,
699 IM_COL32(0, 200, 255, 200));
700 draw_list->AddText(icon_pos, IM_COL32(255, 255, 255, 255), "i");
701 }
702
703 // Get object name for display
704 // Truncate name for display
705 std::string display_name = full_name;
706 const size_t kMaxDisplayChars = 12;
707 if (display_name.length() > kMaxDisplayChars) {
708 display_name = display_name.substr(0, kMaxDisplayChars - 2) + "..";
709 }
710
711 // Draw object name (smaller, above ID)
712 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
713 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
714 button_pos.y + item_size - 26);
715 draw_list->AddText(name_pos,
716 ImGui::GetColorU32(theme.text_secondary_gray),
717 display_name.c_str());
718
719 // Draw object ID at bottom (hex format)
720 std::string id_text = absl::StrFormat("%03X", obj_id);
721 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
722 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
723 button_pos.y + item_size - id_size.y - 2);
724 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
725 id_text.c_str());
726
727 // Enhanced tooltip
728 if (ImGui::IsItemHovered()) {
729 gui::StyleColorGuard tooltip_guard(
730 {{ImGuiCol_PopupBg, theme.panel_bg_color},
731 {ImGuiCol_Border, theme.panel_border_color}});
732
733 if (ImGui::BeginTooltip()) {
734 ImGui::TextColored(theme.selection_primary, "Object 0x%03X",
735 obj_id);
736 ImGui::Text("%s", full_name.c_str());
737 int subtype = zelda3::GetObjectSubtype(obj_id);
738 ImGui::TextColored(theme.text_secondary_gray, "Subtype %d",
739 subtype);
740 ImGui::Separator();
741
742 uint32_t layout_key = (static_cast<uint32_t>(obj_id) << 16) |
743 static_cast<uint32_t>(subtype);
744 const bool can_capture_layout =
745 rom_ && rooms_ && current_room_id_ >= 0 &&
747 if (can_capture_layout &&
748 layout_cache_.find(layout_key) == layout_cache_.end()) {
750 auto& room_ref = (*rooms_)[current_room_id_];
751 auto layout_or = editor.CaptureObjectLayout(
752 obj_id, room_ref, current_palette_group_);
753 if (layout_or.ok()) {
754 layout_cache_[layout_key] = layout_or.value();
755 }
756 }
757
758 if (layout_cache_.count(layout_key)) {
759 const auto& layout = layout_cache_[layout_key];
760 ImGui::TextColored(theme.status_success, "Tiles: %zu",
761 layout.cells.size());
762
763 if (can_capture_layout) {
764 auto& room_ref = (*rooms_)[current_room_id_];
766 room_ref.get_gfx_buffer().data());
767 int rid = drawer.GetDrawRoutineId(obj_id);
768 ImGui::TextColored(theme.status_active, "Draw Routine: %d",
769 rid);
770 }
771
772 ImGui::Text("Layout:");
773 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
774 ImVec2 grid_start = ImGui::GetCursorScreenPos();
775 float cell_size = 4.0f;
776 for (const auto& cell : layout.cells) {
777 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
778 grid_start.y + cell.rel_y * cell_size);
779 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
780 tooltip_draw_list->AddRectFilled(p1, p2,
781 IM_COL32(200, 200, 200, 255));
782 tooltip_draw_list->AddRect(p1, p2, IM_COL32(50, 50, 50, 255));
783 }
784 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
785 layout.bounds_height * cell_size));
786 }
787
788 ImGui::Separator();
789 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
790 "Click to select for placement");
791 ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f),
792 "Double-click to view details");
793 ImGui::EndTooltip();
794 }
795 }
796
797 ImGui::PopID();
798
799 current_column = (current_column + 1) % columns;
800 } // end object loop
801 } // end range loop
802
804
805 // Custom Objects Section
806 gui::StyleColorGuard custom_hdr_guard(
807 {{ImGuiCol_Header,
808 ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 120, 255))},
809 {ImGuiCol_HeaderHovered,
810 ImGui::ColorConvertU32ToFloat4(IM_COL32(130, 210, 150, 255))}});
811 bool custom_open = ImGui::CollapsingHeader("Custom Objects",
812 ImGuiTreeNodeFlags_DefaultOpen);
813
814 if (custom_open) {
815 // "+ New Custom Object" button
816 if (tile_editor_panel_) {
817 if (ImGui::SmallButton(ICON_MD_ADD " New Custom Object")) {
818 show_create_dialog_ = true;
819 // Auto-generate a default filename
820 snprintf(create_filename_, sizeof(create_filename_),
821 "custom_%02x_%02d.bin", create_object_id_,
822 zelda3::CustomObjectManager::Get().GetSubtypeCount(
824 }
825 if (ImGui::IsItemHovered()) {
826 ImGui::SetTooltip("Create a new custom object from scratch");
827 }
828 }
829
830 auto& obj_manager = zelda3::CustomObjectManager::Get();
831 const std::string custom_base_path = obj_manager.GetBasePath();
832 if (custom_base_path.empty()) {
833 ImGui::TextColored(theme.text_secondary_gray,
834 "Custom objects folder: not configured");
835 } else {
836 ImGui::Text("Custom objects folder: %s", custom_base_path.c_str());
837 if (ImGui::IsItemHovered()) {
838 ImGui::SetTooltip("%s", custom_base_path.c_str());
839 }
840 ImGui::SameLine();
841 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload")) {
842 obj_manager.ReloadAll();
844 }
845 if (ImGui::IsItemHovered()) {
846 ImGui::SetTooltip(
847 "Reload custom object binaries and refresh previews");
848 }
849 }
850 ImGui::TextColored(theme.text_secondary_gray,
851 "Corner overrides: 0x100/0x101/0x102/0x103 use 0x31 "
852 "subtypes 02/04/03/05");
853
855
856 int custom_col = 0;
857
858 // Initialize if needed (hacky lazy init if drawer hasn't done it yet)
859 // Ideally should be initialized by system.
860 // We'll skip init here and assume ObjectDrawer did it or will do it.
861 // But we need counts. If uninitialized, counts might be wrong?
862 // GetSubtypeCount checks static lists, so it's safe even if not fully init with paths.
863
864 for (int obj_id : {0x31, 0x32}) {
866 continue;
867 }
868 int subtype_count = obj_manager.GetSubtypeCount(obj_id);
869 for (int subtype = 0; subtype < subtype_count; ++subtype) {
870 std::string base_name = zelda3::GetObjectName(obj_id);
871 std::string subtype_name =
872 absl::StrFormat("%s %02X", base_name.c_str(), subtype);
873 if (!MatchesObjectSearch(obj_id, subtype_name, subtype)) {
874 continue;
875 }
876
877 if (custom_col > 0)
878 ImGui::SameLine();
879
880 ImGui::PushID(obj_id * 1000 + subtype);
881
882 bool is_selected = (selected_object_id_ == obj_id &&
883 (preview_object_.size_ & 0x1F) == subtype);
884 ImVec2 button_size(item_size, item_size);
885
886 if (ImGui::Selectable("", is_selected,
887 ImGuiSelectableFlags_AllowDoubleClick,
888 button_size)) {
889 SelectObject(obj_id, subtype);
890
891 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
894 }
895 }
896
897 // Draw Preview
898 ImVec2 button_pos = ImGui::GetItemRectMin();
899 ImDrawList* draw_list = ImGui::GetWindowDrawList();
900
901 bool rendered = false;
902 // Native preview requires loaded ROM and correct pathing, might fail if not init.
903 // But we can try constructing a temp object with correct subtype.
905 auto temp_obj = MakePreviewObject(obj_id);
906 temp_obj.size_ = subtype;
907 rendered = DrawObjectPreview(temp_obj, button_pos, item_size);
908 }
909
910 if (!rendered) {
911 // Fallback visuals
912 ImU32 obj_color = IM_COL32(100, 180, 120, 255);
913 ImU32 darker_color = IM_COL32(60, 100, 70, 255);
914
915 draw_list->AddRectFilledMultiColor(
916 button_pos,
917 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
918 darker_color, darker_color, obj_color, obj_color);
919
920 std::string symbol = (obj_id == 0x31) ? "Trk" : "Cus";
921 // Subtype
922 std::string sub_text = absl::StrFormat("%02X", subtype);
923 ImVec2 sub_size = ImGui::CalcTextSize(sub_text.c_str());
924 ImVec2 sub_pos(button_pos.x + (item_size - sub_size.x) / 2,
925 button_pos.y + (item_size - sub_size.y) / 2);
926 draw_list->AddText(sub_pos, IM_COL32(255, 255, 255, 220),
927 sub_text.c_str());
928 }
929
930 // Border
931 bool is_static_editor_obj = (obj_id == static_editor_object_id_ &&
933 // Static editor doesn't track subtype currently, so highlighting all subtypes of 0x31 is correct
934 // if we are editing 0x31 generic. But maybe we only edit specific subtype?
935 // Static editor usually edits the code/logic common to ID.
936 ImU32 border_color =
937 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
938 : ImGui::GetColorU32(theme.panel_bg_darker);
939 float border_thickness = is_selected ? 3.0f : 1.0f;
940 draw_list->AddRect(
941 button_pos,
942 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
943 border_color, 0.0f, 0, border_thickness);
944
945 // Name/ID
946 std::string id_text = absl::StrFormat("%02X:%02X", obj_id, subtype);
947 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
948 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
949 button_pos.y + item_size - id_size.y - 2);
950 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
951 id_text.c_str());
952
953 if (ImGui::IsItemHovered()) {
954 gui::StyleColorGuard tooltip_guard(
955 {{ImGuiCol_PopupBg, theme.panel_bg_color},
956 {ImGuiCol_Border, theme.panel_border_color}});
957 if (ImGui::BeginTooltip()) {
958 const std::string filename =
959 obj_manager.ResolveFilename(obj_id, subtype);
960 const bool has_base = !custom_base_path.empty();
961 std::filesystem::path full_path =
962 has_base
963 ? (std::filesystem::path(custom_base_path) / filename)
964 : std::filesystem::path();
965 const bool file_exists = has_base && !filename.empty() &&
966 std::filesystem::exists(full_path);
967
968 ImGui::TextColored(theme.selection_primary, "Custom 0x%02X:%02X",
969 obj_id, subtype);
970 ImGui::Text("%s", subtype_name.c_str());
971 ImGui::Separator();
972 ImGui::Text("File: %s",
973 filename.empty() ? "(unmapped)" : filename.c_str());
974 if (!has_base) {
975 ImGui::TextColored(theme.text_warning_yellow,
976 "Folder not configured in project");
977 } else if (file_exists) {
978 ImGui::TextColored(theme.status_success, "File found");
979 } else {
980 ImGui::TextColored(theme.status_error, "File missing: %s",
981 full_path.string().c_str());
982 }
983
984 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
985 const char* corner_id = "";
986 if (subtype == 2) {
987 corner_id = "0x100 (TL)";
988 } else if (subtype == 3) {
989 corner_id = "0x102 (TR)";
990 } else if (subtype == 4) {
991 corner_id = "0x101 (BL)";
992 } else {
993 corner_id = "0x103 (BR)";
994 }
995 ImGui::Separator();
996 ImGui::TextColored(theme.status_active,
997 "Also used by corner override %s",
998 corner_id);
999 }
1000 ImGui::EndTooltip();
1001 }
1002 }
1003
1004 ImGui::PopID();
1005 custom_col = (custom_col + 1) % columns;
1006 }
1007 }
1008 }
1009 }
1010
1011 ImGui::EndChild();
1012}
1013
1014bool DungeonObjectSelector::MatchesObjectFilter(int obj_id, int filter_type) {
1015 switch (filter_type) {
1016 case 1: // Walls
1017 return obj_id >= 0x10 && obj_id <= 0x1F;
1018 case 2: // Floors
1019 return obj_id >= 0x20 && obj_id <= 0x2F;
1020 case 3: // Chests
1021 return obj_id == 0xF9 || obj_id == 0xFA;
1022 case 4: // Doors
1023 return obj_id >= 0x17 && obj_id <= 0x1E;
1024 case 5: // Decorations
1025 return obj_id >= 0x30 && obj_id <= 0x3F;
1026 case 6: // Stairs
1027 return obj_id >= 0x138 && obj_id <= 0x13B;
1028 default: // All
1029 return true;
1030 }
1031}
1032
1034 const std::string& name,
1035 int subtype) const {
1036 if (object_search_buffer_[0] == '\0') {
1037 return true;
1038 }
1039
1040 auto to_lower = [](std::string value) {
1041 std::transform(value.begin(), value.end(), value.begin(),
1042 [](unsigned char c) { return std::tolower(c); });
1043 return value;
1044 };
1045
1046 std::string needle = to_lower(object_search_buffer_);
1047 std::string name_lower = to_lower(name);
1048
1049 std::string id_hex = absl::StrFormat("%03X", obj_id);
1050 std::string id_lower = to_lower(id_hex);
1051 std::string id_pref = "0x" + id_lower;
1052
1053 if (name_lower.find(needle) != std::string::npos) {
1054 return true;
1055 }
1056 if (id_lower.find(needle) != std::string::npos ||
1057 id_pref.find(needle) != std::string::npos) {
1058 return true;
1059 }
1060
1061 if (subtype >= 0) {
1062 std::string sub_hex = absl::StrFormat("%02X", subtype);
1063 std::string sub_lower = to_lower(sub_hex);
1064 std::string combined = id_lower + ":" + sub_lower;
1065 std::string combined_pref = "0x" + combined;
1066 if (combined.find(needle) != std::string::npos ||
1067 combined_pref.find(needle) != std::string::npos) {
1068 return true;
1069 }
1070 }
1071
1072 return false;
1073}
1074
1076 const zelda3::RoomObject& object, int& width, int& height) {
1077 auto [w, h] = zelda3::DimensionService::Get().GetPixelDimensions(object);
1078 width = std::min(w, 256);
1079 height = std::min(h, 256);
1080}
1081
1084 return;
1085 }
1086
1087 // Create object with specified position
1088 auto placed_object = preview_object_;
1089 placed_object.set_x(static_cast<uint8_t>(x));
1090 placed_object.set_y(static_cast<uint8_t>(y));
1091
1092 // Call placement callback
1093 object_placement_callback_(placed_object);
1094}
1095
1097 ImGui::Text("Sprite Editor");
1098 Separator();
1099
1100 // Display current room sprites from Room data
1101 if (rooms_ && current_room_id_ >= 0 &&
1103 const auto& room = (*rooms_)[current_room_id_];
1104 const auto& sprites = room.GetSprites();
1105
1106 ImGui::Text("Sprites in room: %zu", sprites.size());
1107
1108 // Show first few sprites in compact format
1109 int display_count = std::min(3, static_cast<int>(sprites.size()));
1110 for (int i = 0; i < display_count; ++i) {
1111 const auto& sprite = sprites[i];
1112 ImGui::Text("ID:%02X (%d,%d) L%d", sprite.id(), sprite.x(), sprite.y(),
1113 sprite.layer());
1114 }
1115 if (sprites.size() > 3) {
1116 ImGui::Text("... and %zu more", sprites.size() - 3);
1117 }
1118 } else {
1119 ImGui::TextDisabled("No room selected");
1120 }
1121
1122 Separator();
1123 ImGui::TextDisabled("Use Sprite Editor panel for editing");
1124}
1125
1127 ImGui::Text("Item Editor");
1128 Separator();
1129
1130 // Display current room pot items from Room data
1131 if (rooms_ && current_room_id_ >= 0 &&
1133 const auto& room = (*rooms_)[current_room_id_];
1134 const auto& pot_items = room.GetPotItems();
1135
1136 ImGui::Text("Pot items in room: %zu", pot_items.size());
1137
1138 // Show first few items in compact format
1139 int display_count = std::min(3, static_cast<int>(pot_items.size()));
1140 for (int i = 0; i < display_count; ++i) {
1141 const auto& item = pot_items[i];
1142 ImGui::Text("Item:%02X (%d,%d)", item.item, item.GetTileX(),
1143 item.GetTileY());
1144 }
1145 if (pot_items.size() > 3) {
1146 ImGui::Text("... and %zu more", pot_items.size() - 3);
1147 }
1148 } else {
1149 ImGui::TextDisabled("No room selected");
1150 }
1151
1152 Separator();
1153 ImGui::TextDisabled("Use Item Editor panel for editing");
1154}
1155
1157 ImGui::Text("Entrance Editor");
1158 Separator();
1159
1160 // Entrances are managed through the dedicated Entrances panel
1161 // which accesses the entrances_ array in DungeonEditorV2
1162 ImGui::TextDisabled("Use Entrances panel for editing");
1163 ImGui::TextDisabled("Room entrances and connections");
1164}
1165
1167 const auto& theme = AgentUI::GetTheme();
1168
1169 ImGui::Text("Door Editor");
1170 Separator();
1171
1172 // Show doors from the Room data (if available)
1173 if (rooms_ && current_room_id_ >= 0 &&
1175 const auto& room = (*rooms_)[current_room_id_];
1176 const auto& doors = room.GetDoors();
1177
1178 ImGui::Text("Room Doors: %zu", doors.size());
1179
1180 if (!doors.empty()) {
1181 gui::StyledChild door_list("##DoorList", ImVec2(-1, 120),
1182 {.bg = ImVec4(0.1f, 0.1f, 0.15f, 0.5f)}, true);
1183 if (door_list) {
1184 for (size_t i = 0; i < doors.size(); ++i) {
1185 const auto& door = doors[i];
1186 auto [tile_x, tile_y] = door.GetTileCoords();
1187
1188 ImGui::PushID(static_cast<int>(i));
1189
1190 // Draw door info with type name
1191 std::string type_name(zelda3::GetDoorTypeName(door.type));
1192 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
1193 ImGui::Text("[%zu] %s (%s) at tile(%d,%d)", i, type_name.c_str(),
1194 dir_name.c_str(), tile_x, tile_y);
1195
1196 // Delete button
1197 ImGui::SameLine();
1198 if (ImGui::SmallButton("X")) {
1199 // Remove door (mutable ref needed)
1200 auto& mutable_room = (*rooms_)[current_room_id_];
1201 mutable_room.RemoveDoor(i);
1202 }
1203
1204 ImGui::PopID();
1205 }
1206 }
1207 }
1208
1209 // Door type selector
1210 Separator();
1211 ImGui::Text("Door Type:");
1212 static int selected_door_type =
1213 static_cast<int>(zelda3::DoorType::NormalDoor);
1214
1215 // Build door type combo items (common types)
1216 constexpr std::array<zelda3::DoorType, 20> door_types = {
1237 };
1238
1239 if (ImGui::BeginCombo(
1240 "##DoorType",
1241 std::string(zelda3::GetDoorTypeName(
1242 static_cast<zelda3::DoorType>(selected_door_type)))
1243 .c_str())) {
1244 for (auto door_type : door_types) {
1245 bool is_selected = (selected_door_type == static_cast<int>(door_type));
1246 if (ImGui::Selectable(
1247 std::string(zelda3::GetDoorTypeName(door_type)).c_str(),
1248 is_selected)) {
1249 selected_door_type = static_cast<int>(door_type);
1250 }
1251 if (is_selected) {
1252 ImGui::SetItemDefaultFocus();
1253 }
1254 }
1255 ImGui::EndCombo();
1256 }
1257
1258 // Instructions
1259 ImGui::TextWrapped(
1260 "Click on a room wall edge to place a door. Doors snap to valid "
1261 "positions.");
1262 } else {
1263 ImGui::Text("No room selected");
1264 }
1265}
1266
1268 ImGui::Text("Chest Editor");
1269 Separator();
1270
1271 // Display current room chests from Room data
1272 if (rooms_ && current_room_id_ >= 0 &&
1274 const auto& room = (*rooms_)[current_room_id_];
1275 const auto& chests = room.GetChests();
1276
1277 ImGui::Text("Chests in room: %zu", chests.size());
1278
1279 // Show chests in compact format
1280 for (size_t i = 0; i < chests.size(); ++i) {
1281 const auto& chest = chests[i];
1282 ImGui::Text("[%zu] Item:%02X %s", i, chest.id,
1283 chest.size ? "(Big)" : "(Small)");
1284 }
1285 } else {
1286 ImGui::TextDisabled("No room selected");
1287 }
1288
1289 Separator();
1290 ImGui::TextDisabled("Chest editing through Room data");
1291}
1292
1294 ImGui::Text("Room Properties");
1295 Separator();
1296
1297 // Display current room properties from Room data
1298 if (rooms_ && current_room_id_ >= 0 &&
1300 const auto& room = (*rooms_)[current_room_id_];
1301
1302 ImGui::Text("Room ID: %03X", current_room_id_);
1303 ImGui::Text("Blockset: %d", room.blockset());
1304 ImGui::Text("Spriteset: %d", room.spriteset());
1305 ImGui::Text("Palette: %d", room.palette());
1306 ImGui::Text("Layout: %d", room.layout_id());
1307
1308 Separator();
1309 ImGui::Text("Header Data");
1310 ImGui::Text("Floor1: %d", room.floor1());
1311 ImGui::Text("Floor2: %d", room.floor2());
1312 ImGui::Text("Effect: %d", static_cast<int>(room.effect()));
1313 ImGui::Text("Tag1: %d", static_cast<int>(room.tag1()));
1314 ImGui::Text("Tag2: %d", static_cast<int>(room.tag2()));
1315 } else {
1316 ImGui::TextDisabled("No room selected");
1317 }
1318
1319 Separator();
1320 ImGui::TextDisabled("Full editing in Room Properties panel");
1321}
1322
1329
1330void DungeonObjectSelector::SetCustomObjectsFolder(const std::string& folder) {
1331 if (custom_objects_folder_ != folder) {
1332 custom_objects_folder_ = folder;
1335 }
1337}
1338
1348
1350 zelda3::RoomObject obj(obj_id, 0, 0, 0x12, 0);
1351 obj.SetRom(rom_);
1352 obj.EnsureTilesLoaded();
1353 return obj;
1354}
1355
1360
1362 float size,
1363 gfx::BackgroundBuffer** out) {
1364 if (!rom_ || !rom_->is_loaded()) {
1365 return false;
1366 }
1368
1369 // Check if room context changed - invalidate cache if so
1370 if (rooms_ && current_room_id_ < static_cast<int>(rooms_->size())) {
1371 const auto& room = (*rooms_)[current_room_id_];
1372 if (!room.IsLoaded()) {
1373 return false; // Can't render without loaded room
1374 }
1375
1376 // Invalidate cache if room/palette/blockset changed
1378 room.blockset() != cached_preview_blockset_ ||
1379 room.palette() != cached_preview_palette_) {
1382 cached_preview_blockset_ = room.blockset();
1383 cached_preview_palette_ = room.palette();
1384 }
1385 } else {
1386 return false;
1387 }
1388
1389 // Check if already in cache
1390 // Key: (object_id << 32) | (subtype << 16) | (blockset << 8) | palette
1391 int subtype = object.size_ & 0x1F;
1392 uint64_t cache_key = (static_cast<uint64_t>(object.id_) << 32) |
1393 (static_cast<uint64_t>(subtype) << 16) |
1394 (static_cast<uint64_t>(cached_preview_blockset_) << 8) |
1395 static_cast<uint64_t>(cached_preview_palette_);
1396
1397 auto it = preview_cache_.find(cache_key);
1398 if (it != preview_cache_.end()) {
1399 *out = it->second.get();
1400 return (*out)->bitmap().texture() != nullptr;
1401 }
1402
1403 // Create new preview using ObjectTileEditor
1404 auto& room = (*rooms_)[current_room_id_];
1405 const uint8_t* gfx_data = room.get_gfx_buffer().data();
1406
1408 auto layout_or =
1409 editor.CaptureObjectLayout(object.id_, room, current_palette_group_);
1410 if (!layout_or.ok()) {
1411 return false;
1412 }
1413 const auto& layout = layout_or.value();
1414
1415 // Create preview buffer large enough for object
1416 int bmp_w = std::max(8, layout.bounds_width * 8);
1417 int bmp_h = std::max(8, layout.bounds_height * 8);
1418 auto preview = std::make_unique<gfx::BackgroundBuffer>(bmp_w, bmp_h);
1419 preview->EnsureBitmapInitialized();
1420
1421 // Render layout to bitmap
1422 auto render_status = editor.RenderLayoutToBitmap(
1423 layout, preview->bitmap(), gfx_data, current_palette_group_);
1424 if (!render_status.ok()) {
1425 return false;
1426 }
1427
1428 auto& bitmap = preview->bitmap();
1429 // Texture creation and SDL sync
1430 if (bitmap.surface()) {
1431 // Sync to surface
1432 SDL_LockSurface(bitmap.surface());
1433 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
1434 bitmap.mutable_data().size());
1435 SDL_UnlockSurface(bitmap.surface());
1436
1437 // Create texture
1441 }
1442
1443 if (!bitmap.texture()) {
1444 return false;
1445 }
1446
1447 // Store in cache and return
1448 *out = preview.get();
1449 preview_cache_[cache_key] = std::move(preview);
1450 return true;
1451}
1452
1454 ImVec2 top_left, float size) {
1455 gfx::BackgroundBuffer* preview = nullptr;
1456 if (!GetOrCreatePreview(object, size, &preview)) {
1457 return false;
1458 }
1459
1460 // Draw the cached preview image
1461 auto& bitmap = preview->bitmap();
1462 if (!bitmap.texture()) {
1463 return false;
1464 }
1465
1466 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1467 ImVec2 bottom_right(top_left.x + size, top_left.y + size);
1468 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), top_left,
1469 bottom_right);
1470 return true;
1471}
1472
1474 if (show_create_dialog_) {
1475 ImGui::OpenPopup("New Custom Object");
1476 show_create_dialog_ = false;
1477 }
1478
1479 if (ImGui::BeginPopupModal("New Custom Object", nullptr,
1480 ImGuiWindowFlags_AlwaysAutoResize)) {
1481 ImGui::Text("Create a new custom dungeon object");
1482 ImGui::Separator();
1483
1484 // Dimensions
1485 ImGui::SliderInt("Width (tiles)", &create_width_, 1, 32);
1486 ImGui::SliderInt("Height (tiles)", &create_height_, 1, 32);
1487
1488 // Object group
1489 const char* group_labels[] = {"0x31 - Track/Custom", "0x32 - Misc"};
1490 int group_index = (create_object_id_ == 0x32) ? 1 : 0;
1491 if (ImGui::Combo("Object Group", &group_index, group_labels,
1492 IM_ARRAYSIZE(group_labels))) {
1493 create_object_id_ = (group_index == 1) ? 0x32 : 0x31;
1494 // Regenerate filename when group changes
1495 snprintf(create_filename_, sizeof(create_filename_),
1496 "custom_%02x_%02d.bin", create_object_id_,
1497 zelda3::CustomObjectManager::Get().GetSubtypeCount(
1499 }
1500
1501 // Filename
1502 ImGui::InputText("Filename", create_filename_, sizeof(create_filename_));
1503
1504 // Validation
1505 bool valid = true;
1506 std::string error_msg;
1507
1508 if (create_filename_[0] == '\0') {
1509 valid = false;
1510 error_msg = "Filename cannot be empty";
1511 } else if (!rooms_ || current_room_id_ < 0) {
1512 valid = false;
1513 error_msg = "Load a room first (needed for tile graphics)";
1514 } else {
1516 if (mgr.GetBasePath().empty()) {
1517 valid = false;
1518 error_msg = "Custom objects folder not configured in project";
1519 } else {
1520 // Check if file already exists
1521 auto path = std::filesystem::path(mgr.GetBasePath()) / create_filename_;
1522 if (std::filesystem::exists(path)) {
1523 valid = false;
1524 error_msg = "File already exists: " + std::string(create_filename_);
1525 }
1526 }
1527 }
1528
1529 if (!error_msg.empty()) {
1530 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
1531 error_msg.c_str());
1532 }
1533
1534 ImGui::Separator();
1535
1536 if (!valid)
1537 ImGui::BeginDisabled();
1538 if (ImGui::Button("Create", ImVec2(120, 0))) {
1541 static_cast<int16_t>(create_object_id_), current_room_id_, rooms_);
1542 ImGui::CloseCurrentPopup();
1543 }
1544 if (!valid)
1545 ImGui::EndDisabled();
1546
1547 ImGui::SameLine();
1548 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
1549 ImGui::CloseCurrentPopup();
1550 }
1551
1552 ImGui::EndPopup();
1553 }
1554}
1555
1556} // namespace yaze::editor
auto data() const
Definition rom.h:139
bool is_loaded() const
Definition rom.h:132
std::function< void(const zelda3::RoomObject &) object_placement_callback_)
zelda3::DungeonObjectEditor * object_editor_
std::array< zelda3::Room, 0x128 > * rooms_
bool GetOrCreatePreview(const zelda3::RoomObject &object, float size, gfx::BackgroundBuffer **out)
void SetCustomObjectsFolder(const std::string &folder)
void CalculateObjectDimensions(const zelda3::RoomObject &object, int &width, int &height)
zelda3::DungeonObjectRegistry object_registry_
std::function< void(int)> object_double_click_callback_
void RenderObjectPrimitive(const zelda3::RoomObject &object, int x, int y)
bool MatchesObjectSearch(int obj_id, const std::string &name, int subtype=-1) const
std::unique_ptr< zelda3::DungeonEditorSystem > * dungeon_editor_system_
void SelectObject(int obj_id, int subtype=-1)
std::function< void(const zelda3::RoomObject &) object_selected_callback_)
std::map< uint64_t, std::unique_ptr< gfx::BackgroundBuffer > > preview_cache_
zelda3::RoomObject MakePreviewObject(int obj_id) const
std::map< uint32_t, zelda3::ObjectTileLayout > layout_cache_
bool DrawObjectPreview(const zelda3::RoomObject &object, ImVec2 top_left, float size)
bool MatchesObjectFilter(int obj_id, int filter_type)
void OpenForNewObject(int width, int height, const std::string &filename, int16_t object_id, int room_id, std::array< zelda3::Room, 0x128 > *rooms)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
static Arena & Get()
Definition arena.cc:21
Lightweight RAII guard for existing Canvas instances.
Definition canvas.h:869
auto height() const
Definition canvas.h:495
auto width() const
Definition canvas.h:494
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1093
void AddImageAt(ImTextureID texture, ImVec2 local_top_left, ImVec2 size)
Definition canvas.cc:2489
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1423
void DrawText(const std::string &text, int x, int y)
Definition canvas.cc:1428
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui child windows with optional styling.
static CustomObjectManager & Get()
void Initialize(const std::string &custom_objects_folder)
static DimensionService & Get()
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
void RegisterVanillaRange(int16_t start_id, int16_t end_id)
Draws dungeon objects to background buffers using game patterns.
int GetDrawRoutineId(int16_t object_id) const
Get draw routine ID for an object.
Captures and edits the tile8 composition of dungeon objects.
absl::Status RenderLayoutToBitmap(const ObjectTileLayout &layout, gfx::Bitmap &bitmap, const uint8_t *room_gfx_buffer, const gfx::PaletteGroup &palette)
absl::StatusOr< ObjectTileLayout > CaptureObjectLayout(int16_t object_id, const Room &room, const gfx::PaletteGroup &palette)
void set_x(uint8_t x)
Definition room_object.h:76
void SetRom(Rom *rom)
Definition room_object.h:70
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_CLEAR
Definition icons.h:416
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
void ThemedTooltip(const char *text)
Draw a tooltip with theme-aware background and borders.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
bool ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ SmallKeyStairsUp
Small key stairs (upwards)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ ExitMarker
Exit marker.
@ DoubleSidedShutter
Double sided shutter door.
int GetObjectSubtype(int object_id)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:161
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
constexpr int kNumberOfRooms
Treasure chest.
Definition zelda.h:425
std::optional< float > grid_step
Definition canvas.h:70
static constexpr float kContentMinHeightCanvas
Definition ui_config.h:56
static constexpr float kContentMinWidthSidebar
Definition ui_config.h:58
static constexpr float kContentMinHeightList
Definition ui_config.h:54
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89