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#include "util/i18n/tr.h"
5
6// C system headers
7#include <cstring>
8#include <filesystem>
9
10// C++ standard library headers
11#include <algorithm>
12#include <cctype>
13#include <iterator>
14
15// Third-party library headers
16#include "imgui/imgui.h"
17
18// Project headers
22#include "app/gui/core/icons.h"
26#include "rom/rom.h"
27#include "zelda3/dungeon/custom_object.h" // For CustomObjectManager
32#include "zelda3/dungeon/room.h"
33#include "zelda3/dungeon/room_object.h" // For GetObjectName()
34
35namespace yaze::editor {
36
37namespace {
38
39float GetObjectGridItemSize(int density) {
40 switch (density) {
41 case 0:
42 return 56.0f;
43 case 2:
44 return 88.0f;
45 case 1:
46 default:
47 return 72.0f;
48 }
49}
50
51ImU32 ThemeColor(const ImVec4& color) {
52 return ImGui::ColorConvertFloat4ToU32(color);
53}
54
55ImVec4 WithAlpha(ImVec4 color, float alpha) {
56 color.w = alpha;
57 return color;
58}
59
60ImVec4 Dimmed(ImVec4 color, float factor) {
61 color.x *= factor;
62 color.y *= factor;
63 color.z *= factor;
64 return color;
65}
66
67ImU32 GetObjectRangeHeaderColor(int start_id) {
68 const auto& theme = AgentUI::GetTheme();
69 if (start_id >= 0xF80) {
70 return ThemeColor(theme.music_zone_color);
71 }
72 if (start_id >= 0x100) {
73 return ThemeColor(theme.selection_secondary);
74 }
75 return ThemeColor(theme.dungeon_object_wall);
76}
77
78ImU32 GetObjectRangeHeaderHoverColor(int start_id) {
79 const auto& theme = AgentUI::GetTheme();
80 if (start_id >= 0xF80) {
81 return ThemeColor(WithAlpha(theme.music_zone_color, 1.0f));
82 }
83 if (start_id >= 0x100) {
84 return ThemeColor(WithAlpha(theme.selection_secondary, 1.0f));
85 }
86 return ThemeColor(WithAlpha(theme.dungeon_object_wall, 1.0f));
87}
88
89void DrawFallbackPreviewTile(ImDrawList* draw_list, ImVec2 top_left,
90 float item_size, const ImVec4& accent_color,
91 const char* label) {
92 const auto& theme = AgentUI::GetTheme();
93 const ImU32 accent = ThemeColor(accent_color);
94 const ImU32 dimmed = ThemeColor(Dimmed(accent_color, 0.55f));
95
96 draw_list->AddRectFilledMultiColor(
97 top_left, ImVec2(top_left.x + item_size, top_left.y + item_size), dimmed,
98 dimmed, accent, accent);
99
100 ImVec2 label_size = ImGui::CalcTextSize(label);
101 ImVec2 label_pos(top_left.x + (item_size - label_size.x) / 2,
102 top_left.y + (item_size - label_size.y) / 2 - 10);
103 draw_list->AddText(label_pos,
104 ThemeColor(WithAlpha(theme.text_primary, 0.82f)), label);
105}
106
107} // namespace
108
110 const auto& theme = AgentUI::GetTheme();
111
112 // Type 3 objects (0xF80-0xFFF) - Special room features
113 if (object_id >= 0xF80) {
114 if (object_id >= 0xF80 && object_id <= 0xF8F) {
115 return ImGui::ColorConvertFloat4ToU32(
116 theme.selection_secondary); // Light blue for layer indicators
117 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
118 return ImGui::ColorConvertFloat4ToU32(
119 theme.transport_color); // Orange/Purple for door indicators
120 } else {
121 return ImGui::ColorConvertFloat4ToU32(
122 theme.music_zone_color); // Purple for misc Type 3
123 }
124 }
125
126 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
127 if (object_id >= 0x100 && object_id < 0x200) {
128 if (object_id >= 0x100 && object_id <= 0x10F) {
129 return ImGui::GetColorU32(theme.status_warning); // Torches
130 } else if (object_id >= 0x110 && object_id <= 0x11F) {
131 return ImGui::GetColorU32(theme.dungeon_object_default); // Blocks
132 } else if (object_id >= 0x120 && object_id <= 0x12F) {
133 return ImGui::ColorConvertFloat4ToU32(
134 theme.status_success); // Green for switches
135 } else if (object_id >= 0x130 && object_id <= 0x13F) {
136 return ImGui::GetColorU32(theme.selection_primary); // Yellow for stairs
137 } else {
138 return ImGui::GetColorU32(theme.text_secondary_gray); // Other Type 2
139 }
140 }
141
142 // Type 1 objects (0x00-0xFF) - Base room objects
143 if (object_id >= 0x10 && object_id <= 0x1F) {
144 return ImGui::GetColorU32(theme.dungeon_object_wall); // Gray for walls
145 } else if (object_id >= 0x20 && object_id <= 0x2F) {
146 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for floors
147 } else if (object_id == 0xF9 || object_id == 0xFA) {
148 return ImGui::GetColorU32(theme.item_color); // Gold for chests
149 } else if (object_id >= 0x17 && object_id <= 0x1E) {
150 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for doors
151 } else if (object_id == 0x2F || object_id == 0x2B) {
152 return ImGui::GetColorU32(
153 theme.dungeon_object_pot); // Saddle brown for pots
154 } else if (object_id >= 0x30 && object_id <= 0x3F) {
155 return ImGui::GetColorU32(
156 theme.dungeon_object_decoration); // Dim gray for decorations
157 } else if (object_id >= 0x00 && object_id <= 0x0F) {
158 return ImGui::GetColorU32(theme.dungeon_selection_secondary); // Corners
159 } else {
160 return ImGui::GetColorU32(theme.dungeon_object_default); // Default gray
161 }
162}
163
165 // Type 3 objects (0xF80-0xFFF) - Special room features
166 if (object_id >= 0xF80) {
167 if (object_id >= 0xF80 && object_id <= 0xF8F) {
168 return "L"; // Layer
169 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
170 return "D"; // Door indicator
171 } else {
172 return "S"; // Special
173 }
174 }
175
176 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
177 if (object_id >= 0x100 && object_id < 0x200) {
178 if (object_id >= 0x100 && object_id <= 0x10F) {
179 return "*"; // Torch (flame)
180 } else if (object_id >= 0x110 && object_id <= 0x11F) {
181 return "#"; // Block
182 } else if (object_id >= 0x120 && object_id <= 0x12F) {
183 return "o"; // Switch
184 } else if (object_id >= 0x130 && object_id <= 0x13F) {
185 return "^"; // Stairs
186 } else {
187 return "2"; // Type 2
188 }
189 }
190
191 // Type 1 objects (0x00-0xFF) - Base room objects
192 if (object_id >= 0x10 && object_id <= 0x1F) {
193 return "|"; // Wall
194 } else if (object_id >= 0x20 && object_id <= 0x2F) {
195 return "_"; // Floor
196 } else if (object_id == 0xF9 || object_id == 0xFA) {
197 return "C"; // Chest
198 } else if (object_id >= 0x17 && object_id <= 0x1E) {
199 return "+"; // Door
200 } else if (object_id == 0x2F || object_id == 0x2B) {
201 return "o"; // Pot
202 } else if (object_id >= 0x30 && object_id <= 0x3F) {
203 return "~"; // Decoration
204 } else if (object_id >= 0x00 && object_id <= 0x0F) {
205 return "/"; // Corner
206 } else {
207 return "?"; // Unknown
208 }
209}
210
211void DungeonObjectSelector::SelectObject(int obj_id, int subtype) {
212 selected_object_id_ = obj_id;
213
214 // Create and update preview object
215 uint8_t size = 0x12;
216 if (subtype >= 0) {
217 size = static_cast<uint8_t>(subtype & 0x1F);
218 }
219 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, size, 0);
221 if (game_data_) {
222 auto palette =
224 preview_palette_ = palette;
225 }
226 object_loaded_ = true;
227
228 // Notify callback
231 }
232}
233
235 const auto& theme = AgentUI::GetTheme();
236
237 // Object ranges: Type 1 (0x00-0xFF), Type 2 (0x100-0x141), Type 3 (0xF80-0xFFF)
238 struct ObjectRange {
239 int start;
240 int end;
241 const char* label;
242 };
243 static const ObjectRange ranges[] = {
244 {0x00, 0xFF, "Type 1"},
245 {0x100, 0x141, "Type 2"},
246 {0xF80, 0xFFF, "Type 3"},
247 };
248
249 // Total object count
250 int total_objects =
251 (0xFF - 0x00 + 1) + (0x141 - 0x100 + 1) + (0xFFF - 0xF80 + 1);
252
254 auto& obj_manager = zelda3::CustomObjectManager::Get();
255 const int custom_count =
256 obj_manager.GetSubtypeCount(0x31) + obj_manager.GetSubtypeCount(0x32);
257
258 // Row 1: search input, full-width since this is the most-used control.
259 ImGui::SetNextItemWidth(-1.0f);
260 ImGui::InputTextWithHint(
261 "##ObjectSearch", ICON_MD_SEARCH " Filter objects by name or hex...",
263
264 // Row 2: category filter + clear + display popover. Display options
265 // (thumbnails toggle, grid density) are rare-use preferences hidden behind
266 // an ICON_MD_TUNE popover so the chrome stays one row instead of three.
267 static const char* kFilterLabels[] = {"All", "Walls", "Floors", "Chests",
268 "Doors", "Decor", "Stairs"};
269 const float controls_width = ImGui::GetContentRegionAvail().x;
270 const float filter_width = std::min(170.0f, controls_width);
271 ImGui::SetNextItemWidth(filter_width);
272 ImGui::Combo("##ObjectFilterType", &object_type_filter_, kFilterLabels,
273 IM_ARRAYSIZE(kFilterLabels));
274 ImGui::SameLine();
275 if (gui::ThemedIconButton(ICON_MD_CLEAR, "Clear search and category filter",
277 object_search_buffer_[0] = '\0';
279 }
280 ImGui::SameLine();
281 if (gui::ThemedIconButton(ICON_MD_TUNE, "Display options",
283 ImGui::OpenPopup("##ObjectSelectorDisplayPopup");
284 }
285 if (ImGui::BeginPopup("##ObjectSelectorDisplayPopup")) {
286 ImGui::TextDisabled(tr("Display"));
287 ImGui::Checkbox(ICON_MD_IMAGE " Thumbnails", &enable_object_previews_);
288 if (ImGui::IsItemHovered()) {
289 ImGui::SetTooltip(
290 tr("Show rendered object thumbnails in the selector.\n"
291 "Requires a room to be loaded and may cost some performance."));
292 }
293 ImGui::Spacing();
294 ImGui::TextDisabled(tr("Grid density"));
295 static constexpr const char* kDensityLabels[] = {"Small", "Medium",
296 "Large"};
297 constexpr float kDensitySegmentWidth = 84.0f;
298 const float density_spacing = ImGui::GetStyle().ItemSpacing.x;
299 for (int density = 0; density < 3; ++density) {
300 if (density > 0) {
301 ImGui::SameLine(0.0f, density_spacing);
302 }
303 if (gui::ToggleButton(kDensityLabels[density],
304 object_grid_density_ == density,
305 ImVec2(kDensitySegmentWidth, 0.0f))) {
306 object_grid_density_ = density;
307 }
308 }
309 ImGui::EndPopup();
310 }
311
312 // Row 3: status row, count + selection chip + Custom Workshop entry.
313 // Inlines on wider drawers, wraps on narrow.
314 ImGui::Spacing();
315 ImGui::TextColored(theme.text_secondary_gray, tr("%d vanilla objects"),
316 total_objects);
317 if (selected_object_id_ >= 0) {
318 if (ImGui::GetContentRegionAvail().x > 220.0f) {
319 ImGui::SameLine();
320 }
321 ImGui::TextColored(theme.text_info, ICON_MD_LABEL " Queued 0x%03X %s",
324 }
325 if (ImGui::GetContentRegionAvail().x > 180.0f) {
326 ImGui::SameLine();
327 }
328 DrawCustomObjectWorkshopButton(custom_count);
329
330 // Create asset browser-style grid
331 const float item_spacing = 6.0f;
332 // Defensive clamp: when the inspector drawer narrows below the user's chosen
333 // density, scale the cell down so at least one item is fully visible per row
334 // before the horizontal scrollbar engages. The 16px margin accounts for the
335 // child window's vertical scrollbar plus a small buffer for the cell border.
336 constexpr float kRightMargin = 16.0f;
337 constexpr float kMinItemSize = 32.0f;
338 const float available_width = ImGui::GetContentRegionAvail().x;
339 const float requested_item_size = GetObjectGridItemSize(object_grid_density_);
340 const float item_size =
341 std::min(requested_item_size,
342 std::max(kMinItemSize, available_width - kRightMargin));
343 const int columns =
344 std::max(1, static_cast<int>((available_width - item_spacing) /
345 (item_size + item_spacing)));
346
347 // Scrollable child region for grid - use all available space.
348 // Horizontal scrollbar guards against silent right-edge clipping when a row
349 // overruns due to per-frame column recalculation; the Item/Sprite selectors
350 // use the same flag combination.
351 float child_height = ImGui::GetContentRegionAvail().y;
352 if (ImGui::BeginChild("##ObjectGrid", ImVec2(0, child_height), false,
353 ImGuiWindowFlags_AlwaysVerticalScrollbar |
354 ImGuiWindowFlags_HorizontalScrollbar)) {
355
356 // Iterate through all object ranges
357 for (const auto& range : ranges) {
358 // Section header for each type
359 const ImU32 header_color = GetObjectRangeHeaderColor(range.start);
360 const ImU32 header_hover_color =
361 GetObjectRangeHeaderHoverColor(range.start);
362 gui::StyleColorGuard section_guard(
363 {{ImGuiCol_Header, ImGui::ColorConvertU32ToFloat4(header_color)},
364 {ImGuiCol_HeaderHovered,
365 ImGui::ColorConvertU32ToFloat4(header_hover_color)}});
366 bool section_open = ImGui::CollapsingHeader(
367 absl::StrFormat("%s (0x%03X-0x%03X)", range.label, range.start,
368 range.end)
369 .c_str(),
370 ImGuiTreeNodeFlags_DefaultOpen);
371
372 if (!section_open)
373 continue;
374
375 int current_column = 0;
376
377 for (int obj_id = range.start; obj_id <= range.end; ++obj_id) {
379 continue;
380 }
381
382 std::string full_name = zelda3::GetObjectName(obj_id);
383 if (!MatchesObjectSearch(obj_id, full_name)) {
384 continue;
385 }
386
387 if (current_column > 0) {
388 ImGui::SameLine();
389 }
390
391 ImGui::PushID(obj_id);
392
393 // Create selectable button for object
394 bool is_selected = (selected_object_id_ == obj_id);
395 ImVec2 button_size(item_size, item_size);
396
397 if (ImGui::Selectable("", is_selected, 0, button_size)) {
398 selected_object_id_ = obj_id;
399
400 // Create and update preview object
401 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, 0x12, 0);
403 if (game_data_ &&
406 auto palette = game_data_->palette_groups
408 preview_palette_ = palette;
409 }
410 object_loaded_ = true;
411
412 // Notify callbacks
415 }
416 }
417 const bool item_visible = ImGui::IsItemVisible();
418 ImVec2 button_pos = ImGui::GetItemRectMin();
419 gui::BeginRoomObjectDragSource(static_cast<uint16_t>(obj_id),
420 current_room_id_, 0, 0, 0x12);
421 // Draw object preview on the button; fall back to styled placeholder
422 ImDrawList* draw_list = ImGui::GetWindowDrawList();
423
424 // Only attempt graphical preview if enabled (performance optimization)
425 bool rendered = false;
426 if (item_visible && enable_object_previews_) {
427 rendered = DrawObjectPreview(MakePreviewObject(obj_id), button_pos,
428 item_size);
429 }
430
431 if (item_visible && !rendered) {
432 // Draw a styled fallback with gradient background
433 std::string symbol = GetObjectTypeSymbol(obj_id);
434 DrawFallbackPreviewTile(
435 draw_list, button_pos, item_size,
436 ImGui::ColorConvertU32ToFloat4(GetObjectTypeColor(obj_id)),
437 symbol.c_str());
438 }
439
440 // Draw selection border
441 ImU32 border_color;
442 float border_thickness;
443
444 if (is_selected) {
445 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
446 border_thickness = 3.0f;
447 } else {
448 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
449 border_thickness = 1.0f;
450 }
451
452 if (item_visible) {
453 draw_list->AddRect(
454 button_pos,
455 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
456 border_color, 0.0f, 0, border_thickness);
457 }
458
459 // Get object name for display
460 // Truncate name for display
461 std::string display_name = full_name;
462 const size_t max_display_chars =
463 std::max<size_t>(7, static_cast<size_t>(item_size / 6.0f));
464 if (display_name.length() > max_display_chars) {
465 display_name = display_name.substr(0, max_display_chars - 2) + "..";
466 }
467
468 if (item_visible) {
469 // Draw object name (smaller, above ID)
470 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
471 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
472 button_pos.y + item_size - 26);
473 draw_list->AddText(name_pos,
474 ImGui::GetColorU32(theme.text_secondary_gray),
475 display_name.c_str());
476
477 // Draw object ID at bottom (hex format)
478 std::string id_text = absl::StrFormat("%03X", obj_id);
479 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
480 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
481 button_pos.y + item_size - id_size.y - 2);
482 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
483 id_text.c_str());
484 }
485
486 // Enhanced tooltip
487 if (ImGui::IsItemHovered()) {
488 gui::StyleColorGuard tooltip_guard(
489 {{ImGuiCol_PopupBg, theme.panel_bg_color},
490 {ImGuiCol_Border, theme.panel_border_color}});
491
492 if (ImGui::BeginTooltip()) {
493 ImGui::TextColored(theme.selection_primary, tr("Object 0x%03X"),
494 obj_id);
495 ImGui::Text("%s", full_name.c_str());
496 int subtype = zelda3::GetObjectSubtype(obj_id);
497 ImGui::TextColored(theme.text_secondary_gray, tr("Subtype %d"),
498 subtype);
499 ImGui::TextColored(
500 rendered ? theme.status_success : theme.status_warning,
501 tr("Preview: %s"),
502 rendered ? "rendered tile layout"
503 : (enable_object_previews_ ? "fallback symbol"
504 : "thumbnails off"));
505 ImGui::Separator();
506
507 uint32_t layout_key = (static_cast<uint32_t>(obj_id) << 16) |
508 static_cast<uint32_t>(subtype);
509 const bool can_capture_layout =
510 rom_ && rooms_ && current_room_id_ >= 0 &&
512 if (can_capture_layout &&
513 layout_cache_.find(layout_key) == layout_cache_.end()) {
515 auto& room_ref = (*rooms_)[current_room_id_];
516 auto layout_or = editor.CaptureObjectLayout(
517 obj_id, room_ref, current_palette_group_);
518 if (layout_or.ok()) {
519 layout_cache_[layout_key] = layout_or.value();
520 }
521 }
522
523 if (layout_cache_.count(layout_key)) {
524 const auto& layout = layout_cache_[layout_key];
525 ImGui::TextColored(theme.status_success, tr("Tiles: %zu"),
526 layout.cells.size());
527
528 if (can_capture_layout) {
529 auto& room_ref = (*rooms_)[current_room_id_];
531 room_ref.get_gfx_buffer().data());
532 int rid = drawer.GetDrawRoutineId(obj_id);
533 ImGui::TextColored(theme.status_active, tr("Draw Routine: %d"),
534 rid);
535 }
536
537 ImGui::Text(tr("Layout:"));
538 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
539 ImVec2 grid_start = ImGui::GetCursorScreenPos();
540 float cell_size = 4.0f;
541 for (const auto& cell : layout.cells) {
542 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
543 grid_start.y + cell.rel_y * cell_size);
544 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
545 tooltip_draw_list->AddRectFilled(
546 p1, p2, ThemeColor(theme.dungeon_grid_cell_highlight));
547 tooltip_draw_list->AddRect(
548 p1, p2, ThemeColor(theme.panel_border_color));
549 }
550 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
551 layout.bounds_height * cell_size));
552 }
553
554 ImGui::Separator();
555 ImGui::TextColored(theme.text_secondary_gray,
556 tr("Click to select for placement"));
557 ImGui::EndTooltip();
558 }
559 }
560
561 ImGui::PopID();
562
563 current_column = (current_column + 1) % columns;
564 } // end object loop
565 } // end range loop
566 }
567
568 ImGui::EndChild();
570}
571
572bool DungeonObjectSelector::MatchesObjectFilter(int obj_id, int filter_type) {
573 switch (filter_type) {
574 case 1: // Walls
575 return obj_id >= 0x10 && obj_id <= 0x1F;
576 case 2: // Floors
577 return obj_id >= 0x20 && obj_id <= 0x2F;
578 case 3: // Chests
579 return obj_id == 0xF9 || obj_id == 0xFA;
580 case 4: // Doors
581 return obj_id >= 0x17 && obj_id <= 0x1E;
582 case 5: // Decorations
583 return obj_id >= 0x30 && obj_id <= 0x3F;
584 case 6: // Stairs
585 return obj_id >= 0x138 && obj_id <= 0x13B;
586 default: // All
587 return true;
588 }
589}
590
592 const std::string& name,
593 int subtype) const {
594 if (object_search_buffer_[0] == '\0') {
595 return true;
596 }
597
598 auto to_lower = [](std::string value) {
599 std::transform(value.begin(), value.end(), value.begin(),
600 [](unsigned char c) { return std::tolower(c); });
601 return value;
602 };
603
604 std::string needle = to_lower(object_search_buffer_);
605 std::string name_lower = to_lower(name);
606
607 std::string id_hex = absl::StrFormat("%03X", obj_id);
608 std::string id_lower = to_lower(id_hex);
609 std::string id_pref = "0x" + id_lower;
610
611 if (name_lower.find(needle) != std::string::npos) {
612 return true;
613 }
614 if (id_lower.find(needle) != std::string::npos ||
615 id_pref.find(needle) != std::string::npos) {
616 return true;
617 }
618
619 if (subtype >= 0) {
620 std::string sub_hex = absl::StrFormat("%02X", subtype);
621 std::string sub_lower = to_lower(sub_hex);
622 std::string combined = id_lower + ":" + sub_lower;
623 std::string combined_pref = "0x" + combined;
624 if (combined.find(needle) != std::string::npos ||
625 combined_pref.find(needle) != std::string::npos) {
626 return true;
627 }
628 }
629
630 return false;
631}
632
634 const zelda3::RoomObject& object, int& width, int& height) {
636 width = std::min(w, 256);
637 height = std::min(h, 256);
638}
639
646
655
665
667 zelda3::RoomObject obj(obj_id, 0, 0, 0x12, 0);
668 obj.SetRom(rom_);
669 obj.EnsureTilesLoaded();
670 return obj;
671}
672
678
680 float size,
681 gfx::BackgroundBuffer** out) {
682 if (!rom_ || !rom_->is_loaded()) {
683 return false;
684 }
686
687 // Check if room context changed - invalidate cache if so
688 if (rooms_ && current_room_id_ < static_cast<int>(rooms_->size())) {
689 const auto& room = (*rooms_)[current_room_id_];
690 if (!room.IsLoaded()) {
691 return false; // Can't render without loaded room
692 }
693
694 // Invalidate cache if room/palette/blockset changed
696 room.blockset() != cached_preview_blockset_ ||
697 room.palette() != cached_preview_palette_) {
700 cached_preview_blockset_ = room.blockset();
701 cached_preview_palette_ = room.palette();
702 }
703 } else {
704 return false;
705 }
706
707 // Check if already in cache
708 // Key: (object_id << 32) | (subtype << 16) | (blockset << 8) | palette
709 int subtype = object.size_ & 0x1F;
710 uint64_t cache_key = (static_cast<uint64_t>(object.id_) << 32) |
711 (static_cast<uint64_t>(subtype) << 16) |
712 (static_cast<uint64_t>(cached_preview_blockset_) << 8) |
713 static_cast<uint64_t>(cached_preview_palette_);
714
715 auto it = preview_cache_.find(cache_key);
716 if (it != preview_cache_.end()) {
717 *out = it->second.get();
718 return (*out)->bitmap().texture() != nullptr;
719 }
720
721 // Create new preview using ObjectTileEditor
722 auto& room = (*rooms_)[current_room_id_];
723 const uint8_t* gfx_data = room.get_gfx_buffer().data();
724
726 auto layout_or =
727 editor.CaptureObjectLayout(object.id_, room, current_palette_group_);
728 if (!layout_or.ok()) {
729 return false;
730 }
731 const auto& layout = layout_or.value();
732
733 // Create preview buffer large enough for object
734 int bmp_w = std::max(8, layout.bounds_width * 8);
735 int bmp_h = std::max(8, layout.bounds_height * 8);
736 auto preview = std::make_unique<gfx::BackgroundBuffer>(bmp_w, bmp_h);
737 preview->EnsureBitmapInitialized();
738
739 // Render layout to bitmap
740 auto render_status = editor.RenderLayoutToBitmap(
741 layout, preview->bitmap(), gfx_data, current_palette_group_);
742 if (!render_status.ok()) {
743 return false;
744 }
745
746 auto& bitmap = preview->bitmap();
747 // Texture creation and SDL sync
748 if (bitmap.surface()) {
749 // Sync to surface
750 SDL_LockSurface(bitmap.surface());
751 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
752 bitmap.mutable_data().size());
753 SDL_UnlockSurface(bitmap.surface());
754
755 // Create texture
759 }
760
761 if (!bitmap.texture()) {
762 return false;
763 }
764
765 // Store in cache and return
766 *out = preview.get();
767 preview_cache_[cache_key] = std::move(preview);
768 return true;
769}
770
772 ImVec2 top_left, float size) {
773 gfx::BackgroundBuffer* preview = nullptr;
774 if (!GetOrCreatePreview(object, size, &preview)) {
775 return false;
776 }
777
778 // Draw the cached preview image
779 auto& bitmap = preview->bitmap();
780 if (!bitmap.texture()) {
781 return false;
782 }
783
784 ImDrawList* draw_list = ImGui::GetWindowDrawList();
785 ImVec2 bottom_right(top_left.x + size, top_left.y + size);
786 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), top_left,
787 bottom_right);
788 return true;
789}
790
792 const auto& theme = AgentUI::GetTheme();
794 ImGui::OpenPopup("New Custom Object");
795 show_create_dialog_ = false;
796 }
797
798 if (ImGui::BeginPopupModal("New Custom Object", nullptr,
799 ImGuiWindowFlags_AlwaysAutoResize)) {
800 ImGui::Text(tr("Create a new custom dungeon object"));
801 ImGui::Separator();
802
803 // Dimensions
804 ImGui::SliderInt(tr("Width (tiles)"), &create_width_, 1, 32);
805 ImGui::SliderInt(tr("Height (tiles)"), &create_height_, 1, 32);
806
807 // Object group
808 const char* group_labels[] = {"0x31 - Track/Custom", "0x32 - Misc"};
809 int group_index = (create_object_id_ == 0x32) ? 1 : 0;
810 if (ImGui::Combo(tr("Object Group"), &group_index, group_labels,
811 IM_ARRAYSIZE(group_labels))) {
812 create_object_id_ = (group_index == 1) ? 0x32 : 0x31;
813 // Regenerate filename when group changes
814 snprintf(create_filename_, sizeof(create_filename_),
815 "custom_%02x_%02d.bin", create_object_id_,
816 zelda3::CustomObjectManager::Get().GetSubtypeCount(
818 }
819
820 // Filename
821 ImGui::InputText(tr("Filename"), create_filename_,
822 sizeof(create_filename_));
823
824 // Validation
825 bool valid = true;
826 std::string error_msg;
827
828 if (create_filename_[0] == '\0') {
829 valid = false;
830 error_msg = "Filename cannot be empty";
831 } else if (!rooms_ || current_room_id_ < 0) {
832 valid = false;
833 error_msg = "Load a room first (needed for tile graphics)";
834 } else {
836 if (mgr.GetBasePath().empty()) {
837 valid = false;
838 error_msg = "Custom objects folder not configured in project";
839 } else {
840 // Check if file already exists
841 auto path = std::filesystem::path(mgr.GetBasePath()) / create_filename_;
842 if (std::filesystem::exists(path)) {
843 valid = false;
844 error_msg = "File already exists: " + std::string(create_filename_);
845 }
846 }
847 }
848
849 if (!error_msg.empty()) {
850 ImGui::TextColored(theme.status_error, "%s", error_msg.c_str());
851 }
852
853 ImGui::Separator();
854
855 if (!valid)
856 ImGui::BeginDisabled();
857 if (ImGui::Button(tr("Create"), ImVec2(120, 0))) {
860 static_cast<int16_t>(create_object_id_), current_room_id_, rooms_);
861 ImGui::CloseCurrentPopup();
862 }
863 if (!valid)
864 ImGui::EndDisabled();
865
866 ImGui::SameLine();
867 if (ImGui::Button(tr("Cancel"), ImVec2(120, 0))) {
868 ImGui::CloseCurrentPopup();
869 }
870
871 ImGui::EndPopup();
872 }
873}
874
877 ImGui::OpenPopup("Custom Object Workshop");
879 }
880
881 if (ImGui::SmallButton(absl::StrFormat(ICON_MD_PRECISION_MANUFACTURING
882 " Workshop (%d)",
883 custom_count)
884 .c_str())) {
885 ImGui::OpenPopup("Custom Object Workshop");
886 }
887 if (ImGui::IsItemHovered()) {
888 ImGui::SetTooltip(tr(
889 "Browse and create custom dungeon objects without cluttering the main "
890 "selector."));
891 }
892}
893
895 const auto& theme = AgentUI::GetTheme();
896 auto& obj_manager = zelda3::CustomObjectManager::Get();
897 const std::string custom_base_path = obj_manager.GetBasePath();
898
900
901 ImGui::SetNextWindowSize(ImVec2(860.0f, 620.0f), ImGuiCond_FirstUseEver);
902 if (!ImGui::BeginPopupModal("Custom Object Workshop", nullptr,
903 ImGuiWindowFlags_None)) {
904 return;
905 }
906
907 ImGui::TextColored(theme.text_info,
908 ICON_MD_PRECISION_MANUFACTURING " Custom Object Workshop");
909 ImGui::TextColored(
910 theme.text_secondary_gray,
911 tr("Create and place custom dungeon objects without mixing "
912 "them into the main selector grid."));
913 ImGui::Separator();
914
915 if (ImGui::BeginTable(
916 "##CustomObjectToolbar", 2,
917 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
918 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f);
919 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch,
920 1.0f);
921 ImGui::TableNextRow();
922
923 ImGui::TableNextColumn();
924 if (custom_base_path.empty()) {
925 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING
926 " Custom object folder is not configured.");
927 } else {
928 ImGui::TextColored(theme.text_secondary_gray,
929 ICON_MD_FOLDER " Workshop folder");
930 if (ImGui::IsItemHovered()) {
931 ImGui::SetTooltip("%s", custom_base_path.c_str());
932 }
933 }
934
935 ImGui::TableNextColumn();
936 if (tile_editor_panel_) {
937 if (ImGui::Button(ICON_MD_ADD " New Custom Object", ImVec2(-1, 0))) {
938 show_create_dialog_ = true;
939 std::snprintf(create_filename_, sizeof(create_filename_),
940 "custom_%02x_%02d.bin", create_object_id_,
941 zelda3::CustomObjectManager::Get().GetSubtypeCount(
943 }
944 if (ImGui::IsItemHovered()) {
945 ImGui::SetTooltip(tr("Create a new custom object from scratch"));
946 }
947 }
948 if (ImGui::Button(ICON_MD_REFRESH " Reload Workshop", ImVec2(-1, 0))) {
949 obj_manager.ReloadAll();
951 }
952 if (ImGui::IsItemHovered()) {
953 ImGui::SetTooltip(
954 tr("Reload custom object binaries and refresh their previews"));
955 }
956 ImGui::EndTable();
957 }
958
959 ImGui::TextColored(
960 theme.text_secondary_gray, ICON_MD_INFO
961 " Corner overrides map to 0x31 subtypes 02, 04, 03, and 05.");
962 ImGui::Spacing();
963
964 if (ImGui::BeginChild("##CustomObjectGrid",
965 ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4.0f),
966 false,
967 ImGuiWindowFlags_AlwaysVerticalScrollbar |
968 ImGuiWindowFlags_HorizontalScrollbar)) {
969 const float item_spacing = 6.0f;
970 const int columns = std::max(
971 1, static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
972 (item_size + item_spacing)));
973 int custom_col = 0;
974 for (int obj_id : {0x31, 0x32}) {
976 continue;
977 }
978 int subtype_count = obj_manager.GetSubtypeCount(obj_id);
979 for (int subtype = 0; subtype < subtype_count; ++subtype) {
980 std::string base_name = zelda3::GetObjectName(obj_id);
981 std::string subtype_name =
982 absl::StrFormat("%s %02X", base_name.c_str(), subtype);
983 if (!MatchesObjectSearch(obj_id, subtype_name, subtype)) {
984 continue;
985 }
986
987 if (custom_col > 0) {
988 ImGui::SameLine();
989 }
990
991 ImGui::PushID(obj_id * 1000 + subtype);
992
993 bool is_selected = (selected_object_id_ == obj_id &&
994 (preview_object_.size_ & 0x1F) == subtype);
995 ImVec2 button_size(item_size, item_size);
996
997 if (ImGui::Selectable("", is_selected, 0, button_size)) {
998 SelectObject(obj_id, subtype);
999 ImGui::CloseCurrentPopup();
1000 }
1001 const bool item_visible = ImGui::IsItemVisible();
1002 ImVec2 button_pos = ImGui::GetItemRectMin();
1003 gui::BeginRoomObjectDragSource(static_cast<uint16_t>(obj_id),
1004 current_room_id_, 0, 0,
1005 static_cast<uint8_t>(subtype & 0x1F));
1006 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1007
1008 bool rendered = false;
1009 if (item_visible && enable_object_previews_) {
1010 auto temp_obj = MakePreviewObject(obj_id);
1011 temp_obj.size_ = subtype;
1012 rendered = DrawObjectPreview(temp_obj, button_pos, item_size);
1013 }
1014
1015 if (item_visible && !rendered) {
1016 std::string sub_text = absl::StrFormat("%02X", subtype);
1017 DrawFallbackPreviewTile(draw_list, button_pos, item_size,
1018 theme.status_success, sub_text.c_str());
1019 }
1020
1021 ImU32 border_color =
1022 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
1023 : ImGui::GetColorU32(theme.panel_bg_darker);
1024 float border_thickness = is_selected ? 3.0f : 1.0f;
1025 if (item_visible) {
1026 draw_list->AddRect(
1027 button_pos,
1028 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1029 border_color, 0.0f, 0, border_thickness);
1030
1031 std::string id_text = absl::StrFormat("%02X:%02X", obj_id, subtype);
1032 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1033 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1034 button_pos.y + item_size - id_size.y - 2);
1035 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1036 id_text.c_str());
1037 }
1038
1039 if (ImGui::IsItemHovered()) {
1040 gui::StyleColorGuard tooltip_guard(
1041 {{ImGuiCol_PopupBg, theme.panel_bg_color},
1042 {ImGuiCol_Border, theme.panel_border_color}});
1043 if (ImGui::BeginTooltip()) {
1044 const std::string filename =
1045 obj_manager.ResolveFilename(obj_id, subtype);
1046 const bool has_base = !custom_base_path.empty();
1047 std::filesystem::path full_path =
1048 has_base ? (std::filesystem::path(custom_base_path) / filename)
1049 : std::filesystem::path();
1050 const bool file_exists = has_base && !filename.empty() &&
1051 std::filesystem::exists(full_path);
1052
1053 ImGui::TextColored(theme.selection_primary,
1054 tr("Custom 0x%02X:%02X"), obj_id, subtype);
1055 ImGui::Text("%s", subtype_name.c_str());
1056 ImGui::TextColored(
1057 rendered ? theme.status_success : theme.status_warning,
1058 tr("Preview: %s"),
1059 rendered ? "rendered custom layout"
1060 : (enable_object_previews_ ? "fallback subtype"
1061 : "thumbnails off"));
1062 ImGui::Separator();
1063 ImGui::Text(tr("File: %s"),
1064 filename.empty() ? "(unmapped)" : filename.c_str());
1065 if (!has_base) {
1066 ImGui::TextColored(theme.text_warning_yellow,
1067 tr("Folder not configured in project"));
1068 } else if (file_exists) {
1069 ImGui::TextColored(theme.status_success, tr("File found"));
1070 } else {
1071 ImGui::TextColored(theme.status_error, tr("File missing: %s"),
1072 full_path.string().c_str());
1073 }
1074
1075 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
1076 const char* corner_id = "";
1077 if (subtype == 2) {
1078 corner_id = "0x100 (TL)";
1079 } else if (subtype == 3) {
1080 corner_id = "0x102 (TR)";
1081 } else if (subtype == 4) {
1082 corner_id = "0x101 (BL)";
1083 } else {
1084 corner_id = "0x103 (BR)";
1085 }
1086 ImGui::Separator();
1087 ImGui::TextColored(theme.status_active,
1088 tr("Also used by corner override %s"),
1089 corner_id);
1090 }
1091 ImGui::EndTooltip();
1092 }
1093 }
1094
1095 ImGui::PopID();
1096 custom_col = (custom_col + 1) % columns;
1097 }
1098 }
1099 }
1100 ImGui::EndChild();
1101
1102 ImGui::Separator();
1103 if (ImGui::Button(ICON_MD_CLOSE " Close")) {
1104 ImGui::CloseCurrentPopup();
1105 }
1106
1107 ImGui::EndPopup();
1108}
1109
1110} // namespace yaze::editor
auto data() const
Definition rom.h:151
bool is_loaded() const
Definition rom.h:144
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_
bool MatchesObjectSearch(int obj_id, const std::string &name, int subtype=-1) const
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, DungeonRoomStore *rooms)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
RAII guard for ImGui style colors.
Definition style_guard.h:27
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 SetRom(Rom *rom)
Definition room_object.h:79
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_PRECISION_MANUFACTURING
Definition icons.h:1509
const AgentUITheme & GetTheme()
void DrawFallbackPreviewTile(ImDrawList *draw_list, ImVec2 top_left, float item_size, const ImVec4 &accent_color, const char *label)
Editors are the view controllers for the application.
bool ThemedIconButton(const char *icon, const char *tooltip, const ImVec2 &size, bool is_active, bool is_disabled, const char *panel_id, const char *anim_id)
Draw a standard icon button with theme-aware colors.
bool ToggleButton(const char *label, bool active, const ImVec2 &size)
bool BeginRoomObjectDragSource(uint16_t object_id, int room_id, int pos_x, int pos_y, uint8_t size=0)
Definition drag_drop.h:99
int GetObjectSubtype(int object_id)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92