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