yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_selector.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5
6#include "absl/strings/str_format.h"
10#include "app/gui/core/input.h"
13#include "imgui/imgui.h"
14#include "util/hex.h"
16#include "zelda3/dungeon/room.h"
20
21namespace yaze::editor {
22
23using ImGui::BeginChild;
24using ImGui::EndChild;
25using ImGui::SameLine;
26
28 // Legacy combined view - prefer using DrawRoomSelector() and
29 // DrawEntranceSelector() separately via their own EditorPanels
31}
32
34 RoomSelectionIntent single_click_intent) {
35 DrawRoomSelectorInternal(single_click_intent, true);
36}
37
39 RoomSelectionIntent single_click_intent) {
40 DrawRoomSelectorInternal(single_click_intent, false);
41}
42
46
47 for (int i = 0; i < zelda3::kNumberOfRooms; ++i) {
48 std::string display_name = zelda3::GetRoomLabel(i);
49 if (room_filter_.PassFilter(display_name.c_str()) &&
51 filtered_room_indices_.push_back(i);
52 }
53 }
54}
55
58 return true;
59 if (!rooms_ || room_id < 0 || room_id >= static_cast<int>(rooms_->size())) {
60 return true;
61 }
62 const auto* room = rooms_->GetIfLoaded(room_id);
63 if (room == nullptr) {
64 return false;
65 }
66 switch (entity_type_filter_) {
68 return !room->GetSprites().empty();
69 case kFilterHasItems:
70 return !room->GetPotItems().empty();
72 return !room->GetTileObjects().empty();
73 default:
74 return true;
75 }
76}
77
79 RoomSelectionIntent single_click_intent, bool show_room_id_input) {
80 if (!rom_ || !rom_->is_loaded()) {
81 ImGui::Text("ROM not loaded");
82 return;
83 }
84
85 if (show_room_id_input) {
86 if (gui::InputHexWord("Room ID", &current_room_id_, 50.f, true)) {
88 current_room_id_ = static_cast<uint16_t>(zelda3::kNumberOfRooms - 1);
89 }
90
91 // Publish selection changed event
92 if (auto* bus = ContentRegistry::Context::event_bus()) {
93 bus->Publish(SelectionChangedEvent::CreateSingle("dungeon_room",
95 }
96
97 // Callback support
100 }
101 }
102 ImGui::Separator();
103 }
104
105 room_filter_.Draw("Filter", ImGui::GetContentRegionAvail().x);
106
107 // View mode + entity-type filter row
108 {
109 // View mode toggle
110 if (ImGui::SmallButton(view_mode_ == kViewList ? ICON_MD_LIST " List"
112 " Grouped")) {
114 }
115 if (ImGui::IsItemHovered()) {
116 ImGui::SetTooltip("Toggle between flat list and dungeon-grouped view");
117 }
118 ImGui::SameLine(0, 12);
119
120 // Entity-type filter chips (compact, touch-friendly)
121 const char* labels[] = {"All", "Sprites", "Items", "Objects"};
124 for (int idx = 0; idx < 4; ++idx) {
125 if (idx > 0)
126 ImGui::SameLine();
127 bool selected = (entity_type_filter_ == values[idx]);
128 if (selected) {
129 ImGui::PushStyleColor(ImGuiCol_Button,
130 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
131 }
132 if (ImGui::SmallButton(labels[idx])) {
133 entity_type_filter_ = values[idx];
134 }
135 if (selected) {
136 ImGui::PopStyleColor();
137 }
138 }
139 }
140
141 // Rebuild every frame so renamed room labels appear immediately.
142 std::string current_filter(room_filter_.InputBuf);
143 if (current_filter != last_room_filter_) {
144 last_room_filter_ = current_filter;
145 }
147
148 // Increase row height on touch devices for easier tapping
149 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
150 std::optional<gui::StyleVarGuard> touch_pad_guard;
151 if (is_touch) {
152 float touch_pad = std::max(
153 6.0f, (gui::LayoutHelpers::GetMinTouchTarget() - ImGui::GetFontSize()) *
154 0.5f);
155 touch_pad_guard.emplace(ImGuiStyleVar_CellPadding,
156 ImVec2(ImGui::GetStyle().CellPadding.x, touch_pad));
157 }
158
159 // Dispatch to appropriate view mode
160 if (view_mode_ == kViewGrouped) {
161 DrawGroupedRoomList(single_click_intent);
162 return;
163 }
164
165 // === Flat list view (original) ===
166 if (ImGui::BeginTable("RoomList", 2,
167 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders |
168 ImGuiTableFlags_RowBg |
169 ImGuiTableFlags_Resizable)) {
170 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f);
171 ImGui::TableSetupColumn("Name");
172 ImGui::TableHeadersRow();
173
174 // Use ImGuiListClipper for virtualized rendering
175 ImGuiListClipper clipper;
176 clipper.Begin(static_cast<int>(filtered_room_indices_.size()));
177
178 while (clipper.Step()) {
179 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
180 int room_id = filtered_room_indices_[row];
181 std::string display_name = zelda3::GetRoomLabel(room_id);
182
183 ImGui::TableNextRow();
184 ImGui::TableNextColumn();
185
186 char label[32];
187 snprintf(label, sizeof(label), "%03X", room_id);
188 if (ImGui::Selectable(label, current_room_id_ == room_id,
189 ImGuiSelectableFlags_SpanAllColumns |
190 ImGuiSelectableFlags_AllowDoubleClick)) {
191 current_room_id_ = room_id;
192
193 // Publish selection changed event
194 if (auto* bus = ContentRegistry::Context::event_bus()) {
195 bus->Publish(
196 SelectionChangedEvent::CreateSingle("dungeon_room", room_id));
197 }
198
199 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
201 room_intent_callback_(room_id,
203 } else if (room_selected_callback_) {
205 }
206 } else {
208 room_intent_callback_(room_id, single_click_intent);
209 } else if (room_selected_callback_) {
211 }
212 }
213 }
214
215 // Context menu
216 if (ImGui::BeginPopupContextItem()) {
217 if (ImGui::MenuItem("Open in Workbench")) {
218 current_room_id_ = room_id;
220 room_intent_callback_(room_id,
222 } else if (room_selected_callback_) {
224 }
225 }
226 if (ImGui::MenuItem("Open as Panel")) {
227 current_room_id_ = room_id;
229 room_intent_callback_(room_id,
231 } else if (room_selected_callback_) {
233 }
234 }
235 ImGui::Separator();
236 char id_buf[16];
237 snprintf(id_buf, sizeof(id_buf), "0x%03X", room_id);
238 if (ImGui::MenuItem("Copy Room ID")) {
239 ImGui::SetClipboardText(id_buf);
240 }
241 ImGui::EndPopup();
242 }
243
244 // Tooltip with room name and thumbnail
245 if (ImGui::IsItemHovered()) {
246 ImGui::BeginTooltip();
247 ImGui::Text("%s", display_name.c_str());
248 if (rooms_) {
249 auto* loaded_room = rooms_->GetIfLoaded(room_id);
250 if (loaded_room != nullptr) {
251 ImGui::TextDisabled("Blockset: %d | Palette: %d",
252 loaded_room->blockset(),
253 loaded_room->palette());
254 auto& room = *loaded_room;
255 zelda3::RoomLayerManager layer_mgr;
256 layer_mgr.ApplyLayerMerging(room.layer_merging());
257 auto& bmp = room.GetCompositeBitmap(layer_mgr);
258 if (bmp.is_active() && bmp.texture() != 0) {
259 ImGui::Image((ImTextureID)(intptr_t)bmp.texture(),
260 ImVec2(64, 64));
261 }
262 }
263 }
264 ImGui::EndTooltip();
265 }
266
267 ImGui::TableNextColumn();
268 ImGui::TextUnformatted(display_name.c_str());
269 }
270 }
271
272 ImGui::EndTable();
273 }
274}
275
277 constexpr int kNumSpawnPoints = 7;
278 constexpr int kNumEntrances = 133;
279 constexpr int kTotalEntries = 140;
280
282 filtered_entrance_indices_.reserve(kTotalEntries);
283
284 for (int i = 0; i < kTotalEntries; i++) {
285 std::string display_name;
286
287 if (i < kNumSpawnPoints) {
288 display_name = absl::StrFormat("Spawn Point %d", i);
289 } else {
290 int entrance_id = i - kNumSpawnPoints;
291 if (entrance_id < kNumEntrances) {
292 display_name = zelda3::GetEntranceLabel(entrance_id);
293 } else {
294 display_name = absl::StrFormat("Unknown Entrance %d", i);
295 }
296 }
297
298 int room_id = (entrances_ && i < static_cast<int>(entrances_->size()))
299 ? (*entrances_)[i].room_
300 : 0;
301
302 char filter_text[256];
303 snprintf(filter_text, sizeof(filter_text), "%s %03X", display_name.c_str(),
304 room_id);
305
306 if (entrance_filter_.PassFilter(filter_text)) {
307 filtered_entrance_indices_.push_back(i);
308 }
309 }
310}
311
315
319
321 if (!rom_ || !rom_->is_loaded()) {
322 ImGui::Text("ROM not loaded");
323 return;
324 }
325
326 if (!entrances_) {
327 ImGui::Text("Entrances not loaded");
328 return;
329 }
330
331 if (show_properties) {
332 auto current_entrance = (*entrances_)[current_entrance_id_];
333
334 // Keep the full property editor in the standalone entrance panel.
335 if (ImGui::BeginTable("EntranceProps", 4, ImGuiTableFlags_Borders)) {
336 ImGui::TableSetupColumn("Core", ImGuiTableColumnFlags_WidthStretch);
337 ImGui::TableSetupColumn("Position", ImGuiTableColumnFlags_WidthStretch);
338 ImGui::TableSetupColumn("Camera", ImGuiTableColumnFlags_WidthStretch);
339 ImGui::TableSetupColumn("Scroll", ImGuiTableColumnFlags_WidthStretch);
340 ImGui::TableHeadersRow();
341
342 ImGui::TableNextRow();
343 ImGui::TableNextColumn();
344 gui::InputHexWord("Entr ID", &current_entrance.entrance_id_);
345 gui::InputHexWord("Room ID", &current_entrance.room_);
346 gui::InputHexByte("Dungeon", &current_entrance.dungeon_id_);
347 gui::InputHexByte("Music", &current_entrance.music_);
348
349 ImGui::TableNextColumn();
350 gui::InputHexWord("Player X", &current_entrance.x_position_);
351 gui::InputHexWord("Player Y", &current_entrance.y_position_);
352 gui::InputHexByte("Blockset", &current_entrance.blockset_);
353 gui::InputHexByte("Floor", &current_entrance.floor_);
354
355 ImGui::TableNextColumn();
356 gui::InputHexWord("Cam Trg X", &current_entrance.camera_trigger_x_);
357 gui::InputHexWord("Cam Trg Y", &current_entrance.camera_trigger_y_);
358 gui::InputHexWord("Exit", &current_entrance.exit_);
359
360 ImGui::TableNextColumn();
361 gui::InputHexWord("Scroll X", &current_entrance.camera_x_);
362 gui::InputHexWord("Scroll Y", &current_entrance.camera_y_);
363
364 ImGui::EndTable();
365 }
366
367 ImGui::Separator();
368 if (ImGui::CollapsingHeader("Camera Boundaries")) {
369 ImGui::Text(" North East South West");
370 ImGui::Text("Quadrant ");
371 SameLine();
372 gui::InputHexByte("##QN", &current_entrance.camera_boundary_qn_, 40.f);
373 SameLine();
374 gui::InputHexByte("##QE", &current_entrance.camera_boundary_qe_, 40.f);
375 SameLine();
376 gui::InputHexByte("##QS", &current_entrance.camera_boundary_qs_, 40.f);
377 SameLine();
378 gui::InputHexByte("##QW", &current_entrance.camera_boundary_qw_, 40.f);
379
380 ImGui::Text("Full Room ");
381 SameLine();
382 gui::InputHexByte("##FN", &current_entrance.camera_boundary_fn_, 40.f);
383 SameLine();
384 gui::InputHexByte("##FE", &current_entrance.camera_boundary_fe_, 40.f);
385 SameLine();
386 gui::InputHexByte("##FS", &current_entrance.camera_boundary_fs_, 40.f);
387 SameLine();
388 gui::InputHexByte("##FW", &current_entrance.camera_boundary_fw_, 40.f);
389 }
390 ImGui::Separator();
391 }
392
393 entrance_filter_.Draw("Filter", ImGui::GetContentRegionAvail().x);
394
395 // Rebuild cache if filter changed
396 std::string current_filter(entrance_filter_.InputBuf);
397 if (current_filter != last_entrance_filter_ ||
399 last_entrance_filter_ = current_filter;
401 }
402
403 constexpr int kNumSpawnPoints = 7;
404
405 if (ImGui::BeginTable("EntranceList", 3,
406 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders |
407 ImGuiTableFlags_RowBg |
408 ImGuiTableFlags_Resizable)) {
409 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f);
410 ImGui::TableSetupColumn("Room", ImGuiTableColumnFlags_WidthFixed, 50.0f);
411 ImGui::TableSetupColumn("Name");
412 ImGui::TableHeadersRow();
413
414 // Use ImGuiListClipper for virtualized rendering
415 ImGuiListClipper clipper;
416 clipper.Begin(static_cast<int>(filtered_entrance_indices_.size()));
417
418 while (clipper.Step()) {
419 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
420 int i = filtered_entrance_indices_[row];
421 std::string display_name;
422
423 if (i < kNumSpawnPoints) {
424 display_name = absl::StrFormat("Spawn Point %d", i);
425 } else {
426 int entrance_id = i - kNumSpawnPoints;
427 display_name = zelda3::GetEntranceLabel(entrance_id);
428 }
429
430 int room_id = (i < static_cast<int>(entrances_->size()))
431 ? (*entrances_)[i].room_
432 : 0;
433
434 ImGui::TableNextRow();
435 ImGui::TableNextColumn();
436
437 char label[32];
438 snprintf(label, sizeof(label), "%02X", i);
439 if (ImGui::Selectable(label, current_entrance_id_ == i,
440 ImGuiSelectableFlags_SpanAllColumns)) {
442 if (i < static_cast<int>(entrances_->size())) {
443 // Publish selection changed event
444 if (auto* bus = ContentRegistry::Context::event_bus()) {
445 bus->Publish(
446 SelectionChangedEvent::CreateSingle("dungeon_entrance", i));
447 }
448
449 // Legacy callback support
452 } else if (room_selected_callback_) {
454 }
455 }
456 }
457
458 ImGui::TableNextColumn();
459 ImGui::Text("%03X", room_id);
460
461 ImGui::TableNextColumn();
462 ImGui::TextUnformatted(display_name.c_str());
463 }
464 }
465
466 ImGui::EndTable();
467 }
468}
469
470} // namespace yaze::editor
471
472// ============================================================================
473// Grouped view + Blockset group name helper (appended)
474// ============================================================================
475
476namespace yaze::editor {
477
478const char* DungeonRoomSelector::GetBlocksetGroupName(uint8_t blockset) {
479 // ALttP blockset -> dungeon name mapping (vanilla ROM)
480 switch (blockset) {
481 case 0:
482 return "Hyrule Castle";
483 case 1:
484 return "Eastern Palace";
485 case 2:
486 return "Desert Palace";
487 case 3:
488 return "Tower of Hera";
489 case 4:
490 return "Agahnim's Tower";
491 case 5:
492 return "Palace of Darkness";
493 case 6:
494 return "Swamp Palace";
495 case 7:
496 return "Skull Woods";
497 case 8:
498 return "Thieves' Town";
499 case 9:
500 return "Ice Palace";
501 case 10:
502 return "Misery Mire";
503 case 11:
504 return "Turtle Rock";
505 case 12:
506 return "Ganon's Tower";
507 case 13:
508 return "Cave";
509 case 14:
510 return "Sanctuary / Church";
511 case 15:
512 return "Houses / Interior";
513 case 16:
514 return "Shops";
515 case 17:
516 return "Fairy Fountain";
517 case 18:
518 return "Underground";
519 case 19:
520 return "Master Sword";
521 case 20:
522 return "Fortune Teller";
523 case 21:
524 return "Tower (Ganon)";
525 case 22:
526 return "Chris Houlihan";
527 case 23:
528 return "Links House";
529 case 24:
530 return "Tomb";
531 default:
532 return "Unknown";
533 }
534}
535
537 RoomSelectionIntent single_click_intent) {
538 // Build groups from filtered rooms
539 // Group key = blockset (if rooms loaded), else "Unloaded"
540 struct GroupInfo {
541 const char* name;
542 std::vector<int> room_ids;
543 };
544 std::map<int, GroupInfo> groups;
545
546 for (int room_id : filtered_room_indices_) {
547 int key = 255; // Unknown/unloaded
548 const char* group_name = "Unloaded";
549 if (rooms_ && room_id >= 0 && room_id < static_cast<int>(rooms_->size())) {
550 if (auto* loaded_room = rooms_->GetIfLoaded(room_id)) {
551 key = loaded_room->blockset();
552 group_name = GetBlocksetGroupName(static_cast<uint8_t>(key));
553 }
554 }
555 auto& g = groups[key];
556 g.name = group_name;
557 g.room_ids.push_back(room_id);
558 }
559
560 // Draw as scrollable child with collapsible tree nodes
561 if (ImGui::BeginChild("##GroupedRoomList", ImVec2(0, 0), false,
562 ImGuiWindowFlags_None)) {
563 for (auto& [key, group] : groups) {
564 char header[64];
565 snprintf(header, sizeof(header), "%s (%zu rooms)##grp%d", group.name,
566 group.room_ids.size(), key);
567
568 // Auto-open the group that contains the currently selected room
569 bool has_current =
570 std::find(group.room_ids.begin(), group.room_ids.end(),
571 static_cast<int>(current_room_id_)) != group.room_ids.end();
572 if (has_current) {
573 ImGui::SetNextItemOpen(true, ImGuiCond_Once);
574 }
575
576 if (ImGui::CollapsingHeader(header)) {
577 for (int room_id : group.room_ids) {
578 std::string display_name = zelda3::GetRoomLabel(room_id);
579 char label[64];
580 snprintf(label, sizeof(label), "%03X %s##r%d", room_id,
581 display_name.c_str(), room_id);
582
583 if (ImGui::Selectable(label, current_room_id_ == room_id,
584 ImGuiSelectableFlags_AllowDoubleClick)) {
585 current_room_id_ = room_id;
586
587 if (auto* bus = ContentRegistry::Context::event_bus()) {
588 bus->Publish(
589 SelectionChangedEvent::CreateSingle("dungeon_room", room_id));
590 }
591
592 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
594 room_intent_callback_(room_id,
596 } else if (room_selected_callback_) {
598 }
599 } else {
601 room_intent_callback_(room_id, single_click_intent);
602 } else if (room_selected_callback_) {
604 }
605 }
606 }
607
608 // Tooltip with thumbnail
609 if (ImGui::IsItemHovered() && rooms_) {
610 auto* loaded_room = rooms_->GetIfLoaded(room_id);
611 if (loaded_room != nullptr) {
612 ImGui::BeginTooltip();
613 ImGui::Text("%s", display_name.c_str());
614 ImGui::TextDisabled("Blockset: %d | Palette: %d",
615 loaded_room->blockset(),
616 loaded_room->palette());
617 auto& room = *loaded_room;
618 zelda3::RoomLayerManager layer_mgr;
619 layer_mgr.ApplyLayerMerging(room.layer_merging());
620 auto& bmp = room.GetCompositeBitmap(layer_mgr);
621 if (bmp.is_active() && bmp.texture() != 0) {
622 ImGui::Image((ImTextureID)(intptr_t)bmp.texture(),
623 ImVec2(64, 64));
624 }
625 ImGui::EndTooltip();
626 }
627 }
628 }
629 }
630 }
631 }
632 ImGui::EndChild();
633}
634
635} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:132
void DrawEntranceSelectorInternal(bool show_properties)
void DrawGroupedRoomList(RoomSelectionIntent single_click_intent)
void DrawRoomSelectorInternal(RoomSelectionIntent single_click_intent, bool show_room_id_input)
void DrawRoomSelector(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
std::array< zelda3::RoomEntrance, 0x8C > * entrances_
void DrawRoomBrowser(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
bool PassesEntityTypeFilter(int room_id) const
std::function< void(int)> room_selected_callback_
std::function< void(int, RoomSelectionIntent)> room_intent_callback_
static const char * GetBlocksetGroupName(uint8_t blockset)
std::function< void(int)> entrance_selected_callback_
zelda3::Room * GetIfLoaded(int room_id)
static float GetMinTouchTarget()
RoomLayerManager - Manages layer visibility and compositing.
void ApplyLayerMerging(const LayerMergeType &merge_type)
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:228
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_FOLDER
Definition icons.h:809
::yaze::EventBus * event_bus()
Get the current EventBus instance.
Editors are the view controllers for the application.
RoomSelectionIntent
Intent for room selection in the dungeon editor.
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:354
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:380
std::string GetEntranceLabel(int id)
Convenience function to get an entrance label.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
constexpr int kNumberOfRooms
static SelectionChangedEvent CreateSingle(const std::string &src, int id, size_t session=0)