yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_connected_view.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstdio>
7#include <functional>
8#include <limits>
9#include <map>
10#include <optional>
11#include <queue>
12#include <set>
13#include <string>
14#include <tuple>
15#include <utility>
16#include <vector>
17
18#include "absl/strings/str_format.h"
21
22namespace yaze::editor {
23
24namespace {
25
43
44std::pair<int, int> ConnectedDoorDelta(zelda3::DoorDirection dir) {
45 switch (dir) {
47 return {0, -1};
49 return {0, 1};
51 return {-1, 0};
53 return {1, 0};
54 }
55 return {0, 0};
56}
57
59 const std::map<std::pair<int, int>, int>& occupied_slots, int col,
60 int row) {
61 return occupied_slots.contains({col, row});
62}
63
64std::pair<int, int> FindConnectedDoorPlacement(
65 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
66 int source_row, zelda3::DoorDirection dir) {
67 const auto [dx, dy] = ConnectedDoorDelta(dir);
68 int best_score = std::numeric_limits<int>::max();
69 std::pair<int, int> best = {source_col + dx, source_row + dy};
70
71 for (int radius = 1; radius <= 24; ++radius) {
72 for (int row = source_row - radius; row <= source_row + radius; ++row) {
73 for (int col = source_col - radius; col <= source_col + radius; ++col) {
74 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
75 continue;
76 }
77
78 const int rel_col = col - source_col;
79 const int rel_row = row - source_row;
80 const int forward = (rel_col * dx) + (rel_row * dy);
81 if (forward <= 0) {
82 continue;
83 }
84
85 const int lateral = std::abs((rel_col * dy) - (rel_row * dx));
86 const int distance_from_ideal =
87 std::abs(rel_col - dx) + std::abs(rel_row - dy);
88 const int overshoot = std::max(0, forward - 1);
89 const int score =
90 (distance_from_ideal * 16) + (lateral * 20) + (overshoot * 6);
91 if (score < best_score) {
92 best_score = score;
93 best = {col, row};
94 }
95 }
96 }
97 if (best_score != std::numeric_limits<int>::max()) {
98 return best;
99 }
100 }
101
102 return best;
103}
104
106 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
107 int source_row) {
108 int best_score = std::numeric_limits<int>::max();
109 std::pair<int, int> best = {source_col + 1, source_row + 1};
110
111 for (int radius = 1; radius <= 24; ++radius) {
112 for (int row = source_row - radius; row <= source_row + radius; ++row) {
113 for (int col = source_col - radius; col <= source_col + radius; ++col) {
114 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
115 continue;
116 }
117
118 const int rel_col = col - source_col;
119 const int rel_row = row - source_row;
120 const int distance = std::abs(rel_col) + std::abs(rel_row);
121 if (distance == 0) {
122 continue;
123 }
124
125 const bool diagonal = rel_col != 0 && rel_row != 0;
126 const bool axis_aligned = rel_col == 0 || rel_row == 0;
127 const int distance_bias = std::abs(distance - 2);
128 const int score = (distance_bias * 10) +
129 (diagonal ? 0
130 : axis_aligned ? 8
131 : 4) +
132 ((std::abs(rel_col) + std::abs(rel_row)) > 3 ? 4 : 0);
133 if (score < best_score) {
134 best_score = score;
135 best = {col, row};
136 }
137 }
138 }
139 if (best_score != std::numeric_limits<int>::max()) {
140 return best;
141 }
142 }
143
144 return best;
145}
146
147// Dedup key for the connected-mode graph.
148//
149// Door / Holewarp links are undirected — emitting from either side produces
150// the same logical edge — so we order the room pair (minmax) and treat both
151// emissions as one. There can only be one of either type per room pair.
152//
153// Staircase links carry per-instance provenance (slot_index + object_id) and
154// are *directed*: a stair object lives in exactly one room and points to
155// another. Two stair objects in the same source room targeting the same
156// destination, or two reciprocal stairs (A->B at slot 0 + B->A at slot 1),
157// are distinct edges that must each be visible in the matrix. Including
158// from_room/slot_index/object_id in the key preserves them.
159std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>
162 return std::make_tuple(link.from_room_id, link.to_room_id, link.type,
163 link.slot_index, link.object_id);
164 }
165 const auto ordered_rooms = std::minmax(link.from_room_id, link.to_room_id);
166 return std::make_tuple(ordered_rooms.first, ordered_rooms.second, link.type,
167 -1, static_cast<int16_t>(-1));
168}
169
170} // namespace
171
173 int room_id, const zelda3::Room& room,
174 const std::function<bool(int, zelda3::DoorDirection)>&
175 has_reciprocal_door) {
177
178 const auto& doors = room.GetDoors();
179 for (size_t door_index = 0; door_index < doors.size(); ++door_index) {
180 const auto& door = doors[door_index];
181 if (!zelda3::IsRoomConnectionDoorType(door.type)) {
182 continue;
183 }
184
185 const int neighbor = NeighborRoomId(room_id, door.direction);
186 if (neighbor < 0) {
187 continue;
188 }
189 if (has_reciprocal_door &&
190 !has_reciprocal_door(neighbor, OppositeDir(door.direction))) {
191 continue;
192 }
193
195 link.from_room_id = room_id;
196 link.to_room_id = neighbor;
198 link.direction = door.direction;
199 link.door_index = static_cast<int>(door_index);
200 link.door_type = door.type;
201 result.links.push_back(link);
202 }
203
204 // Walk placed header-backed staircase objects in placement order. The Nth
205 // such object consumes header slot N (room.staircase_room(N)). Mismatches
206 // are surfaced as diagnostic entries instead of being silently skipped:
207 //
208 // - Slot consumed + valid header → real Staircase link.
209 // - Slot consumed + zero/invalid header → MissingDestination diagnostic
210 // (placed object would be a dead-end stair at runtime).
211 // - >4 placed objects → ExtraPlacedObject diagnostic per surplus object.
212 // - Header non-zero but no consuming object → UnusedHeader diagnostic.
213 //
214 // ASSUMPTION (load-bearing for diagnostics): the placement-order → header-
215 // slot-index mapping mirrors how the runtime walks Room_LoadDungeonState.
216 // ZScream's `Dungeon_LoadStaircaseRooms` and the usdasm dungeon-load path
217 // both consume `Object_Tile_Staircase*` writers in placement order and
218 // index `staircase_rooms[]` with an internal counter. If any custom
219 // sub-routine ever indexes the slot table by a parameter byte instead of
220 // placement order, this mapping will misreport which placed object
221 // collides with which header destination. Verify against the ROM with
222 // `staircase_room_position_select.asm` (usdasm bank-01) when adding
223 // ROM-backed parity tests; the synthetic AddObject fixtures here do not
224 // exercise the real load path.
225 std::array<bool, 4> slot_consumed{false, false, false, false};
226 std::array<int16_t, 4> slot_object_id{
227 static_cast<int16_t>(-1), static_cast<int16_t>(-1),
228 static_cast<int16_t>(-1), static_cast<int16_t>(-1)};
229 int next_slot = 0;
230 for (const auto& object : room.GetTileObjects()) {
231 if (!IsHeaderBackedInterroomStaircaseObject(object.id_)) {
232 continue;
233 }
234 if (next_slot >= 4) {
236 extra.from_room_id = room_id;
238 extra.object_id = object.id_;
239 result.staircase_issues.push_back(extra);
240 continue;
241 }
242 slot_consumed[next_slot] = true;
243 slot_object_id[next_slot] = object.id_;
244 ++next_slot;
245 }
246
247 for (int slot = 0; slot < 4; ++slot) {
248 const int stair_room = static_cast<int>(room.staircase_room(slot));
249 const bool header_valid =
250 stair_room > 0 && stair_room < zelda3::kNumberOfRooms;
251 if (slot_consumed[slot]) {
252 if (header_valid) {
254 link.from_room_id = room_id;
255 link.to_room_id = stair_room;
258 link.slot_index = slot;
259 link.object_id = slot_object_id[slot];
260 result.links.push_back(link);
261 } else {
263 issue.from_room_id = room_id;
265 issue.slot_index = slot;
266 issue.header_room_id = stair_room;
267 issue.object_id = slot_object_id[slot];
268 result.staircase_issues.push_back(issue);
269 }
270 } else if (header_valid) {
272 issue.from_room_id = room_id;
274 issue.slot_index = slot;
275 issue.header_room_id = stair_room;
276 result.staircase_issues.push_back(issue);
277 }
278 }
279
280 const int holewarp_room = static_cast<int>(room.holewarp());
281 if (holewarp_room > 0 && holewarp_room < zelda3::kNumberOfRooms) {
283 link.from_room_id = room_id;
284 link.to_room_id = holewarp_room;
287 result.links.push_back(link);
288 }
289
290 return result;
291}
292
293std::vector<DungeonConnectedRoomLink> CollectDungeonConnectedRoomLinks(
294 int room_id, const zelda3::Room& room,
295 const std::function<bool(int, zelda3::DoorDirection)>&
296 has_reciprocal_door) {
298 has_reciprocal_door)
299 .links;
300}
301
302namespace {
303
305 switch (dir) {
307 return "North";
309 return "South";
311 return "West";
313 return "East";
314 }
315 return "?";
316}
317
318} // namespace
319
321 const DungeonConnectedRoomLink& link) {
322 switch (link.type) {
324 return absl::StrFormat("Door (%s, type 0x%02X) -> [%03X]",
325 DoorDirectionLabel(link.direction),
326 static_cast<unsigned>(link.door_type),
327 static_cast<unsigned>(link.to_room_id));
329 std::string object_str =
330 (link.object_id >= 0)
331 ? absl::StrFormat("0x%03X", static_cast<unsigned>(link.object_id))
332 : std::string("(unknown)");
333 const int slot_index = link.slot_index >= 0 ? link.slot_index : -1;
334 if (slot_index < 0) {
335 return absl::StrFormat("Staircase obj %s -> [%03X]", object_str,
336 static_cast<unsigned>(link.to_room_id));
337 }
338 return absl::StrFormat("Staircase slot %d obj %s -> [%03X]", slot_index,
339 object_str,
340 static_cast<unsigned>(link.to_room_id));
341 }
343 return absl::StrFormat("Holewarp -> [%03X]",
344 static_cast<unsigned>(link.to_room_id));
345 }
346 return absl::StrFormat("Link -> [%03X]",
347 static_cast<unsigned>(link.to_room_id));
348}
349
351 const DungeonStaircaseIssue& issue) {
352 switch (issue.kind) {
354 return absl::StrFormat(
355 "Stale staircase slot %d -> [%03X] (no placed interroom-stair "
356 "object consumes this slot)",
357 issue.slot_index, static_cast<unsigned>(issue.header_room_id));
359 const std::string object_str =
360 (issue.object_id >= 0)
361 ? absl::StrFormat("0x%03X",
362 static_cast<unsigned>(issue.object_id))
363 : std::string("(unknown)");
364 const std::string header_str =
365 (issue.header_room_id == 0)
366 ? std::string("0 (unset)")
367 : absl::StrFormat("0x%03X (out of range)",
368 static_cast<unsigned>(issue.header_room_id));
369 return absl::StrFormat(
370 "Missing staircase destination at slot %d (placed object %s, "
371 "header value %s)",
372 issue.slot_index, object_str, header_str);
373 }
375 const std::string object_str =
376 (issue.object_id >= 0)
377 ? absl::StrFormat("0x%03X",
378 static_cast<unsigned>(issue.object_id))
379 : std::string("(unknown)");
380 return absl::StrFormat(
381 "Extra staircase object %s beyond the 4 header slots (runtime "
382 "cannot reach this stair)",
383 object_str);
384 }
385 }
386 return std::string("Unknown staircase issue");
387}
388
390 int room_id) {
391 if (!rooms_ || !rom_ || !rom_->is_loaded() || room_id < 0 ||
392 room_id >= zelda3::kNumberOfRooms) {
393 return nullptr;
394 }
395
396 auto* room_ptr = rooms_->TryEnsureRoom(room_id);
397 if (!room_ptr) {
398 return nullptr;
399 }
400 auto& room = *room_ptr;
401 room.SetRom(rom_);
403 if (!room.IsLoaded()) {
404 room = zelda3::LoadRoomFromRom(rom_, room_id);
406 }
407 return &room;
408}
409
411 int room_id, zelda3::DoorDirection dir) {
413 if (!room) {
414 return false;
415 }
416
417 for (const auto& door : room->GetDoors()) {
418 if (door.direction == dir && zelda3::IsRoomConnectionDoorType(door.type)) {
419 return true;
420 }
421 }
422 return false;
423}
424
428 if (start_room_id < 0 || start_room_id >= zelda3::kNumberOfRooms) {
429 return graph;
430 }
431
432 zelda3::Room* start_room = EnsureRoomLoadedForConnectedView(start_room_id);
433 if (!start_room) {
434 return graph;
435 }
436
437 // Project-aware scoping: when the project registry maps the start room to
438 // a dungeon entry, restrict BFS to that dungeon's rooms by default.
439 // Cross-blockset / cross-dungeon resolved links are still surfaced via
440 // out_of_scope_links so the diagnostic remains visible without polluting
441 // the visual matrix with rooms from other dungeons.
442 std::set<int> scoped_room_ids;
443 std::map<int, const core::DungeonRoom*> scoped_rooms_by_id;
444 if (const auto* dungeon =
446 graph.dungeon_scope_active = true;
447 for (const auto& dungeon_room : dungeon->rooms) {
448 if (dungeon_room.id < 0 || dungeon_room.id >= zelda3::kNumberOfRooms) {
449 continue;
450 }
451 scoped_room_ids.insert(dungeon_room.id);
452 scoped_rooms_by_id[dungeon_room.id] = &dungeon_room;
453 graph.room_floor_labels[static_cast<size_t>(dungeon_room.id)] =
454 dungeon_room.floor;
455 if (!dungeon_room.floor.empty() &&
456 std::find(graph.floor_order.begin(), graph.floor_order.end(),
457 dungeon_room.floor) == graph.floor_order.end()) {
458 graph.floor_order.push_back(dungeon_room.floor);
459 }
460 }
461 }
462
463 std::map<std::pair<int, int>, int> occupied_slots;
464 std::queue<int> to_visit;
465 std::set<std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>>
466 seen_links;
467
468 auto track_room_bounds = [&](int room_id) {
469 const auto& placement = graph.room_positions[static_cast<size_t>(room_id)];
470 graph.min_col = std::min(graph.min_col, placement.col);
471 graph.max_col = std::max(graph.max_col, placement.col);
472 graph.min_row = std::min(graph.min_row, placement.row);
473 graph.max_row = std::max(graph.max_row, placement.row);
474 };
475
476 auto registry_placement_for =
477 [&](int room_id) -> std::optional<std::pair<int, int>> {
478 const auto it = scoped_rooms_by_id.find(room_id);
479 if (it == scoped_rooms_by_id.end() || !it->second->has_grid_position) {
480 return std::nullopt;
481 }
482 return std::make_pair(it->second->grid_col, it->second->grid_row);
483 };
484
485 auto place_room = [&](int room_id, std::pair<int, int> desired_placement,
486 bool connected_to_start) {
487 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
488 return;
489 }
490 const size_t index = static_cast<size_t>(room_id);
491 if (graph.room_mask[index]) {
492 if (connected_to_start &&
493 !graph.room_positions[index].connected_to_start) {
494 graph.room_positions[index].connected_to_start = true;
495 graph.unlinked_room_count = std::max(0, graph.unlinked_room_count - 1);
496 }
497 return;
498 }
499
500 std::pair<int, int> placement = desired_placement;
501 const auto occupied = occupied_slots.find(placement);
502 if (occupied != occupied_slots.end() && occupied->second != room_id) {
503 placement = FindConnectedTransportPlacement(
504 occupied_slots, desired_placement.first, desired_placement.second);
505 }
506
507 graph.room_mask[index] = true;
508 graph.room_positions[index] = {placement.first, placement.second, true,
509 connected_to_start};
510 occupied_slots[placement] = room_id;
511 ++graph.room_count;
512 if (!connected_to_start) {
513 ++graph.unlinked_room_count;
514 }
515 if (graph.room_count == 1) {
516 graph.min_col = graph.max_col = placement.first;
517 graph.min_row = graph.max_row = placement.second;
518 } else {
519 track_room_bounds(room_id);
520 }
521 };
522
523 auto in_dungeon_scope = [&](int candidate_room_id) {
524 if (!graph.dungeon_scope_active) {
525 return true;
526 }
527 return scoped_room_ids.find(candidate_room_id) != scoped_room_ids.end();
528 };
529
530 place_room(
531 start_room_id,
532 registry_placement_for(start_room_id).value_or(std::make_pair(0, 0)),
533 true);
534 to_visit.push(start_room_id);
535
536 while (!to_visit.empty()) {
537 const int room_id = to_visit.front();
538 to_visit.pop();
539
541 if (!room) {
542 continue;
543 }
544
545 const auto outgoing_diagnostics =
547 room_id, *room,
548 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
549 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
550 });
551 for (const auto& issue : outgoing_diagnostics.staircase_issues) {
552 graph.staircase_issues.push_back(issue);
553 }
554 const auto& outgoing_links = outgoing_diagnostics.links;
555
556 for (const auto& link : outgoing_links) {
557 if (link.to_room_id < 0 || link.to_room_id >= zelda3::kNumberOfRooms) {
558 continue;
559 }
560
561 // Out-of-scope link (target room is not in the current dungeon group):
562 // record as a diagnostic, do not recurse, do not add the target to the
563 // visual graph. We still de-dup against `seen_links` so a reciprocal
564 // edge from the other side won't double-list it later.
565 if (!in_dungeon_scope(link.to_room_id)) {
566 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
567 graph.out_of_scope_links.push_back(link);
568 }
569 continue;
570 }
571
572 if (!EnsureRoomLoadedForConnectedView(link.to_room_id)) {
573 continue;
574 }
575
576 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
577 graph.links.push_back(link);
578 }
579
580 if (!graph.room_mask[static_cast<size_t>(link.to_room_id)]) {
581 const auto& source_placement =
582 graph.room_positions[static_cast<size_t>(room_id)];
583 const std::pair<int, int> placement =
584 registry_placement_for(link.to_room_id)
585 .value_or((link.type == DungeonConnectedLinkType::Door)
586 ? FindConnectedDoorPlacement(
587 occupied_slots, source_placement.col,
588 source_placement.row, link.direction)
589 : FindConnectedTransportPlacement(
590 occupied_slots, source_placement.col,
591 source_placement.row));
592 place_room(link.to_room_id, placement, true);
593 to_visit.push(link.to_room_id);
594 }
595 }
596 }
597
598 // In project-scoped mode, connected view should show the whole dungeon group,
599 // not only the currently reachable component. Unlinked rooms are drawn in the
600 // same canvas with floor badges so bad headers/stairs are visible and fixable
601 // instead of hiding the rest of the floor.
602 if (graph.dungeon_scope_active) {
603 const auto start_placement =
604 graph.room_positions[static_cast<size_t>(start_room_id)];
605 for (int room_id : scoped_room_ids) {
606 if (graph.room_mask[static_cast<size_t>(room_id)]) {
607 continue;
608 }
609 const std::pair<int, int> placement =
610 registry_placement_for(room_id).value_or(
611 FindConnectedTransportPlacement(
612 occupied_slots, start_placement.col, start_placement.row));
613 place_room(room_id, placement, false);
614 }
615 }
616
617 return graph;
618}
619
621 int center_room_id) {
622 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms) {
623 connected_action_status_message_ = "No current room for connected fixes.";
625 return 0;
626 }
627
628 if (connected_graph_cache_start_room_id_ != center_room_id) {
631 }
632
633 int fixed_count = 0;
634 for (const auto& issue : connected_graph_cache_.staircase_issues) {
635 if (issue.kind != DungeonStaircaseIssueKind::UnusedHeader ||
636 issue.slot_index < 0 || issue.slot_index >= 4 ||
637 issue.header_room_id <= 0) {
638 continue;
639 }
640 zelda3::Room* room = EnsureRoomLoadedForConnectedView(issue.from_room_id);
641 if (!room || room->staircase_room(issue.slot_index) !=
642 static_cast<uint8_t>(issue.header_room_id)) {
643 continue;
644 }
645 room->SetStaircaseRoom(issue.slot_index, 0);
646 ++fixed_count;
647 }
648
649 if (fixed_count > 0) {
651 absl::StrFormat("Cleared %d stale staircase header slot%s.",
652 fixed_count, fixed_count == 1 ? "" : "s");
656 } else {
658 "No stale staircase header slots can be auto-cleared.";
660 }
661 return fixed_count;
662}
663
664} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:155
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
int ApplyConnectedStaircaseIssueAutoFixes(int center_room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ConnectedRoomGraphData connected_graph_cache_
ConnectedRoomGraphData BuildConnectedRoomGraph(int start_room_id)
const project::YazeProject * project_
zelda3::Room * TryEnsureRoom(int room_id)
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
uint8_t holewarp() const
Definition room.h:962
void SetStaircaseRoom(int index, uint8_t room)
Definition room.h:777
uint8_t staircase_room(int index) const
Definition room.h:944
const std::vector< Door > & GetDoors() const
Definition room.h:373
bool IsLoaded() const
Definition room.h:919
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:405
void SetRom(Rom *rom)
Definition room.h:1010
void SetGameData(GameData *data)
Definition room.h:1012
bool IsConnectedSlotOccupied(const std::map< std::pair< int, int >, int > &occupied_slots, int col, int row)
std::pair< int, int > FindConnectedDoorPlacement(const std::map< std::pair< int, int >, int > &occupied_slots, int source_col, int source_row, zelda3::DoorDirection dir)
std::tuple< int, int, DungeonConnectedLinkType, int, int16_t > MakeConnectedLinkKey(const DungeonConnectedRoomLink &link)
std::pair< int, int > FindConnectedTransportPlacement(const std::map< std::pair< int, int >, int > &occupied_slots, int source_col, int source_row)
const core::DungeonEntry * FindDungeonForRoom(const project::YazeProject *project, int room_id, size_t *dungeon_index=nullptr)
Editors are the view controllers for the application.
std::string FormatDungeonConnectedLinkDescription(const DungeonConnectedRoomLink &link)
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
DungeonConnectedRoomLinkDiagnostics CollectDungeonConnectedRoomLinkDiagnostics(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
std::vector< DungeonConnectedRoomLink > CollectDungeonConnectedRoomLinks(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
Definition door_types.h:333
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:648
constexpr int kNumberOfRooms
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
std::array< RoomPlacement, zelda3::kNumberOfRooms > room_positions
std::array< std::string, zelda3::kNumberOfRooms > room_floor_labels
std::vector< DungeonConnectedRoomLink > links
std::vector< DungeonStaircaseIssue > staircase_issues