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"
20#include "core/features.h"
24
25namespace yaze::editor {
26
27namespace {
28
46
47std::pair<int, int> ConnectedDoorDelta(zelda3::DoorDirection dir) {
48 switch (dir) {
50 return {0, -1};
52 return {0, 1};
54 return {-1, 0};
56 return {1, 0};
57 }
58 return {0, 0};
59}
60
62 const std::map<std::pair<int, int>, int>& occupied_slots, int col,
63 int row) {
64 return occupied_slots.contains({col, row});
65}
66
67std::pair<int, int> FindConnectedDoorPlacement(
68 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
69 int source_row, zelda3::DoorDirection dir) {
70 const auto [dx, dy] = ConnectedDoorDelta(dir);
71 int best_score = std::numeric_limits<int>::max();
72 std::pair<int, int> best = {source_col + dx, source_row + dy};
73
74 for (int radius = 1; radius <= 24; ++radius) {
75 for (int row = source_row - radius; row <= source_row + radius; ++row) {
76 for (int col = source_col - radius; col <= source_col + radius; ++col) {
77 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
78 continue;
79 }
80
81 const int rel_col = col - source_col;
82 const int rel_row = row - source_row;
83 const int forward = (rel_col * dx) + (rel_row * dy);
84 if (forward <= 0) {
85 continue;
86 }
87
88 const int lateral = std::abs((rel_col * dy) - (rel_row * dx));
89 const int distance_from_ideal =
90 std::abs(rel_col - dx) + std::abs(rel_row - dy);
91 const int overshoot = std::max(0, forward - 1);
92 const int score =
93 (distance_from_ideal * 16) + (lateral * 20) + (overshoot * 6);
94 if (score < best_score) {
95 best_score = score;
96 best = {col, row};
97 }
98 }
99 }
100 if (best_score != std::numeric_limits<int>::max()) {
101 return best;
102 }
103 }
104
105 return best;
106}
107
109 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
110 int source_row) {
111 int best_score = std::numeric_limits<int>::max();
112 std::pair<int, int> best = {source_col + 1, source_row + 1};
113
114 for (int radius = 1; radius <= 24; ++radius) {
115 for (int row = source_row - radius; row <= source_row + radius; ++row) {
116 for (int col = source_col - radius; col <= source_col + radius; ++col) {
117 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
118 continue;
119 }
120
121 const int rel_col = col - source_col;
122 const int rel_row = row - source_row;
123 const int distance = std::abs(rel_col) + std::abs(rel_row);
124 if (distance == 0) {
125 continue;
126 }
127
128 const bool diagonal = rel_col != 0 && rel_row != 0;
129 const bool axis_aligned = rel_col == 0 || rel_row == 0;
130 const int distance_bias = std::abs(distance - 2);
131 const int score = (distance_bias * 10) +
132 (diagonal ? 0
133 : axis_aligned ? 8
134 : 4) +
135 ((std::abs(rel_col) + std::abs(rel_row)) > 3 ? 4 : 0);
136 if (score < best_score) {
137 best_score = score;
138 best = {col, row};
139 }
140 }
141 }
142 if (best_score != std::numeric_limits<int>::max()) {
143 return best;
144 }
145 }
146
147 return best;
148}
149
150// Dedup key for the connected-mode graph.
151//
152// Door links are undirected. Transport links are directed and keep their
153// slot provenance, including project warp-tag quadrants targeting one room.
154std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>
157 return std::make_tuple(link.from_room_id, link.to_room_id, link.type,
158 link.slot_index, link.object_id);
159 }
160 const auto ordered_rooms = std::minmax(link.from_room_id, link.to_room_id);
161 return std::make_tuple(ordered_rooms.first, ordered_rooms.second, link.type,
162 -1, static_cast<int16_t>(-1));
163}
164
165} // namespace
166
168 int room_id, const zelda3::Room& room,
169 const std::function<bool(int, zelda3::DoorDirection)>& has_reciprocal_door,
170 const zelda3::RoomLinkFacts* link_facts,
171 const std::set<uint8_t>& warp_tag_ids) {
173
174 const auto& doors = room.GetDoors();
175 for (size_t door_index = 0; door_index < doors.size(); ++door_index) {
176 const auto& door = doors[door_index];
177 if (!zelda3::IsRoomConnectionDoorType(door.type)) {
178 continue;
179 }
180
181 const int neighbor = NeighborRoomId(room_id, door.direction);
182 if (neighbor < 0) {
183 continue;
184 }
185 if (has_reciprocal_door &&
186 !has_reciprocal_door(neighbor, OppositeDir(door.direction))) {
187 continue;
188 }
189
191 link.from_room_id = room_id;
192 link.to_room_id = neighbor;
194 link.direction = door.direction;
195 link.door_index = static_cast<int>(door_index);
196 link.door_type = door.type;
197 result.links.push_back(link);
198 }
199
200 // Provisional navigation only: placement order is not a verified runtime
201 // slot mapping. Vanilla writers use family-specific counters ($0438,
202 // $047E/$0482, $04A2/$04A4, $043A, $0480/$0484, $04A6/$04A8) into $06B0.
203 // Collision generation assigns indices later; $0462 & 3 selects the header.
204 // Do not use these estimates to mutate or clear room metadata.
205 std::array<bool, 4> slot_consumed{false, false, false, false};
206 std::array<int16_t, 4> slot_object_id{
207 static_cast<int16_t>(-1), static_cast<int16_t>(-1),
208 static_cast<int16_t>(-1), static_cast<int16_t>(-1)};
209 int next_slot = 0;
210 for (const auto& object : room.GetTileObjects()) {
211 if (!IsHeaderBackedInterroomStaircaseObject(object.id_)) {
212 continue;
213 }
214 if (next_slot >= 4) {
216 extra.from_room_id = room_id;
218 extra.object_id = object.id_;
219 result.staircase_issues.push_back(extra);
220 continue;
221 }
222 slot_consumed[next_slot] = true;
223 slot_object_id[next_slot] = object.id_;
224 ++next_slot;
225 }
226
227 for (int slot = 0; slot < 4; ++slot) {
228 // Room 000 is a valid destination. The runtime writes the header byte to
229 // $A0 and keeps $A1, so $119 slot $1D leads to $11D; zero is not a
230 // sentinel for a consumed slot.
231 const int stair_room = room.staircase_destination_room(slot);
232 const bool header_valid =
233 stair_room >= 0 && stair_room < zelda3::kNumberOfRooms;
234 if (slot_consumed[slot]) {
235 if (header_valid) {
237 link.from_room_id = room_id;
238 link.to_room_id = stair_room;
241 link.slot_index = slot;
242 link.object_id = slot_object_id[slot];
243 result.links.push_back(link);
244 } else {
246 issue.from_room_id = room_id;
248 issue.slot_index = slot;
249 issue.header_room_id = stair_room;
250 issue.object_id = slot_object_id[slot];
251 result.staircase_issues.push_back(issue);
252 }
253 } else if (header_valid && room.staircase_room(slot) != 0 &&
254 !warp_tag_ids.contains(static_cast<uint8_t>(room.tag1())) &&
255 !warp_tag_ids.contains(static_cast<uint8_t>(room.tag2()))) {
257 issue.from_room_id = room_id;
259 issue.slot_index = slot;
260 issue.header_room_id = stair_room;
261 result.staircase_issues.push_back(issue);
262 }
263 }
264
265 // A header byte alone is not a traversable edge. Reuse the census rules
266 // so damaging pits, redefined tags and a consumed zero destination agree.
267 const auto loaded_facts = zelda3::CollectLoadedRoomLinkFacts(
268 room, room.game_data() ? &room.game_data()->pit_damage_table : nullptr);
269 const auto& facts = link_facts ? *link_facts : loaded_facts;
270 const auto shared_links = zelda3::CollectRoomLinks(
271 room_id, [&](int id) { return id == room_id ? &facts : nullptr; },
272 warp_tag_ids);
273 for (const auto& shared : shared_links) {
274 if (!shared.strong || shared.to < 0 ||
275 (shared.kind != zelda3::RoomReferenceKind::kHolewarp &&
276 shared.kind != zelda3::RoomReferenceKind::kWarpTag)) {
277 continue;
278 }
280 link.from_room_id = room_id;
281 link.to_room_id = shared.to;
284 link.slot_index = shared.slot;
285 result.links.push_back(link);
286 }
287
288 return result;
289}
290
291std::vector<DungeonConnectedRoomLink> CollectDungeonConnectedRoomLinks(
292 int room_id, const zelda3::Room& room,
293 const std::function<bool(int, zelda3::DoorDirection)>& has_reciprocal_door,
294 const zelda3::RoomLinkFacts* link_facts,
295 const std::set<uint8_t>& warp_tag_ids) {
297 room_id, room, has_reciprocal_door, link_facts, warp_tag_ids)
298 .links;
299}
300
301namespace {
302
304 switch (dir) {
306 return "North";
308 return "South";
310 return "West";
312 return "East";
313 }
314 return "?";
315}
316
317} // namespace
318
320 const DungeonConnectedRoomLink& link) {
321 switch (link.type) {
323 return absl::StrFormat("Door (%s, type 0x%02X) -> [%03X]",
324 DoorDirectionLabel(link.direction),
325 static_cast<unsigned>(link.door_type),
326 static_cast<unsigned>(link.to_room_id));
328 std::string object_str =
329 (link.object_id >= 0)
330 ? absl::StrFormat("0x%03X", static_cast<unsigned>(link.object_id))
331 : std::string("(unknown)");
332 const int slot_index = link.slot_index >= 0 ? link.slot_index : -1;
333 if (slot_index < 0) {
334 return absl::StrFormat("Estimated staircase obj %s -> [%03X]",
335 object_str,
336 static_cast<unsigned>(link.to_room_id));
337 }
338 return absl::StrFormat("Estimated staircase slot %d obj %s -> [%03X]",
339 slot_index, object_str,
340 static_cast<unsigned>(link.to_room_id));
341 }
343 if (link.slot_index >= 0) {
344 return absl::StrFormat("Warp tag quadrant %d -> [%03X]",
345 link.slot_index + 1, link.to_room_id);
346 }
347 return absl::StrFormat("Holewarp -> [%03X]",
348 static_cast<unsigned>(link.to_room_id));
349 }
350 return absl::StrFormat("Link -> [%03X]",
351 static_cast<unsigned>(link.to_room_id));
352}
353
355 const DungeonStaircaseIssue& issue) {
356 switch (issue.kind) {
358 return absl::StrFormat(
359 "Possibly unused staircase slot %d -> [%03X] (no placed "
360 "interroom-stair "
361 "object consumes this slot)",
362 issue.slot_index, static_cast<unsigned>(issue.header_room_id));
364 const std::string object_str =
365 (issue.object_id >= 0)
366 ? absl::StrFormat("0x%03X",
367 static_cast<unsigned>(issue.object_id))
368 : std::string("(unknown)");
369 const std::string header_str = absl::StrFormat(
370 "0x%03X (out of range)", static_cast<unsigned>(issue.header_room_id));
371 return absl::StrFormat(
372 "Estimated missing staircase destination at slot %d (placed object "
373 "%s, "
374 "header value %s)",
375 issue.slot_index, object_str, header_str);
376 }
378 const std::string object_str =
379 (issue.object_id >= 0)
380 ? absl::StrFormat("0x%03X",
381 static_cast<unsigned>(issue.object_id))
382 : std::string("(unknown)");
383 return absl::StrFormat(
384 "Additional staircase object %s beyond the estimated 4 slots "
385 "(runtime mapping unverified)",
386 object_str);
387 }
388 }
389 return std::string("Unknown staircase issue");
390}
391
393 int room_id) {
394 if (!rooms_ || !rom_ || !rom_->is_loaded() || room_id < 0 ||
395 room_id >= zelda3::kNumberOfRooms) {
396 return nullptr;
397 }
398
399 auto* room_ptr = rooms_->TryEnsureRoom(room_id);
400 if (!room_ptr) {
401 return nullptr;
402 }
403 auto& room = *room_ptr;
404 room.SetRom(rom_);
406 if (!room.IsLoaded()) {
407 room = zelda3::LoadRoomFromRom(rom_, room_id);
409 }
410 return &room;
411}
412
414 int room_id, zelda3::DoorDirection dir) {
416 if (!room) {
417 return false;
418 }
419
420 for (const auto& door : room->GetDoors()) {
421 if (door.direction == dir && zelda3::IsRoomConnectionDoorType(door.type)) {
422 return true;
423 }
424 }
425 return false;
426}
427
431 if (start_room_id < 0 || start_room_id >= zelda3::kNumberOfRooms) {
432 return graph;
433 }
434
435 zelda3::Room* start_room = EnsureRoomLoadedForConnectedView(start_room_id);
436 if (!start_room) {
437 return graph;
438 }
439
440 // Project-aware scoping: when the project registry maps the start room to
441 // a dungeon entry, restrict BFS to that dungeon's rooms by default.
442 // Cross-blockset / cross-dungeon resolved links are still surfaced via
443 // out_of_scope_links so the diagnostic remains visible without polluting
444 // the visual matrix with rooms from other dungeons.
445 std::set<int> scoped_room_ids;
446 std::map<int, const core::DungeonRoom*> scoped_rooms_by_id;
447 if (const auto* dungeon =
449 graph.dungeon_scope_active = true;
450 for (const auto& dungeon_room : dungeon->rooms) {
451 if (dungeon_room.id < 0 || dungeon_room.id >= zelda3::kNumberOfRooms) {
452 continue;
453 }
454 scoped_room_ids.insert(dungeon_room.id);
455 scoped_rooms_by_id[dungeon_room.id] = &dungeon_room;
456 graph.room_floor_labels[static_cast<size_t>(dungeon_room.id)] =
457 dungeon_room.floor;
458 if (!dungeon_room.floor.empty() &&
459 std::find(graph.floor_order.begin(), graph.floor_order.end(),
460 dungeon_room.floor) == graph.floor_order.end()) {
461 graph.floor_order.push_back(dungeon_room.floor);
462 }
463 }
464 }
465
466 const auto custom_objects =
468 core::FeatureFlags::get().kEnableCustomObjects);
469 const std::set<uint8_t> warp_tags =
471 : std::set<uint8_t>{};
472 zelda3::PitDamageTable rom_pit_table;
473 const zelda3::PitDamageTable* pit_table =
475 if (!pit_table &&
476 zelda3::PitDamageTable::LoadFromRom(rom_, &rom_pit_table).ok()) {
477 pit_table = &rom_pit_table;
478 }
479 auto collect_diagnostics = [&](int room_id, zelda3::Room& room) {
480 if (room.GetSprites().empty()) {
481 room.EnsureSpritesLoaded();
482 }
483 auto facts =
484 zelda3::CollectRoomLinkFacts(rom_, room, pit_table, &custom_objects);
485 // Incomplete/synthetic ROMs can still establish placed object links.
486 const auto loaded_facts =
487 zelda3::CollectLoadedRoomLinkFacts(room, pit_table);
489 room_id, room,
490 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
491 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
492 },
493 facts.ok() ? &*facts : &loaded_facts, warp_tags);
494 };
495
496 std::map<std::pair<int, int>, int> occupied_slots;
497 std::queue<int> to_visit;
498 std::set<std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>>
499 seen_links;
500
501 auto track_room_bounds = [&](int room_id) {
502 const auto& placement = graph.room_positions[static_cast<size_t>(room_id)];
503 graph.min_col = std::min(graph.min_col, placement.col);
504 graph.max_col = std::max(graph.max_col, placement.col);
505 graph.min_row = std::min(graph.min_row, placement.row);
506 graph.max_row = std::max(graph.max_row, placement.row);
507 };
508
509 auto registry_placement_for =
510 [&](int room_id) -> std::optional<std::pair<int, int>> {
511 const auto it = scoped_rooms_by_id.find(room_id);
512 if (it == scoped_rooms_by_id.end() || !it->second->has_grid_position) {
513 return std::nullopt;
514 }
515 return std::make_pair(it->second->grid_col, it->second->grid_row);
516 };
517
518 auto place_room = [&](int room_id, std::pair<int, int> desired_placement,
519 bool connected_to_start) {
520 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
521 return;
522 }
523 const size_t index = static_cast<size_t>(room_id);
524 if (graph.room_mask[index]) {
525 if (connected_to_start &&
526 !graph.room_positions[index].connected_to_start) {
527 graph.room_positions[index].connected_to_start = true;
528 graph.unlinked_room_count = std::max(0, graph.unlinked_room_count - 1);
529 }
530 return;
531 }
532
533 std::pair<int, int> placement = desired_placement;
534 const auto occupied = occupied_slots.find(placement);
535 if (occupied != occupied_slots.end() && occupied->second != room_id) {
536 placement = FindConnectedTransportPlacement(
537 occupied_slots, desired_placement.first, desired_placement.second);
538 }
539
540 graph.room_mask[index] = true;
541 graph.room_positions[index] = {placement.first, placement.second, true,
542 connected_to_start};
543 occupied_slots[placement] = room_id;
544 ++graph.room_count;
545 if (!connected_to_start) {
546 ++graph.unlinked_room_count;
547 }
548 if (graph.room_count == 1) {
549 graph.min_col = graph.max_col = placement.first;
550 graph.min_row = graph.max_row = placement.second;
551 } else {
552 track_room_bounds(room_id);
553 }
554 };
555
556 auto in_dungeon_scope = [&](int candidate_room_id) {
557 if (!graph.dungeon_scope_active) {
558 return true;
559 }
560 return scoped_room_ids.find(candidate_room_id) != scoped_room_ids.end();
561 };
562
563 place_room(
564 start_room_id,
565 registry_placement_for(start_room_id).value_or(std::make_pair(0, 0)),
566 true);
567 to_visit.push(start_room_id);
568
569 while (!to_visit.empty()) {
570 const int room_id = to_visit.front();
571 to_visit.pop();
572
574 if (!room) {
575 continue;
576 }
577
578 const auto outgoing_diagnostics = collect_diagnostics(room_id, *room);
579 graph.room_diagnostics[room_id] = outgoing_diagnostics;
580 for (const auto& issue : outgoing_diagnostics.staircase_issues) {
581 graph.staircase_issues.push_back(issue);
582 }
583 const auto& outgoing_links = outgoing_diagnostics.links;
584
585 for (const auto& link : outgoing_links) {
586 if (link.to_room_id < 0 || link.to_room_id >= zelda3::kNumberOfRooms) {
587 continue;
588 }
589
590 // Out-of-scope link (target room is not in the current dungeon group):
591 // record as a diagnostic, do not recurse, do not add the target to the
592 // visual graph. We still de-dup against `seen_links` so a reciprocal
593 // edge from the other side won't double-list it later.
594 if (!in_dungeon_scope(link.to_room_id)) {
595 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
596 graph.out_of_scope_links.push_back(link);
597 }
598 continue;
599 }
600
601 if (!EnsureRoomLoadedForConnectedView(link.to_room_id)) {
602 continue;
603 }
604
605 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
606 graph.links.push_back(link);
607 }
608
609 if (!graph.room_mask[static_cast<size_t>(link.to_room_id)]) {
610 const auto& source_placement =
611 graph.room_positions[static_cast<size_t>(room_id)];
612 const std::pair<int, int> placement =
613 registry_placement_for(link.to_room_id)
614 .value_or((link.type == DungeonConnectedLinkType::Door)
615 ? FindConnectedDoorPlacement(
616 occupied_slots, source_placement.col,
617 source_placement.row, link.direction)
618 : FindConnectedTransportPlacement(
619 occupied_slots, source_placement.col,
620 source_placement.row));
621 place_room(link.to_room_id, placement, true);
622 to_visit.push(link.to_room_id);
623 }
624 }
625 }
626
627 // In project-scoped mode, connected view should show the whole dungeon group,
628 // not only the currently reachable component. Unlinked rooms are drawn in the
629 // same canvas with floor badges so bad headers/stairs are visible and fixable
630 // instead of hiding the rest of the floor.
631 if (graph.dungeon_scope_active) {
632 const auto start_placement =
633 graph.room_positions[static_cast<size_t>(start_room_id)];
634 for (int room_id : scoped_room_ids) {
635 if (graph.room_mask[static_cast<size_t>(room_id)]) {
636 continue;
637 }
638 // Project registry entries can name rooms that have never been opened.
639 // Materialize their ROM data before placing them in the preview graph;
640 // otherwise the renderer sees a default Room with ID zero.
641 auto* room = EnsureRoomLoadedForConnectedView(room_id);
642 if (!room) {
643 continue;
644 }
645 graph.room_diagnostics[room_id] = collect_diagnostics(room_id, *room);
646 const std::pair<int, int> placement =
647 registry_placement_for(room_id).value_or(
648 FindConnectedTransportPlacement(
649 occupied_slots, start_placement.col, start_placement.row));
650 place_room(room_id, placement, false);
651 }
652 }
653
654 return graph;
655}
656
658 int center_room_id) {
659 // Retain a non-mutating guard for legacy callers until the runtime mapping
660 // is implemented. Neither cached diagnostics nor a fresh estimate proves
661 // that a destination is unused.
662 (void)center_room_id;
664 "Automatic staircase repair is unavailable: slot mapping is unverified. "
665 "Review destinations in the room inspector.";
667 return 0;
668}
669
670} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
int ApplyConnectedStaircaseIssueAutoFixes(int center_room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ConnectedRoomGraphData BuildConnectedRoomGraph(int start_room_id)
const project::YazeProject * project_
zelda3::Room * TryEnsureRoom(int room_id)
CustomObjectRenderSnapshot CaptureRenderSnapshot(bool enabled)
static CustomObjectManager & Get()
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
static absl::Status LoadFromRom(Rom *rom, PitDamageTable *out)
void EnsureSpritesLoaded()
Definition room.cc:1033
int staircase_destination_room(int index) const
Definition room.h:1004
auto game_data()
Definition room.h:1086
uint8_t staircase_room(int index) const
Definition room.h:999
const std::vector< Door > & GetDoors() const
Definition room.h:413
bool IsLoaded() const
Definition room.h:970
TagKey tag2() const
Definition room.h:988
TagKey tag1() const
Definition room.h:987
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void SetRom(Rom *rom)
Definition room.h:1085
void SetGameData(GameData *data)
Definition room.h:1088
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, const zelda3::RoomLinkFacts *link_facts, const std::set< uint8_t > &warp_tag_ids)
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, const zelda3::RoomLinkFacts *link_facts, const std::set< uint8_t > &warp_tag_ids)
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
std::set< uint8_t > RoomCensusWarpTagsFromManifest(const core::HackManifest &manifest)
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
Definition door_types.h:333
std::vector< RoomLink > CollectRoomLinks(int room_id, const RoomLinkFactsLookup &lookup, const std::set< uint8_t > &warp_tag_ids)
absl::StatusOr< RoomLinkFacts > CollectRoomLinkFacts(Rom *rom, const Room &room, const PitDamageTable *pit_table, const CustomObjectRenderSnapshot *custom_objects, util::StopToken stop_token)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
constexpr int kNumberOfRooms
RoomLinkFacts CollectLoadedRoomLinkFacts(const Room &room, const PitDamageTable *pit_table)
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::map< int, DungeonConnectedRoomLinkDiagnostics > room_diagnostics
std::vector< DungeonConnectedRoomLink > links
std::vector< DungeonStaircaseIssue > staircase_issues
core::HackManifest hack_manifest
Definition project.h:376
PitDamageTable pit_damage_table
Definition game_data.h:114