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