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"
23#include "rom/rom.h"
24#include "zelda3/dungeon/custom_object.h" // For CustomObjectManager
29#include "zelda3/dungeon/room.h"
30#include "zelda3/dungeon/room_object.h" // For GetObjectName()
31
32namespace yaze::editor {
33
35 const auto& theme = AgentUI::GetTheme();
36
37 // Type 3 objects (0xF80-0xFFF) - Special room features
38 if (object_id >= 0xF80) {
39 if (object_id >= 0xF80 && object_id <= 0xF8F) {
40 return ImGui::ColorConvertFloat4ToU32(
41 theme.selection_secondary); // Light blue for layer indicators
42 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
43 return ImGui::ColorConvertFloat4ToU32(
44 theme.transport_color); // Orange/Purple for door indicators
45 } else {
46 return ImGui::ColorConvertFloat4ToU32(
47 theme.music_zone_color); // Purple for misc Type 3
48 }
49 }
50
51 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
52 if (object_id >= 0x100 && object_id < 0x200) {
53 if (object_id >= 0x100 && object_id <= 0x10F) {
54 return IM_COL32(255, 150, 50, 255); // Orange for torches
55 } else if (object_id >= 0x110 && object_id <= 0x11F) {
56 return IM_COL32(150, 150, 200, 255); // Blue-gray for blocks
57 } else if (object_id >= 0x120 && object_id <= 0x12F) {
58 return ImGui::ColorConvertFloat4ToU32(
59 theme.status_success); // Green for switches
60 } else if (object_id >= 0x130 && object_id <= 0x13F) {
61 return ImGui::GetColorU32(theme.selection_primary); // Yellow for stairs
62 } else {
63 return IM_COL32(180, 180, 180, 255); // Gray for other Type 2
64 }
65 }
66
67 // Type 1 objects (0x00-0xFF) - Base room objects
68 if (object_id >= 0x10 && object_id <= 0x1F) {
69 return ImGui::GetColorU32(theme.dungeon_object_wall); // Gray for walls
70 } else if (object_id >= 0x20 && object_id <= 0x2F) {
71 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for floors
72 } else if (object_id == 0xF9 || object_id == 0xFA) {
73 return ImGui::GetColorU32(theme.item_color); // Gold for chests
74 } else if (object_id >= 0x17 && object_id <= 0x1E) {
75 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for doors
76 } else if (object_id == 0x2F || object_id == 0x2B) {
77 return ImGui::GetColorU32(
78 theme.dungeon_object_pot); // Saddle brown for pots
79 } else if (object_id >= 0x30 && object_id <= 0x3F) {
80 return ImGui::GetColorU32(
81 theme.dungeon_object_decoration); // Dim gray for decorations
82 } else if (object_id >= 0x00 && object_id <= 0x0F) {
83 return IM_COL32(120, 120, 180, 255); // Blue-gray for corners
84 } else {
85 return ImGui::GetColorU32(theme.dungeon_object_default); // Default gray
86 }
87}
88
90 // Type 3 objects (0xF80-0xFFF) - Special room features
91 if (object_id >= 0xF80) {
92 if (object_id >= 0xF80 && object_id <= 0xF8F) {
93 return "L"; // Layer
94 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
95 return "D"; // Door indicator
96 } else {
97 return "S"; // Special
98 }
99 }
100
101 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
102 if (object_id >= 0x100 && object_id < 0x200) {
103 if (object_id >= 0x100 && object_id <= 0x10F) {
104 return "*"; // Torch (flame)
105 } else if (object_id >= 0x110 && object_id <= 0x11F) {
106 return "#"; // Block
107 } else if (object_id >= 0x120 && object_id <= 0x12F) {
108 return "o"; // Switch
109 } else if (object_id >= 0x130 && object_id <= 0x13F) {
110 return "^"; // Stairs
111 } else {
112 return "2"; // Type 2
113 }
114 }
115
116 // Type 1 objects (0x00-0xFF) - Base room objects
117 if (object_id >= 0x10 && object_id <= 0x1F) {
118 return "|"; // Wall
119 } else if (object_id >= 0x20 && object_id <= 0x2F) {
120 return "_"; // Floor
121 } else if (object_id == 0xF9 || object_id == 0xFA) {
122 return "C"; // Chest
123 } else if (object_id >= 0x17 && object_id <= 0x1E) {
124 return "+"; // Door
125 } else if (object_id == 0x2F || object_id == 0x2B) {
126 return "o"; // Pot
127 } else if (object_id >= 0x30 && object_id <= 0x3F) {
128 return "~"; // Decoration
129 } else if (object_id >= 0x00 && object_id <= 0x0F) {
130 return "/"; // Corner
131 } else {
132 return "?"; // Unknown
133 }
134}
135
136void DungeonObjectSelector::SelectObject(int obj_id, int subtype) {
137 selected_object_id_ = obj_id;
138
139 // Create and update preview object
140 uint8_t size = 0x12;
141 if (subtype >= 0) {
142 size = static_cast<uint8_t>(subtype & 0x1F);
143 }
144 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, size, 0);
146 if (game_data_) {
147 auto palette =
149 preview_palette_ = palette;
150 }
151 object_loaded_ = true;
152
153 // Notify callback
156 }
157}
158
160 const auto& theme = AgentUI::GetTheme();
161
162 // Object ranges: Type 1 (0x00-0xFF), Type 2 (0x100-0x141), Type 3 (0xF80-0xFFF)
163 struct ObjectRange {
164 int start;
165 int end;
166 const char* label;
167 ImU32 header_color;
168 };
169 static const ObjectRange ranges[] = {
170 {0x00, 0xFF, "Type 1", IM_COL32(80, 120, 180, 255)},
171 {0x100, 0x141, "Type 2", IM_COL32(120, 80, 180, 255)},
172 {0xF80, 0xFFF, "Type 3", IM_COL32(180, 120, 80, 255)},
173 };
174
175 // Total object count
176 int total_objects =
177 (0xFF - 0x00 + 1) + (0x141 - 0x100 + 1) + (0xFFF - 0xF80 + 1);
178
179 ImGui::TextDisabled("%d objects", total_objects);
180 ImGui::SameLine();
181 ImGui::Checkbox(ICON_MD_IMAGE " Tile thumbnails", &enable_object_previews_);
182 if (ImGui::IsItemHovered()) {
183 ImGui::SetTooltip(
184 "Show rendered object thumbnails in the selector.\n"
185 "Requires a room to be loaded and may cost some performance.");
186 }
187
188 if (selected_object_id_ >= 0) {
189 ImGui::TextColored(theme.text_info, ICON_MD_LABEL " Current: 0x%03X %s",
192 } else {
193 ImGui::TextColored(theme.text_secondary_gray,
194 "Tip: click once to queue placement, double-click to "
195 "inspect the draw routine.");
196 }
197
198 // Search + category filter
199 ImGui::SetNextItemWidth(-1.0f);
200 ImGui::InputTextWithHint(
201 "##ObjectSearch", ICON_MD_SEARCH " Filter by name or hex...",
203 static const char* kFilterLabels[] = {"All", "Walls", "Floors", "Chests",
204 "Doors", "Decor", "Stairs"};
205 ImGui::SetNextItemWidth(170.0f);
206 ImGui::Combo("##ObjectFilterType", &object_type_filter_, kFilterLabels,
207 IM_ARRAYSIZE(kFilterLabels));
208 ImGui::SameLine();
209 if (gui::ThemedButton(ICON_MD_CLEAR " Reset")) {
210 object_search_buffer_[0] = '\0';
212 }
213 if (ImGui::IsItemHovered()) {
214 gui::ThemedTooltip("Clear search and category filter");
215 }
216
217 // Create asset browser-style grid
218 const float item_size = 72.0f;
219 const float item_spacing = 6.0f;
220 const int columns = std::max(
221 1, static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
222 (item_size + item_spacing)));
223
224 // Scrollable child region for grid - use all available space
225 float child_height = ImGui::GetContentRegionAvail().y;
226 if (ImGui::BeginChild("##ObjectGrid", ImVec2(0, child_height), false,
227 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
228
229 // Iterate through all object ranges
230 for (const auto& range : ranges) {
231 // Section header for each type
232 gui::StyleColorGuard section_guard(
233 {{ImGuiCol_Header,
234 ImGui::ColorConvertU32ToFloat4(range.header_color)},
235 {ImGuiCol_HeaderHovered,
236 ImGui::ColorConvertU32ToFloat4(
237 IM_COL32((range.header_color & 0xFF) + 30,
238 ((range.header_color >> 8) & 0xFF) + 30,
239 ((range.header_color >> 16) & 0xFF) + 30, 255))}});
240 bool section_open = ImGui::CollapsingHeader(
241 absl::StrFormat("%s (0x%03X-0x%03X)", range.label, range.start,
242 range.end)
243 .c_str(),
244 ImGuiTreeNodeFlags_DefaultOpen);
245
246 if (!section_open)
247 continue;
248
249 int current_column = 0;
250
251 for (int obj_id = range.start; obj_id <= range.end; ++obj_id) {
253 continue;
254 }
255
256 std::string full_name = zelda3::GetObjectName(obj_id);
257 if (!MatchesObjectSearch(obj_id, full_name)) {
258 continue;
259 }
260
261 if (current_column > 0) {
262 ImGui::SameLine();
263 }
264
265 ImGui::PushID(obj_id);
266
267 // Create selectable button for object
268 bool is_selected = (selected_object_id_ == obj_id);
269 ImVec2 button_size(item_size, item_size);
270
271 if (ImGui::Selectable("", is_selected,
272 ImGuiSelectableFlags_AllowDoubleClick,
273 button_size)) {
274 selected_object_id_ = obj_id;
275
276 // Create and update preview object
277 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, 0x12, 0);
279 if (game_data_ &&
282 auto palette = game_data_->palette_groups
284 preview_palette_ = palette;
285 }
286 object_loaded_ = true;
287
288 // Notify callbacks
291 }
292
293 // Handle double-click to open static object editor
294 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
297 }
298 }
299 }
300
301 // Draw object preview on the button; fall back to styled placeholder
302 ImVec2 button_pos = ImGui::GetItemRectMin();
303 ImDrawList* draw_list = ImGui::GetWindowDrawList();
304
305 // Only attempt graphical preview if enabled (performance optimization)
306 bool rendered = false;
308 rendered = DrawObjectPreview(MakePreviewObject(obj_id), button_pos,
309 item_size);
310 }
311
312 if (!rendered) {
313 // Draw a styled fallback with gradient background
314 ImU32 obj_color = GetObjectTypeColor(obj_id);
315 ImU32 darker_color = IM_COL32((obj_color & 0xFF) * 0.6f,
316 ((obj_color >> 8) & 0xFF) * 0.6f,
317 ((obj_color >> 16) & 0xFF) * 0.6f, 255);
318
319 // Gradient background
320 draw_list->AddRectFilledMultiColor(
321 button_pos,
322 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
323 darker_color, darker_color, obj_color, obj_color);
324
325 // Draw object type symbol in center
326 std::string symbol = GetObjectTypeSymbol(obj_id);
327 ImVec2 symbol_size = ImGui::CalcTextSize(symbol.c_str());
328 ImVec2 symbol_pos(
329 button_pos.x + (item_size - symbol_size.x) / 2,
330 button_pos.y + (item_size - symbol_size.y) / 2 - 10);
331 draw_list->AddText(symbol_pos, IM_COL32(255, 255, 255, 180),
332 symbol.c_str());
333 }
334
335 // Draw border with special highlight for static editor object
336 bool is_static_editor_obj = (obj_id == static_editor_object_id_);
337 ImU32 border_color;
338 float border_thickness;
339
340 if (is_static_editor_obj) {
341 border_color = IM_COL32(0, 200, 255, 255);
342 border_thickness = 3.0f;
343 } else if (is_selected) {
344 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
345 border_thickness = 3.0f;
346 } else {
347 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
348 border_thickness = 1.0f;
349 }
350
351 draw_list->AddRect(
352 button_pos,
353 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
354 border_color, 0.0f, 0, border_thickness);
355
356 // Static editor indicator icon
357 if (is_static_editor_obj) {
358 ImVec2 icon_pos(button_pos.x + item_size - 14, button_pos.y + 2);
359 draw_list->AddCircleFilled(ImVec2(icon_pos.x + 6, icon_pos.y + 6), 6,
360 IM_COL32(0, 200, 255, 200));
361 draw_list->AddText(icon_pos, IM_COL32(255, 255, 255, 255), "i");
362 }
363
364 // Get object name for display
365 // Truncate name for display
366 std::string display_name = full_name;
367 const size_t kMaxDisplayChars = 12;
368 if (display_name.length() > kMaxDisplayChars) {
369 display_name = display_name.substr(0, kMaxDisplayChars - 2) + "..";
370 }
371
372 // Draw object name (smaller, above ID)
373 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
374 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
375 button_pos.y + item_size - 26);
376 draw_list->AddText(name_pos,
377 ImGui::GetColorU32(theme.text_secondary_gray),
378 display_name.c_str());
379
380 // Draw object ID at bottom (hex format)
381 std::string id_text = absl::StrFormat("%03X", obj_id);
382 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
383 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
384 button_pos.y + item_size - id_size.y - 2);
385 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
386 id_text.c_str());
387
388 // Enhanced tooltip
389 if (ImGui::IsItemHovered()) {
390 gui::StyleColorGuard tooltip_guard(
391 {{ImGuiCol_PopupBg, theme.panel_bg_color},
392 {ImGuiCol_Border, theme.panel_border_color}});
393
394 if (ImGui::BeginTooltip()) {
395 ImGui::TextColored(theme.selection_primary, "Object 0x%03X",
396 obj_id);
397 ImGui::Text("%s", full_name.c_str());
398 int subtype = zelda3::GetObjectSubtype(obj_id);
399 ImGui::TextColored(theme.text_secondary_gray, "Subtype %d",
400 subtype);
401 ImGui::Separator();
402
403 uint32_t layout_key = (static_cast<uint32_t>(obj_id) << 16) |
404 static_cast<uint32_t>(subtype);
405 const bool can_capture_layout =
406 rom_ && rooms_ && current_room_id_ >= 0 &&
408 if (can_capture_layout &&
409 layout_cache_.find(layout_key) == layout_cache_.end()) {
411 auto& room_ref = (*rooms_)[current_room_id_];
412 auto layout_or = editor.CaptureObjectLayout(
413 obj_id, room_ref, current_palette_group_);
414 if (layout_or.ok()) {
415 layout_cache_[layout_key] = layout_or.value();
416 }
417 }
418
419 if (layout_cache_.count(layout_key)) {
420 const auto& layout = layout_cache_[layout_key];
421 ImGui::TextColored(theme.status_success, "Tiles: %zu",
422 layout.cells.size());
423
424 if (can_capture_layout) {
425 auto& room_ref = (*rooms_)[current_room_id_];
427 room_ref.get_gfx_buffer().data());
428 int rid = drawer.GetDrawRoutineId(obj_id);
429 ImGui::TextColored(theme.status_active, "Draw Routine: %d",
430 rid);
431 }
432
433 ImGui::Text("Layout:");
434 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
435 ImVec2 grid_start = ImGui::GetCursorScreenPos();
436 float cell_size = 4.0f;
437 for (const auto& cell : layout.cells) {
438 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
439 grid_start.y + cell.rel_y * cell_size);
440 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
441 tooltip_draw_list->AddRectFilled(p1, p2,
442 IM_COL32(200, 200, 200, 255));
443 tooltip_draw_list->AddRect(p1, p2, IM_COL32(50, 50, 50, 255));
444 }
445 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
446 layout.bounds_height * cell_size));
447 }
448
449 ImGui::Separator();
450 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
451 "Click to select for placement");
452 ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f),
453 "Double-click to view details");
454 ImGui::EndTooltip();
455 }
456 }
457
458 ImGui::PopID();
459
460 current_column = (current_column + 1) % columns;
461 } // end object loop
462 } // end range loop
463
465
466 ImGui::Spacing();
467 ImGui::Separator();
468 ImGui::Spacing();
469
470 // Custom Objects Section
471 gui::StyleColorGuard custom_hdr_guard(
472 {{ImGuiCol_Header,
473 ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 120, 255))},
474 {ImGuiCol_HeaderHovered,
475 ImGui::ColorConvertU32ToFloat4(IM_COL32(130, 210, 150, 255))}});
476 bool custom_open = ImGui::CollapsingHeader("Custom Objects",
477 ImGuiTreeNodeFlags_DefaultOpen);
478
479 if (custom_open) {
480 ImGui::TextColored(theme.text_secondary_gray,
481 "Create, reload, and browse custom object variants "
482 "separately from the vanilla selector.");
483 // "+ New Custom Object" button
484 if (tile_editor_panel_) {
485 if (ImGui::SmallButton(ICON_MD_ADD " New Custom Object")) {
486 show_create_dialog_ = true;
487 // Auto-generate a default filename
488 snprintf(create_filename_, sizeof(create_filename_),
489 "custom_%02x_%02d.bin", create_object_id_,
490 zelda3::CustomObjectManager::Get().GetSubtypeCount(
492 }
493 if (ImGui::IsItemHovered()) {
494 ImGui::SetTooltip("Create a new custom object from scratch");
495 }
496 }
497
498 auto& obj_manager = zelda3::CustomObjectManager::Get();
499 const std::string custom_base_path = obj_manager.GetBasePath();
500 if (custom_base_path.empty()) {
501 ImGui::TextColored(theme.text_secondary_gray,
502 "Custom objects folder: not configured");
503 } else {
504 ImGui::Text("Custom objects folder: %s", custom_base_path.c_str());
505 if (ImGui::IsItemHovered()) {
506 ImGui::SetTooltip("%s", custom_base_path.c_str());
507 }
508 ImGui::SameLine();
509 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload")) {
510 obj_manager.ReloadAll();
512 }
513 if (ImGui::IsItemHovered()) {
514 ImGui::SetTooltip(
515 "Reload custom object binaries and refresh previews");
516 }
517 }
518 ImGui::TextColored(theme.text_secondary_gray,
519 "Corner overrides: 0x100/0x101/0x102/0x103 use 0x31 "
520 "subtypes 02/04/03/05");
521
523
524 int custom_col = 0;
525
526 // Initialize if needed (hacky lazy init if drawer hasn't done it yet)
527 // Ideally should be initialized by system.
528 // We'll skip init here and assume ObjectDrawer did it or will do it.
529 // But we need counts. If uninitialized, counts might be wrong?
530 // GetSubtypeCount checks static lists, so it's safe even if not fully init with paths.
531
532 for (int obj_id : {0x31, 0x32}) {
534 continue;
535 }
536 int subtype_count = obj_manager.GetSubtypeCount(obj_id);
537 for (int subtype = 0; subtype < subtype_count; ++subtype) {
538 std::string base_name = zelda3::GetObjectName(obj_id);
539 std::string subtype_name =
540 absl::StrFormat("%s %02X", base_name.c_str(), subtype);
541 if (!MatchesObjectSearch(obj_id, subtype_name, subtype)) {
542 continue;
543 }
544
545 if (custom_col > 0)
546 ImGui::SameLine();
547
548 ImGui::PushID(obj_id * 1000 + subtype);
549
550 bool is_selected = (selected_object_id_ == obj_id &&
551 (preview_object_.size_ & 0x1F) == subtype);
552 ImVec2 button_size(item_size, item_size);
553
554 if (ImGui::Selectable("", is_selected,
555 ImGuiSelectableFlags_AllowDoubleClick,
556 button_size)) {
557 SelectObject(obj_id, subtype);
558
559 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
562 }
563 }
564
565 // Draw Preview
566 ImVec2 button_pos = ImGui::GetItemRectMin();
567 ImDrawList* draw_list = ImGui::GetWindowDrawList();
568
569 bool rendered = false;
570 // Native preview requires loaded ROM and correct pathing, might fail if not init.
571 // But we can try constructing a temp object with correct subtype.
573 auto temp_obj = MakePreviewObject(obj_id);
574 temp_obj.size_ = subtype;
575 rendered = DrawObjectPreview(temp_obj, button_pos, item_size);
576 }
577
578 if (!rendered) {
579 // Fallback visuals
580 ImU32 obj_color = IM_COL32(100, 180, 120, 255);
581 ImU32 darker_color = IM_COL32(60, 100, 70, 255);
582
583 draw_list->AddRectFilledMultiColor(
584 button_pos,
585 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
586 darker_color, darker_color, obj_color, obj_color);
587
588 std::string symbol = (obj_id == 0x31) ? "Trk" : "Cus";
589 // Subtype
590 std::string sub_text = absl::StrFormat("%02X", subtype);
591 ImVec2 sub_size = ImGui::CalcTextSize(sub_text.c_str());
592 ImVec2 sub_pos(button_pos.x + (item_size - sub_size.x) / 2,
593 button_pos.y + (item_size - sub_size.y) / 2);
594 draw_list->AddText(sub_pos, IM_COL32(255, 255, 255, 220),
595 sub_text.c_str());
596 }
597
598 // Border
599 bool is_static_editor_obj = (obj_id == static_editor_object_id_ &&
601 // Static editor doesn't track subtype currently, so highlighting all subtypes of 0x31 is correct
602 // if we are editing 0x31 generic. But maybe we only edit specific subtype?
603 // Static editor usually edits the code/logic common to ID.
604 ImU32 border_color =
605 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
606 : ImGui::GetColorU32(theme.panel_bg_darker);
607 float border_thickness = is_selected ? 3.0f : 1.0f;
608 draw_list->AddRect(
609 button_pos,
610 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
611 border_color, 0.0f, 0, border_thickness);
612
613 // Name/ID
614 std::string id_text = absl::StrFormat("%02X:%02X", obj_id, subtype);
615 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
616 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
617 button_pos.y + item_size - id_size.y - 2);
618 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
619 id_text.c_str());
620
621 if (ImGui::IsItemHovered()) {
622 gui::StyleColorGuard tooltip_guard(
623 {{ImGuiCol_PopupBg, theme.panel_bg_color},
624 {ImGuiCol_Border, theme.panel_border_color}});
625 if (ImGui::BeginTooltip()) {
626 const std::string filename =
627 obj_manager.ResolveFilename(obj_id, subtype);
628 const bool has_base = !custom_base_path.empty();
629 std::filesystem::path full_path =
630 has_base
631 ? (std::filesystem::path(custom_base_path) / filename)
632 : std::filesystem::path();
633 const bool file_exists = has_base && !filename.empty() &&
634 std::filesystem::exists(full_path);
635
636 ImGui::TextColored(theme.selection_primary, "Custom 0x%02X:%02X",
637 obj_id, subtype);
638 ImGui::Text("%s", subtype_name.c_str());
639 ImGui::Separator();
640 ImGui::Text("File: %s",
641 filename.empty() ? "(unmapped)" : filename.c_str());
642 if (!has_base) {
643 ImGui::TextColored(theme.text_warning_yellow,
644 "Folder not configured in project");
645 } else if (file_exists) {
646 ImGui::TextColored(theme.status_success, "File found");
647 } else {
648 ImGui::TextColored(theme.status_error, "File missing: %s",
649 full_path.string().c_str());
650 }
651
652 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
653 const char* corner_id = "";
654 if (subtype == 2) {
655 corner_id = "0x100 (TL)";
656 } else if (subtype == 3) {
657 corner_id = "0x102 (TR)";
658 } else if (subtype == 4) {
659 corner_id = "0x101 (BL)";
660 } else {
661 corner_id = "0x103 (BR)";
662 }
663 ImGui::Separator();
664 ImGui::TextColored(theme.status_active,
665 "Also used by corner override %s",
666 corner_id);
667 }
668 ImGui::EndTooltip();
669 }
670 }
671
672 ImGui::PopID();
673 custom_col = (custom_col + 1) % columns;
674 }
675 }
676 }
677 }
678
679 ImGui::EndChild();
680}
681
682bool DungeonObjectSelector::MatchesObjectFilter(int obj_id, int filter_type) {
683 switch (filter_type) {
684 case 1: // Walls
685 return obj_id >= 0x10 && obj_id <= 0x1F;
686 case 2: // Floors
687 return obj_id >= 0x20 && obj_id <= 0x2F;
688 case 3: // Chests
689 return obj_id == 0xF9 || obj_id == 0xFA;
690 case 4: // Doors
691 return obj_id >= 0x17 && obj_id <= 0x1E;
692 case 5: // Decorations
693 return obj_id >= 0x30 && obj_id <= 0x3F;
694 case 6: // Stairs
695 return obj_id >= 0x138 && obj_id <= 0x13B;
696 default: // All
697 return true;
698 }
699}
700
702 const std::string& name,
703 int subtype) const {
704 if (object_search_buffer_[0] == '\0') {
705 return true;
706 }
707
708 auto to_lower = [](std::string value) {
709 std::transform(value.begin(), value.end(), value.begin(),
710 [](unsigned char c) { return std::tolower(c); });
711 return value;
712 };
713
714 std::string needle = to_lower(object_search_buffer_);
715 std::string name_lower = to_lower(name);
716
717 std::string id_hex = absl::StrFormat("%03X", obj_id);
718 std::string id_lower = to_lower(id_hex);
719 std::string id_pref = "0x" + id_lower;
720
721 if (name_lower.find(needle) != std::string::npos) {
722 return true;
723 }
724 if (id_lower.find(needle) != std::string::npos ||
725 id_pref.find(needle) != std::string::npos) {
726 return true;
727 }
728
729 if (subtype >= 0) {
730 std::string sub_hex = absl::StrFormat("%02X", subtype);
731 std::string sub_lower = to_lower(sub_hex);
732 std::string combined = id_lower + ":" + sub_lower;
733 std::string combined_pref = "0x" + combined;
734 if (combined.find(needle) != std::string::npos ||
735 combined_pref.find(needle) != std::string::npos) {
736 return true;
737 }
738 }
739
740 return false;
741}
742
744 const zelda3::RoomObject& object, int& width, int& height) {
746 width = std::min(w, 256);
747 height = std::min(h, 256);
748}
749
756
765
775
777 zelda3::RoomObject obj(obj_id, 0, 0, 0x12, 0);
778 obj.SetRom(rom_);
779 obj.EnsureTilesLoaded();
780 return obj;
781}
782
787
789 float size,
790 gfx::BackgroundBuffer** out) {
791 if (!rom_ || !rom_->is_loaded()) {
792 return false;
793 }
795
796 // Check if room context changed - invalidate cache if so
797 if (rooms_ && current_room_id_ < static_cast<int>(rooms_->size())) {
798 const auto& room = (*rooms_)[current_room_id_];
799 if (!room.IsLoaded()) {
800 return false; // Can't render without loaded room
801 }
802
803 // Invalidate cache if room/palette/blockset changed
805 room.blockset() != cached_preview_blockset_ ||
806 room.palette() != cached_preview_palette_) {
809 cached_preview_blockset_ = room.blockset();
810 cached_preview_palette_ = room.palette();
811 }
812 } else {
813 return false;
814 }
815
816 // Check if already in cache
817 // Key: (object_id << 32) | (subtype << 16) | (blockset << 8) | palette
818 int subtype = object.size_ & 0x1F;
819 uint64_t cache_key = (static_cast<uint64_t>(object.id_) << 32) |
820 (static_cast<uint64_t>(subtype) << 16) |
821 (static_cast<uint64_t>(cached_preview_blockset_) << 8) |
822 static_cast<uint64_t>(cached_preview_palette_);
823
824 auto it = preview_cache_.find(cache_key);
825 if (it != preview_cache_.end()) {
826 *out = it->second.get();
827 return (*out)->bitmap().texture() != nullptr;
828 }
829
830 // Create new preview using ObjectTileEditor
831 auto& room = (*rooms_)[current_room_id_];
832 const uint8_t* gfx_data = room.get_gfx_buffer().data();
833
835 auto layout_or =
836 editor.CaptureObjectLayout(object.id_, room, current_palette_group_);
837 if (!layout_or.ok()) {
838 return false;
839 }
840 const auto& layout = layout_or.value();
841
842 // Create preview buffer large enough for object
843 int bmp_w = std::max(8, layout.bounds_width * 8);
844 int bmp_h = std::max(8, layout.bounds_height * 8);
845 auto preview = std::make_unique<gfx::BackgroundBuffer>(bmp_w, bmp_h);
846 preview->EnsureBitmapInitialized();
847
848 // Render layout to bitmap
849 auto render_status = editor.RenderLayoutToBitmap(
850 layout, preview->bitmap(), gfx_data, current_palette_group_);
851 if (!render_status.ok()) {
852 return false;
853 }
854
855 auto& bitmap = preview->bitmap();
856 // Texture creation and SDL sync
857 if (bitmap.surface()) {
858 // Sync to surface
859 SDL_LockSurface(bitmap.surface());
860 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
861 bitmap.mutable_data().size());
862 SDL_UnlockSurface(bitmap.surface());
863
864 // Create texture
868 }
869
870 if (!bitmap.texture()) {
871 return false;
872 }
873
874 // Store in cache and return
875 *out = preview.get();
876 preview_cache_[cache_key] = std::move(preview);
877 return true;
878}
879
881 ImVec2 top_left, float size) {
882 gfx::BackgroundBuffer* preview = nullptr;
883 if (!GetOrCreatePreview(object, size, &preview)) {
884 return false;
885 }
886
887 // Draw the cached preview image
888 auto& bitmap = preview->bitmap();
889 if (!bitmap.texture()) {
890 return false;
891 }
892
893 ImDrawList* draw_list = ImGui::GetWindowDrawList();
894 ImVec2 bottom_right(top_left.x + size, top_left.y + size);
895 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), top_left,
896 bottom_right);
897 return true;
898}
899
902 ImGui::OpenPopup("New Custom Object");
903 show_create_dialog_ = false;
904 }
905
906 if (ImGui::BeginPopupModal("New Custom Object", nullptr,
907 ImGuiWindowFlags_AlwaysAutoResize)) {
908 ImGui::Text("Create a new custom dungeon object");
909 ImGui::Separator();
910
911 // Dimensions
912 ImGui::SliderInt("Width (tiles)", &create_width_, 1, 32);
913 ImGui::SliderInt("Height (tiles)", &create_height_, 1, 32);
914
915 // Object group
916 const char* group_labels[] = {"0x31 - Track/Custom", "0x32 - Misc"};
917 int group_index = (create_object_id_ == 0x32) ? 1 : 0;
918 if (ImGui::Combo("Object Group", &group_index, group_labels,
919 IM_ARRAYSIZE(group_labels))) {
920 create_object_id_ = (group_index == 1) ? 0x32 : 0x31;
921 // Regenerate filename when group changes
922 snprintf(create_filename_, sizeof(create_filename_),
923 "custom_%02x_%02d.bin", create_object_id_,
924 zelda3::CustomObjectManager::Get().GetSubtypeCount(
926 }
927
928 // Filename
929 ImGui::InputText("Filename", create_filename_, sizeof(create_filename_));
930
931 // Validation
932 bool valid = true;
933 std::string error_msg;
934
935 if (create_filename_[0] == '\0') {
936 valid = false;
937 error_msg = "Filename cannot be empty";
938 } else if (!rooms_ || current_room_id_ < 0) {
939 valid = false;
940 error_msg = "Load a room first (needed for tile graphics)";
941 } else {
943 if (mgr.GetBasePath().empty()) {
944 valid = false;
945 error_msg = "Custom objects folder not configured in project";
946 } else {
947 // Check if file already exists
948 auto path = std::filesystem::path(mgr.GetBasePath()) / create_filename_;
949 if (std::filesystem::exists(path)) {
950 valid = false;
951 error_msg = "File already exists: " + std::string(create_filename_);
952 }
953 }
954 }
955
956 if (!error_msg.empty()) {
957 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
958 error_msg.c_str());
959 }
960
961 ImGui::Separator();
962
963 if (!valid)
964 ImGui::BeginDisabled();
965 if (ImGui::Button("Create", ImVec2(120, 0))) {
968 static_cast<int16_t>(create_object_id_), current_room_id_, rooms_);
969 ImGui::CloseCurrentPopup();
970 }
971 if (!valid)
972 ImGui::EndDisabled();
973
974 ImGui::SameLine();
975 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
976 ImGui::CloseCurrentPopup();
977 }
978
979 ImGui::EndPopup();
980 }
981}
982
983} // namespace yaze::editor
auto data() const
Definition rom.h:139
bool is_loaded() const
Definition rom.h:132
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_
std::string GetObjectTypeSymbol(int object_id)
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_SEARCH
Definition icons.h:1673
#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
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 ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
int GetObjectSubtype(int object_id)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91