yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_graph_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <optional>
7#include <queue>
8#include <set>
9#include <string>
10#include <vector>
11
12#include "absl/status/statusor.h"
13#include "absl/strings/numbers.h"
14#include "absl/strings/str_format.h"
17#include "cli/util/hex_util.h"
18#include "core/hack_manifest.h"
19#include "rom/rom.h"
21#include "zelda3/dungeon/room.h"
26
27namespace yaze {
28namespace cli {
29namespace handlers {
30
32
33namespace {
34
35// All three graph commands take their edges from zelda3::CollectRoomLinks,
36// the same links the room census uses (room_links.h): entrance tables,
37// header destination bytes (byte 0, out-of-range results), grid neighbors,
38// teleport doors and live holes are decided in one place.
39
40std::string RoomHex(int room_id) {
41 return absl::StrFormat("0x%03X", room_id);
42}
43
44std::string RoomName(int room_id) {
45 if (zelda3::IsDungeonRoomId(room_id)) {
46 return std::string(zelda3::kRoomNames[room_id]);
47 }
48 return absl::StrFormat("Room 0x%03X", room_id);
49}
50
51const char* EntranceTableName(bool expanded) {
52 return expanded ? "zscream_bank_0f" : "vanilla_bank_02";
53}
54
55// Links that come from header bytes: stair slots, warp tags, the holewarp,
56// header-only bytes and teleport doors (stair slot 3 or 4).
59}
60
61// stair1-stair4 by slot, holewarp, or teleport_door.
62std::string HeaderLinkType(const zelda3::RoomLink& link) {
64 return "teleport_door";
65 }
66 if (link.slot >= 0) {
67 return absl::StrFormat("stair%d", link.slot + 1);
68 }
69 return "holewarp";
70}
71
73 const zelda3::RoomLink& link) {
74 formatter.BeginObject();
75 formatter.AddField("from", RoomHex(link.from));
76 formatter.AddField("to", RoomHex(link.to >= 0 ? link.to : link.raw_to));
77 formatter.AddField("type", HeaderLinkType(link));
78 formatter.AddField("kind", zelda3::RoomReferenceKindName(link.kind));
79 formatter.AddField("strong", link.strong);
80 formatter.AddField("detail", link.detail);
81 formatter.EndObject();
82}
83
84// Header destinations past 0x127 are not rooms; list them instead of
85// walking into them.
87 const std::vector<zelda3::RoomLink>& links) {
88 formatter.BeginArray("out_of_range");
89 for (const auto& link : links) {
90 AddLinkObject(formatter, link);
91 }
92 formatter.EndArray();
93}
94
95// Warp tags from --project (the manifest's "WarpTag" room tags), the same
96// input dungeon-room-census uses.
97absl::StatusOr<std::set<uint8_t>> WarpTagsFromProject(
98 const resources::ArgumentParser& parser, core::HackManifest* manifest,
99 bool* loaded) {
100 *loaded = false;
101 auto project = parser.GetString("project");
102 if (!project.has_value()) {
103 return std::set<uint8_t>{};
104 }
105 if (auto status = LoadRoomCensusProject(*project, manifest); !status.ok()) {
106 return status;
107 }
108 *loaded = true;
110}
111
112absl::StatusOr<zelda3::DungeonEntranceTarget> EntranceStart(Rom* rom,
113 int entrance_id) {
114 auto target = zelda3::ReadDungeonEntranceTarget(*rom, entrance_id);
115 if (!target.has_value()) {
116 return absl::OutOfRangeError(absl::StrFormat(
117 "Entrance 0x%02X table entry is outside the ROM.", entrance_id));
118 }
119 if (!zelda3::IsDungeonRoomId(target->room_id)) {
120 return absl::FailedPreconditionError(absl::StrFormat(
121 "Entrance 0x%02X points at room 0x%04X, outside 0x000-0x127.",
122 entrance_id, target->room_id));
123 }
124 return *target;
125}
126
127std::string DoorEdgeTypeName(int direction) {
128 switch (direction) {
129 case 0:
130 return "door_north";
131 case 1:
132 return "door_south";
133 case 2:
134 return "door_west";
135 case 3:
136 return "door_east";
137 default:
138 return "door_unknown";
139 }
140}
141
142} // namespace
143
145 Rom* rom, const resources::ArgumentParser& parser,
146 resources::OutputFormatter& formatter) {
147 auto room_id_opt = parser.GetString("room");
148 auto dungeon_id_opt = parser.GetString("dungeon");
149
150 int room_filter = -1;
151 int dungeon_filter = -1;
152 if (room_id_opt.has_value()) {
153 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
154 return absl::InvalidArgumentError(
155 "Invalid room ID format. Must be hex (e.g., 0x07).");
156 }
157 if (!zelda3::IsDungeonRoomId(room_filter)) {
158 return absl::InvalidArgumentError(absl::StrFormat(
159 "Room ID 0x%X out of range (0x000-0x127).", room_filter));
160 }
161 }
162 if (dungeon_id_opt.has_value()) {
163 if (!ParseHexString(dungeon_id_opt.value(), &dungeon_filter)) {
164 return absl::InvalidArgumentError(
165 "Invalid dungeon ID format. Must be hex (e.g., 0x02).");
166 }
167 }
168
169 core::HackManifest manifest;
170 bool has_project = false;
171 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
172 if (!warp_tags.ok()) {
173 return warp_tags.status();
174 }
175
176 // --dungeon: rooms the census assigns to that dungeon ID (entrances from
177 // the table the game reads, flooded over the same links), not only rooms an
178 // entrance leads into directly.
179 std::vector<int> room_dungeon(zelda3::kDungeonRoomCount, -1);
180 if (dungeon_filter >= 0) {
182 options.manifest = has_project ? &manifest : nullptr;
183 auto census = zelda3::ComputeRoomCensus(rom, options);
184 if (!census.ok()) {
185 return census.status();
186 }
187 for (const auto& entry : census->rooms) {
188 if (entry.owner_index >= 0) {
189 room_dungeon[entry.room_id] =
190 census->owners[entry.owner_index].dungeon_id;
191 }
192 }
193 }
194
196 const auto lookup = cache.AsLookup();
197
198 struct RoomNode {
199 int room_id = 0;
200 std::array<int, 4> stairs{}; // Destination ids, before any range check.
201 int holewarp = 0;
202 bool has_connections = false;
203 };
204 std::vector<RoomNode> nodes;
205 std::vector<zelda3::RoomLink> edges;
206 std::vector<zelda3::RoomLink> out_of_range;
207 std::set<int> rooms_with_edges;
208
209 const int start_room = (room_filter >= 0) ? room_filter : 0;
210 const int end_room =
211 (room_filter >= 0) ? room_filter : zelda3::kDungeonRoomCount - 1;
212 for (int room_id = start_room; room_id <= end_room; ++room_id) {
213 if (dungeon_filter >= 0 && room_dungeon[room_id] != dungeon_filter) {
214 continue;
215 }
216 const zelda3::RoomLinkFacts* facts = cache.Get(room_id);
217 if (facts == nullptr) {
218 continue;
219 }
220 RoomNode node;
221 node.room_id = room_id;
222 for (int i = 0; i < 4; ++i) {
223 node.stairs[i] =
225 }
226 node.holewarp =
228 for (auto& link : zelda3::CollectRoomLinks(room_id, lookup, *warp_tags)) {
229 if (!UsesHeaderByte(link)) {
230 continue;
231 }
232 node.has_connections = true;
233 rooms_with_edges.insert(room_id);
234 if (link.to < 0) {
235 out_of_range.push_back(std::move(link));
236 continue;
237 }
238 rooms_with_edges.insert(link.to);
239 edges.push_back(std::move(link));
240 }
241 nodes.push_back(node);
242 }
243
244 RETURN_IF_ERROR(cache.status());
245 formatter.BeginObject("dungeon_graph");
246
247 // Nodes: all rooms when filtering by room, otherwise connected ones.
248 formatter.BeginArray("nodes");
249 for (const auto& node : nodes) {
250 if (room_filter >= 0 || node.has_connections ||
251 rooms_with_edges.count(node.room_id)) {
252 formatter.BeginObject();
253 formatter.AddField("room_id", RoomHex(node.room_id));
254 formatter.AddField("name", RoomName(node.room_id));
255 formatter.BeginArray("stairs");
256 for (int stair : node.stairs) {
257 formatter.AddArrayItem(RoomHex(stair));
258 }
259 formatter.EndArray();
260 formatter.AddField("holewarp", RoomHex(node.holewarp));
261 formatter.EndObject();
262 }
263 }
264 formatter.EndArray();
265
266 formatter.BeginArray("edges");
267 for (const auto& edge : edges) {
268 AddLinkObject(formatter, edge);
269 }
270 formatter.EndArray();
271 AddOutOfRange(formatter, out_of_range);
272
273 int stair_edges = 0;
274 int hole_edges = 0;
275 int strong_edges = 0;
276 for (const auto& edge : edges) {
277 (HeaderLinkType(edge) == "holewarp" ? hole_edges : stair_edges)++;
278 strong_edges += edge.strong ? 1 : 0;
279 }
280 formatter.BeginObject("stats");
281 formatter.AddField("total_rooms_scanned",
282 static_cast<int>(end_room - start_room + 1));
283 formatter.AddField("total_nodes", static_cast<int>(rooms_with_edges.size()));
284 formatter.AddField("total_edges", static_cast<int>(edges.size()));
285 formatter.AddField("staircase_connections", stair_edges);
286 formatter.AddField("holewarp_connections", hole_edges);
287 formatter.AddField("strong_edges", strong_edges);
288 formatter.AddField("out_of_range_edges",
289 static_cast<int>(out_of_range.size()));
290 formatter.EndObject();
291
292 formatter.EndObject();
293 return absl::OkStatus();
294}
295
297 Rom* rom, const resources::ArgumentParser& parser,
298 resources::OutputFormatter& formatter) {
299 auto entrance_id_str = parser.GetString("entrance").value();
300 bool is_spawn_point = parser.HasFlag("spawn");
301
302 int entrance_id;
303 if (!ParseHexString(entrance_id_str, &entrance_id)) {
304 return absl::InvalidArgumentError(
305 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
306 }
307
308 if (is_spawn_point) {
309 return WriteDungeonSpawnPointReport(rom, entrance_id, formatter,
310 "entrance");
311 }
312
313 // Validate entrance ID range
314 if (entrance_id < 0 || entrance_id > 0x84) {
315 return absl::InvalidArgumentError(absl::StrFormat(
316 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
317 }
318
319 zelda3::RoomEntrance entrance(rom, static_cast<uint8_t>(entrance_id), false);
320 RETURN_IF_ERROR(entrance.load_status());
321 // Room and dungeon ID from the table the game reads (ZScream's bank $0F
322 // copy when present), like dungeon-room-census.
323 const auto target = zelda3::ReadDungeonEntranceTarget(*rom, entrance_id);
324 if (!target.has_value()) {
325 return absl::OutOfRangeError(absl::StrFormat(
326 "Entrance 0x%02X table entry is outside the ROM.", entrance_id));
327 }
328
329 formatter.BeginObject("entrance");
330 formatter.AddField("entrance_id", absl::StrFormat("0x%02X", entrance_id));
331 formatter.AddField("is_spawn_point", is_spawn_point);
332 formatter.AddField("entrance_table", EntranceTableName(target->expanded));
333 formatter.AddField("room_id", absl::StrFormat("0x%03X", target->room_id));
334 formatter.AddField("room_id_full",
335 absl::StrFormat("0x%04X", target->room_id));
336 formatter.AddField("dungeon_id",
337 absl::StrFormat("0x%02X", target->dungeon_id));
338 if (target->expanded) {
339 // Keep the legacy copy as an explicit diagnostic only. Every active
340 // model field below comes from the same validated bank-$0F layout.
341 ASSIGN_OR_RETURN(auto legacy_room,
342 rom->ReadWord(zelda3::kEntranceRoom + entrance_id * 2));
343 formatter.AddField("vanilla_table_room_id",
344 absl::StrFormat("0x%03X", legacy_room));
345 formatter.AddField("other_fields_table", EntranceTableName(true));
346 }
347 formatter.AddField("exit_id", absl::StrFormat("0x%04X", entrance.exit_));
348
349 formatter.BeginObject("position");
350 formatter.AddField("x", entrance.x_position_);
351 formatter.AddField("y", entrance.y_position_);
352 formatter.EndObject();
353
354 formatter.BeginObject("camera");
355 formatter.AddField("x", entrance.camera_x_);
356 formatter.AddField("y", entrance.camera_y_);
357 formatter.AddField("trigger_x", entrance.camera_trigger_x_);
358 formatter.AddField("trigger_y", entrance.camera_trigger_y_);
359 formatter.EndObject();
360
361 formatter.BeginObject("properties");
362 formatter.AddField("blockset", absl::StrFormat("0x%02X", entrance.blockset_));
363 formatter.AddField("floor", absl::StrFormat("0x%02X", entrance.floor_));
364 formatter.AddField("door", absl::StrFormat("0x%02X", entrance.door_));
365 formatter.AddField("ladder_bg",
366 absl::StrFormat("0x%02X", entrance.ladder_bg_));
367 formatter.AddField("scrolling",
368 absl::StrFormat("0x%02X", entrance.scrolling_));
369 formatter.AddField("scroll_quadrant",
370 absl::StrFormat("0x%02X", entrance.scroll_quadrant_));
371 formatter.AddField("music", absl::StrFormat("0x%02X", entrance.music_));
372 formatter.EndObject();
373
374 formatter.BeginObject("camera_boundaries");
375 formatter.AddField("qn",
376 absl::StrFormat("0x%02X", entrance.camera_boundary_qn_));
377 formatter.AddField("fn",
378 absl::StrFormat("0x%02X", entrance.camera_boundary_fn_));
379 formatter.AddField("qs",
380 absl::StrFormat("0x%02X", entrance.camera_boundary_qs_));
381 formatter.AddField("fs",
382 absl::StrFormat("0x%02X", entrance.camera_boundary_fs_));
383 formatter.AddField("qw",
384 absl::StrFormat("0x%02X", entrance.camera_boundary_qw_));
385 formatter.AddField("fw",
386 absl::StrFormat("0x%02X", entrance.camera_boundary_fw_));
387 formatter.AddField("qe",
388 absl::StrFormat("0x%02X", entrance.camera_boundary_qe_));
389 formatter.AddField("fe",
390 absl::StrFormat("0x%02X", entrance.camera_boundary_fe_));
391 formatter.EndObject();
392
393 formatter.EndObject();
394
395 return absl::OkStatus();
396}
397
399 Rom* rom, const resources::ArgumentParser& parser,
400 resources::OutputFormatter& formatter) {
401 auto entrance_id_str = parser.GetString("entrance").value();
402 auto depth_opt = parser.GetString("depth");
403
404 int entrance_id;
405 if (!ParseHexString(entrance_id_str, &entrance_id)) {
406 return absl::InvalidArgumentError(
407 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
408 }
409 if (entrance_id < 0 || entrance_id >= zelda3::kDungeonEntranceCount) {
410 return absl::InvalidArgumentError(absl::StrFormat(
411 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
412 }
413
414 int max_depth = 20;
415 if (depth_opt.has_value()) {
416 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
417 return absl::InvalidArgumentError(
418 "Invalid depth format. Must be an integer between 1 and 100.");
419 }
420 if (max_depth < 1 || max_depth > 100) {
421 return absl::InvalidArgumentError("Depth must be between 1 and 100.");
422 }
423 }
424
425 core::HackManifest manifest;
426 bool has_project = false;
427 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
428 if (!warp_tags.ok()) {
429 return warp_tags.status();
430 }
431 auto start = EntranceStart(rom, entrance_id);
432 if (!start.ok()) {
433 return start.status();
434 }
435 const int start_room = start->room_id;
436
437 // BFS over stairs, warp tags, holes and teleport doors. Only links the
438 // game can take are followed; header bytes nothing uses are listed.
440 const auto lookup = cache.AsLookup();
441 std::set<int> discovered_rooms{start_room};
442 std::vector<zelda3::RoomLink> edges;
443 std::vector<zelda3::RoomLink> out_of_range;
444 std::queue<std::pair<int, int>> to_visit; // (room_id, depth)
445 to_visit.push({start_room, 0});
446 while (!to_visit.empty()) {
447 auto [current_room, current_depth] = to_visit.front();
448 to_visit.pop();
449 if (current_depth >= max_depth) {
450 continue;
451 }
452 for (auto& link :
453 zelda3::CollectRoomLinks(current_room, lookup, *warp_tags)) {
454 if (!UsesHeaderByte(link)) {
455 continue;
456 }
457 if (link.to < 0) {
458 out_of_range.push_back(std::move(link));
459 continue;
460 }
461 if (link.strong && discovered_rooms.insert(link.to).second) {
462 to_visit.push({link.to, current_depth + 1});
463 }
464 edges.push_back(std::move(link));
465 }
466 }
467
468 RETURN_IF_ERROR(cache.status());
469 formatter.BeginObject("discovery");
470 formatter.AddField("entrance_id", absl::StrFormat("0x%02X", entrance_id));
471 formatter.AddField("entrance_table", EntranceTableName(start->expanded));
472 formatter.AddField("start_room", RoomHex(start_room));
473 formatter.AddField("dungeon_id",
474 absl::StrFormat("0x%02X", start->dungeon_id));
475 formatter.AddField("max_depth", max_depth);
476 formatter.AddField("rooms_discovered",
477 static_cast<int>(discovered_rooms.size()));
478
479 formatter.BeginArray("discovered_rooms");
480 for (int room_id : discovered_rooms) {
481 formatter.BeginObject();
482 formatter.AddField("room_id", RoomHex(room_id));
483 formatter.AddField("name", RoomName(room_id));
484 formatter.EndObject();
485 }
486 formatter.EndArray();
487
488 formatter.BeginArray("connections");
489 for (const auto& edge : edges) {
490 AddLinkObject(formatter, edge);
491 }
492 formatter.EndArray();
493 AddOutOfRange(formatter, out_of_range);
494
495 formatter.EndObject();
496 return absl::OkStatus();
497}
498
500 Rom* rom, const resources::ArgumentParser& parser,
501 resources::OutputFormatter& formatter) {
502 auto entrance_id_str = parser.GetString("entrance").value();
503 auto depth_opt = parser.GetString("depth");
504
505 int entrance_id;
506 if (!ParseHexString(entrance_id_str, &entrance_id)) {
507 return absl::InvalidArgumentError(
508 "Invalid entrance ID format. Must be hex (e.g., 0x27).");
509 }
510 if (entrance_id < 0 || entrance_id >= zelda3::kDungeonEntranceCount) {
511 return absl::InvalidArgumentError(absl::StrFormat(
512 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
513 }
514
515 int max_depth = 50;
516 if (depth_opt.has_value()) {
517 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
518 return absl::InvalidArgumentError(
519 "Invalid depth format. Must be an integer between 1 and 200.");
520 }
521 if (max_depth < 1 || max_depth > 200) {
522 return absl::InvalidArgumentError("Depth must be between 1 and 200.");
523 }
524 }
525 const bool same_blockset_filter = parser.HasFlag("same-blockset");
526
527 core::HackManifest manifest;
528 bool has_project = false;
529 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
530 if (!warp_tags.ok()) {
531 return warp_tags.status();
532 }
533 auto start = EntranceStart(rom, entrance_id);
534 if (!start.ok()) {
535 return start.status();
536 }
537 const int start_room = start->room_id;
538
540 const auto lookup = cache.AsLookup();
541 const zelda3::RoomLinkFacts* start_facts = cache.Get(start_room);
542 const int start_blockset =
543 start_facts != nullptr ? start_facts->blockset : -1;
544
545 struct DoorEdge {
546 int from_room = -1;
547 std::string to; // Room id, or exit / stairs / same_room / none.
548 std::string type;
549 std::string door_type_name;
550 std::string role;
551 std::string kind;
552 std::string detail;
553 int tile_x = 0;
554 int tile_y = 0;
555 bool is_exit = false;
556 bool strong = false;
557 };
558
559 std::set<int> visited{start_room};
560 std::vector<DoorEdge> door_edges;
561 std::vector<zelda3::RoomLink> stair_edges;
562 std::vector<zelda3::RoomLink> out_of_range;
563 std::queue<std::pair<int, int>> to_visit; // (room_id, depth)
564 to_visit.push({start_room, 0});
565
566 while (!to_visit.empty()) {
567 auto [room_id, depth] = to_visit.front();
568 to_visit.pop();
569 if (depth >= max_depth) {
570 continue;
571 }
572 const zelda3::RoomLinkFacts* facts = cache.Get(room_id);
573 if (facts == nullptr) {
574 continue;
575 }
576 const auto links = zelda3::CollectRoomLinks(room_id, lookup, *warp_tags);
577 auto visit = [&, depth = depth](int target) {
578 if (visited.insert(target).second) {
579 to_visit.push({target, depth + 1});
580 }
581 };
582
583 // Door edges: every door except layer/dungeon swap markers. Exits and
584 // key-stair doors are listed but not followed (a key-stair door locks a
585 // stair object; the stair is in stair_edges).
586 for (size_t index = 0; index < facts->doors.size(); ++index) {
587 const auto& door = facts->doors[index];
588 const zelda3::RoomLink* link = nullptr;
589 for (const auto& candidate : links) {
590 if (candidate.door == static_cast<int>(index)) {
591 link = &candidate;
592 break;
593 }
594 }
595 DoorEdge edge;
596 edge.from_room = room_id;
597 edge.type = DoorEdgeTypeName(door.direction);
598 edge.door_type_name = std::string(
599 zelda3::GetDoorTypeName(static_cast<zelda3::DoorType>(door.type)));
600 edge.role = zelda3::RoomDoorRoleName(door.role);
601 edge.tile_x = door.tile_x;
602 edge.tile_y = door.tile_y;
603 edge.is_exit = door.role == zelda3::RoomDoorRole::kExit;
604 if (link != nullptr) {
605 edge.to = link->to >= 0 ? RoomHex(link->to) : "none";
606 edge.kind = zelda3::RoomReferenceKindName(link->kind);
607 edge.strong = link->strong;
608 edge.detail = link->detail;
609 } else if (edge.is_exit) {
610 edge.to = "exit";
611 } else if (door.role == zelda3::RoomDoorRole::kKeyStairs) {
612 edge.to = "stairs";
613 } else if (!door.outer) {
614 edge.to = "same_room"; // Middle wall: a quadrant change.
615 } else {
616 edge.to = "none"; // Would leave the room grid.
617 }
618 door_edges.push_back(edge);
619
620 if (link == nullptr || !link->strong || link->to < 0) {
621 continue;
622 }
623 // Optional: only follow grid-neighbor doors within the start blockset.
624 if (same_blockset_filter &&
626 const zelda3::RoomLinkFacts* next = cache.Get(link->to);
627 if (next == nullptr || next->blockset != start_blockset) {
628 continue;
629 }
630 }
631 visit(link->to);
632 }
633
634 // Stair, warp tag and holewarp edges (teleport doors are door edges).
635 for (const auto& link : links) {
636 if (!UsesHeaderByte(link) ||
638 continue;
639 }
640 if (link.to < 0) {
641 out_of_range.push_back(link);
642 continue;
643 }
644 stair_edges.push_back(link);
645 if (link.strong) {
646 visit(link.to);
647 }
648 }
649 }
650
651 RETURN_IF_ERROR(cache.status());
652 formatter.BeginObject("room_graph");
653 formatter.AddField("entrance_id", absl::StrFormat("0x%02X", entrance_id));
654 formatter.AddField("entrance_table", EntranceTableName(start->expanded));
655 formatter.AddField("start_room", RoomHex(start_room));
656 formatter.AddField("dungeon_id",
657 absl::StrFormat("0x%02X", start->dungeon_id));
658 formatter.AddField("rooms_discovered", static_cast<int>(visited.size()));
659
660 formatter.BeginArray("rooms");
661 for (int rid : visited) {
662 formatter.BeginObject();
663 formatter.AddField("room_id", RoomHex(rid));
664 formatter.AddField("name", RoomName(rid));
665 formatter.EndObject();
666 }
667 formatter.EndArray();
668
669 formatter.BeginArray("door_edges");
670 for (const auto& edge : door_edges) {
671 formatter.BeginObject();
672 formatter.AddField("from", RoomHex(edge.from_room));
673 formatter.AddField("to", edge.to);
674 formatter.AddField("type", edge.type);
675 formatter.AddField("door_type", edge.door_type_name);
676 formatter.AddField("role", edge.role);
677 formatter.AddField("tile_x", edge.tile_x);
678 formatter.AddField("tile_y", edge.tile_y);
679 formatter.AddField("is_exit", edge.is_exit);
680 formatter.AddField("strong", edge.strong);
681 if (!edge.kind.empty()) {
682 formatter.AddField("kind", edge.kind);
683 formatter.AddField("detail", edge.detail);
684 }
685 formatter.EndObject();
686 }
687 formatter.EndArray();
688
689 formatter.BeginArray("stair_edges");
690 for (const auto& edge : stair_edges) {
691 AddLinkObject(formatter, edge);
692 }
693 formatter.EndArray();
694 AddOutOfRange(formatter, out_of_range);
695
696 int exit_count = 0;
697 for (const auto& edge : door_edges) {
698 exit_count += edge.is_exit ? 1 : 0;
699 }
700 formatter.BeginObject("stats");
701 formatter.AddField("door_edges", static_cast<int>(door_edges.size()));
702 formatter.AddField("exit_doors", exit_count);
703 formatter.AddField("stair_edges", static_cast<int>(stair_edges.size()));
704 formatter.AddField("out_of_range_edges",
705 static_cast<int>(out_of_range.size()));
706 formatter.EndObject();
707
708 formatter.EndObject();
709 return absl::OkStatus();
710}
711
712} // namespace handlers
713} // namespace cli
714} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
Loads and queries the hack manifest JSON for yaze-ASM integration.
Dungeon Room Entrance or Spawn Point.
const absl::Status & load_status() const
const RoomLinkFacts * Get(int room_id)
const absl::Status & status() const
Definition room_links.h:213
RoomLinkFactsLookup AsLookup()
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void AddOutOfRange(resources::OutputFormatter &formatter, const std::vector< zelda3::RoomLink > &links)
absl::StatusOr< zelda3::DungeonEntranceTarget > EntranceStart(Rom *rom, int entrance_id)
absl::StatusOr< std::set< uint8_t > > WarpTagsFromProject(const resources::ArgumentParser &parser, core::HackManifest *manifest, bool *loaded)
void AddLinkObject(resources::OutputFormatter &formatter, const zelda3::RoomLink &link)
absl::Status WriteDungeonSpawnPointReport(Rom *rom, int spawn_id, resources::OutputFormatter &formatter, std::string_view object_title)
absl::Status LoadRoomCensusProject(const std::string &project_arg, core::HackManifest *manifest)
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
const char * RoomReferenceKindName(RoomReferenceKind kind)
DoorType
Door types from ALTTP.
Definition door_types.h:33
constexpr int ResolveHeaderDestinationRoom(int source_room_id, uint8_t header_byte)
std::set< uint8_t > RoomCensusWarpTagsFromManifest(const core::HackManifest &manifest)
constexpr int kEntranceRoom
std::optional< DungeonEntranceTarget > ReadDungeonEntranceTarget(const Rom &rom, int entrance_id)
absl::StatusOr< RoomCensus > ComputeRoomCensus(Rom *rom, const RoomCensusOptions &options)
bool IsDungeonRoomId(int room_id)
std::vector< RoomLink > CollectRoomLinks(int room_id, const RoomLinkFactsLookup &lookup, const std::set< uint8_t > &warp_tag_ids)
constexpr int kDungeonRoomCount
Definition room_links.h:51
const char * RoomDoorRoleName(RoomDoorRole role)
constexpr int kDungeonEntranceCount
Definition room_links.h:53
constexpr std::array< std::string_view, 297 > kRoomNames
Definition room.h:1478
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
const core::HackManifest * manifest
std::vector< Door > doors
Definition room_links.h:119
std::array< uint8_t, 4 > stair_bytes
Definition room_links.h:106