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