12#include "absl/status/statusor.h"
13#include "absl/strings/numbers.h"
14#include "absl/strings/str_format.h"
41 return absl::StrFormat(
"0x%03X", room_id);
48 return absl::StrFormat(
"Room 0x%03X", room_id);
52 return expanded ?
"zscream_bank_0f" :
"vanilla_bank_02";
64 return "teleport_door";
67 return absl::StrFormat(
"stair%d", link.
slot + 1);
87 const std::vector<zelda3::RoomLink>& links) {
89 for (
const auto& link : links) {
101 auto project = parser.
GetString(
"project");
102 if (!project.has_value()) {
103 return std::set<uint8_t>{};
115 if (!target.has_value()) {
116 return absl::OutOfRangeError(absl::StrFormat(
117 "Entrance 0x%02X table entry is outside the ROM.", entrance_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));
138 return "door_unknown";
147 auto room_id_opt = parser.
GetString(
"room");
148 auto dungeon_id_opt = parser.
GetString(
"dungeon");
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).");
158 return absl::InvalidArgumentError(absl::StrFormat(
159 "Room ID 0x%X out of range (0x000-0x127).", room_filter));
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).");
170 bool has_project =
false;
171 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
172 if (!warp_tags.ok()) {
173 return warp_tags.status();
180 if (dungeon_filter >= 0) {
182 options.
manifest = has_project ? &manifest :
nullptr;
185 return census.status();
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;
196 const auto lookup = cache.
AsLookup();
200 std::array<int, 4> stairs{};
202 bool has_connections =
false;
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;
209 const int start_room = (room_filter >= 0) ? room_filter : 0;
212 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
213 if (dungeon_filter >= 0 && room_dungeon[room_id] != dungeon_filter) {
217 if (facts ==
nullptr) {
221 node.room_id = room_id;
222 for (
int i = 0; i < 4; ++i) {
229 if (!UsesHeaderByte(link)) {
232 node.has_connections =
true;
233 rooms_with_edges.insert(room_id);
235 out_of_range.push_back(std::move(link));
238 rooms_with_edges.insert(link.to);
239 edges.push_back(std::move(link));
241 nodes.push_back(node);
249 for (
const auto& node : nodes) {
250 if (room_filter >= 0 || node.has_connections ||
251 rooms_with_edges.count(node.room_id)) {
253 formatter.
AddField(
"room_id", RoomHex(node.room_id));
254 formatter.
AddField(
"name", RoomName(node.room_id));
256 for (
int stair : node.stairs) {
260 formatter.
AddField(
"holewarp", RoomHex(node.holewarp));
267 for (
const auto& edge : edges) {
268 AddLinkObject(formatter, edge);
271 AddOutOfRange(formatter, out_of_range);
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;
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()));
293 return absl::OkStatus();
299 auto entrance_id_str = parser.
GetString(
"entrance").value();
300 bool is_spawn_point = parser.
HasFlag(
"spawn");
303 if (!ParseHexString(entrance_id_str, &entrance_id)) {
304 return absl::InvalidArgumentError(
305 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
308 if (is_spawn_point) {
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));
324 if (!target.has_value()) {
325 return absl::OutOfRangeError(absl::StrFormat(
326 "Entrance 0x%02X table entry is outside the ROM.", entrance_id));
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));
335 absl::StrFormat(
"0x%04X", target->room_id));
337 absl::StrFormat(
"0x%02X", target->dungeon_id));
338 if (target->expanded) {
343 formatter.
AddField(
"vanilla_table_room_id",
344 absl::StrFormat(
"0x%03X", legacy_room));
345 formatter.
AddField(
"other_fields_table", EntranceTableName(
true));
347 formatter.
AddField(
"exit_id", absl::StrFormat(
"0x%04X", entrance.
exit_));
363 formatter.
AddField(
"floor", absl::StrFormat(
"0x%02X", entrance.
floor_));
364 formatter.
AddField(
"door", absl::StrFormat(
"0x%02X", entrance.
door_));
366 absl::StrFormat(
"0x%02X", entrance.
ladder_bg_));
368 absl::StrFormat(
"0x%02X", entrance.
scrolling_));
369 formatter.
AddField(
"scroll_quadrant",
371 formatter.
AddField(
"music", absl::StrFormat(
"0x%02X", entrance.
music_));
395 return absl::OkStatus();
401 auto entrance_id_str = parser.
GetString(
"entrance").value();
402 auto depth_opt = parser.
GetString(
"depth");
405 if (!ParseHexString(entrance_id_str, &entrance_id)) {
406 return absl::InvalidArgumentError(
407 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
410 return absl::InvalidArgumentError(absl::StrFormat(
411 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
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.");
420 if (max_depth < 1 || max_depth > 100) {
421 return absl::InvalidArgumentError(
"Depth must be between 1 and 100.");
426 bool has_project =
false;
427 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
428 if (!warp_tags.ok()) {
429 return warp_tags.status();
431 auto start = EntranceStart(rom, entrance_id);
433 return start.status();
435 const int start_room = start->room_id;
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;
445 to_visit.push({start_room, 0});
446 while (!to_visit.empty()) {
447 auto [current_room, current_depth] = to_visit.front();
449 if (current_depth >= max_depth) {
454 if (!UsesHeaderByte(link)) {
458 out_of_range.push_back(std::move(link));
461 if (link.strong && discovered_rooms.insert(link.to).second) {
462 to_visit.push({link.to, current_depth + 1});
464 edges.push_back(std::move(link));
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));
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()));
480 for (
int room_id : discovered_rooms) {
482 formatter.
AddField(
"room_id", RoomHex(room_id));
483 formatter.
AddField(
"name", RoomName(room_id));
489 for (
const auto& edge : edges) {
490 AddLinkObject(formatter, edge);
493 AddOutOfRange(formatter, out_of_range);
496 return absl::OkStatus();
502 auto entrance_id_str = parser.
GetString(
"entrance").value();
503 auto depth_opt = parser.
GetString(
"depth");
506 if (!ParseHexString(entrance_id_str, &entrance_id)) {
507 return absl::InvalidArgumentError(
508 "Invalid entrance ID format. Must be hex (e.g., 0x27).");
511 return absl::InvalidArgumentError(absl::StrFormat(
512 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
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.");
521 if (max_depth < 1 || max_depth > 200) {
522 return absl::InvalidArgumentError(
"Depth must be between 1 and 200.");
525 const bool same_blockset_filter = parser.
HasFlag(
"same-blockset");
528 bool has_project =
false;
529 auto warp_tags = WarpTagsFromProject(parser, &manifest, &has_project);
530 if (!warp_tags.ok()) {
531 return warp_tags.status();
533 auto start = EntranceStart(rom, entrance_id);
535 return start.status();
537 const int start_room = start->room_id;
540 const auto lookup = cache.
AsLookup();
542 const int start_blockset =
543 start_facts !=
nullptr ? start_facts->
blockset : -1;
549 std::string door_type_name;
555 bool is_exit =
false;
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;
564 to_visit.push({start_room, 0});
566 while (!to_visit.empty()) {
567 auto [room_id, depth] = to_visit.front();
569 if (depth >= max_depth) {
573 if (facts ==
nullptr) {
577 auto visit = [&, depth = depth](
int target) {
578 if (visited.insert(target).second) {
579 to_visit.push({target, depth + 1});
586 for (
size_t index = 0; index < facts->
doors.size(); ++index) {
587 const auto& door = facts->
doors[index];
589 for (
const auto& candidate : links) {
590 if (candidate.door ==
static_cast<int>(index)) {
596 edge.from_room = room_id;
597 edge.type = DoorEdgeTypeName(door.direction);
598 edge.door_type_name = std::string(
601 edge.tile_x = door.tile_x;
602 edge.tile_y = door.tile_y;
604 if (link !=
nullptr) {
605 edge.to = link->
to >= 0 ? RoomHex(link->
to) :
"none";
607 edge.strong = link->
strong;
608 edge.detail = link->
detail;
609 }
else if (edge.is_exit) {
613 }
else if (!door.outer) {
614 edge.to =
"same_room";
618 door_edges.push_back(edge);
620 if (link ==
nullptr || !link->
strong || link->
to < 0) {
624 if (same_blockset_filter &&
627 if (next ==
nullptr || next->
blockset != start_blockset) {
635 for (
const auto& link : links) {
636 if (!UsesHeaderByte(link) ||
641 out_of_range.push_back(link);
644 stair_edges.push_back(link);
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));
657 absl::StrFormat(
"0x%02X", start->dungeon_id));
658 formatter.
AddField(
"rooms_discovered",
static_cast<int>(visited.size()));
661 for (
int rid : visited) {
663 formatter.
AddField(
"room_id", RoomHex(rid));
664 formatter.
AddField(
"name", RoomName(rid));
670 for (
const auto& edge : door_edges) {
672 formatter.
AddField(
"from", RoomHex(edge.from_room));
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);
690 for (
const auto& edge : stair_edges) {
691 AddLinkObject(formatter, edge);
694 AddOutOfRange(formatter, out_of_range);
697 for (
const auto& edge : door_edges) {
698 exit_count += edge.is_exit ? 1 : 0;
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()));
709 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< uint16_t > ReadWord(int offset) const
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.
Loads and queries the hack manifest JSON for yaze-ASM integration.
Dungeon Room Entrance or Spawn Point.
uint8_t camera_boundary_fw_
uint8_t camera_boundary_qe_
uint8_t camera_boundary_fe_
uint8_t camera_boundary_fs_
uint8_t camera_boundary_qs_
uint8_t camera_boundary_qw_
uint16_t camera_trigger_y_
uint8_t camera_boundary_fn_
uint16_t camera_trigger_x_
uint8_t camera_boundary_qn_
const absl::Status & load_status() const
const RoomLinkFacts * Get(int room_id)
const absl::Status & status() const
RoomLinkFactsLookup AsLookup()
#define ASSIGN_OR_RETURN(type_variable_name, expression)
const char * EntranceTableName(bool expanded)
std::string RoomHex(int room_id)
std::string DoorEdgeTypeName(int direction)
std::string RoomName(int room_id)
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)
std::string HeaderLinkType(const zelda3::RoomLink &link)
bool UsesHeaderByte(const zelda3::RoomLink &link)
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)
const char * RoomReferenceKindName(RoomReferenceKind kind)
DoorType
Door types from ALTTP.
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
const char * RoomDoorRoleName(RoomDoorRole role)
constexpr int kDungeonEntranceCount
constexpr std::array< std::string_view, 297 > kRoomNames
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
#define RETURN_IF_ERROR(expr)
const core::HackManifest * manifest
std::vector< Door > doors
std::array< uint8_t, 4 > stair_bytes