10#include "absl/strings/numbers.h"
11#include "absl/strings/str_format.h"
36 uint8_t staircase_rooms[4];
51 for (
int i = 0; i < 0x84; ++i) {
53 if (entrance.
room_ == room_id) {
66 neighbor = room_id - 0x10;
69 neighbor = room_id + 0x10;
72 neighbor = room_id - 0x01;
75 neighbor = room_id + 0x01;
105 for (
const auto& door : neighbor_room.
GetDoors()) {
123 return "door_unknown";
133 auto room_id_opt = parser.
GetString(
"room");
134 auto dungeon_id_opt = parser.
GetString(
"dungeon");
136 int room_filter = -1;
137 int dungeon_filter = -1;
139 if (room_id_opt.has_value()) {
140 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
141 return absl::InvalidArgumentError(
142 "Invalid room ID format. Must be hex (e.g., 0x07).");
146 if (dungeon_id_opt.has_value()) {
147 if (!ParseHexString(dungeon_id_opt.value(), &dungeon_filter)) {
148 return absl::InvalidArgumentError(
149 "Invalid dungeon ID format. Must be hex (e.g., 0x02).");
154 std::vector<RoomNode> nodes;
155 std::vector<RoomEdge> edges;
156 std::set<int> rooms_with_edges;
159 int start_room = (room_filter >= 0) ? room_filter : 0;
162 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
167 if (dungeon_filter >= 0) {
168 int room_dungeon = GetRoomDungeonId(rom, room_id);
169 if (room_dungeon != dungeon_filter) {
175 node.room_id = room_id;
177 if (room_id >= 0 && room_id < 297) {
180 node.name = absl::StrFormat(
"Room 0x%02X", room_id);
183 node.has_connections =
false;
186 for (
int i = 0; i < 4; ++i) {
190 if (node.staircase_rooms[i] != 0) {
192 edge.from_room = room_id;
193 edge.to_room = node.staircase_rooms[i];
197 edge.type = kEdgeTypeStair1;
200 edge.type = kEdgeTypeStair2;
203 edge.type = kEdgeTypeStair3;
206 edge.type = kEdgeTypeStair4;
210 edges.push_back(edge);
211 node.has_connections =
true;
212 rooms_with_edges.insert(room_id);
213 rooms_with_edges.insert(node.staircase_rooms[i]);
218 if (node.holewarp != 0) {
220 edge.from_room = room_id;
221 edge.to_room = node.holewarp;
222 edge.type = kEdgeTypeHolewarp;
223 edges.push_back(edge);
224 node.has_connections =
true;
225 rooms_with_edges.insert(room_id);
226 rooms_with_edges.insert(node.holewarp);
229 nodes.push_back(node);
237 for (
const auto& node : nodes) {
239 if (room_filter >= 0 || node.has_connections ||
240 rooms_with_edges.count(node.room_id)) {
242 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", node.room_id));
243 formatter.
AddField(
"name", node.name);
247 for (
int i = 0; i < 4; ++i) {
249 absl::StrFormat(
"0x%02X", node.staircase_rooms[i]));
253 formatter.
AddField(
"holewarp", absl::StrFormat(
"0x%02X", node.holewarp));
261 for (
const auto& edge : edges) {
263 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
264 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
265 formatter.
AddField(
"type", edge.type);
272 formatter.
AddField(
"total_rooms_scanned",
273 static_cast<int>(end_room - start_room + 1));
274 formatter.
AddField(
"total_nodes",
static_cast<int>(rooms_with_edges.size()));
275 formatter.
AddField(
"total_edges",
static_cast<int>(edges.size()));
280 for (
const auto& edge : edges) {
281 if (edge.type == kEdgeTypeHolewarp) {
287 formatter.
AddField(
"staircase_connections", stair_edges);
288 formatter.
AddField(
"holewarp_connections", hole_edges);
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));
322 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
323 formatter.
AddField(
"is_spawn_point", is_spawn_point);
325 absl::StrFormat(
"0x%02X", entrance.
room_ & 0xFF));
326 formatter.
AddField(
"room_id_full", absl::StrFormat(
"0x%04X", entrance.
room_));
329 formatter.
AddField(
"exit_id", absl::StrFormat(
"0x%04X", entrance.
exit_));
345 formatter.
AddField(
"floor", absl::StrFormat(
"0x%02X", entrance.
floor_));
346 formatter.
AddField(
"door", absl::StrFormat(
"0x%02X", entrance.
door_));
348 absl::StrFormat(
"0x%02X", entrance.
ladder_bg_));
350 absl::StrFormat(
"0x%02X", entrance.
scrolling_));
351 formatter.
AddField(
"scroll_quadrant",
353 formatter.
AddField(
"music", absl::StrFormat(
"0x%02X", entrance.
music_));
377 return absl::OkStatus();
383 auto entrance_id_str = parser.
GetString(
"entrance").value();
384 auto depth_opt = parser.
GetString(
"depth");
387 if (!ParseHexString(entrance_id_str, &entrance_id)) {
388 return absl::InvalidArgumentError(
389 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
393 if (entrance_id < 0 || entrance_id > 0x84) {
394 return absl::InvalidArgumentError(absl::StrFormat(
395 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
399 if (depth_opt.has_value()) {
400 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
401 return absl::InvalidArgumentError(
402 "Invalid depth format. Must be an integer between 1 and 100.");
404 if (max_depth < 1 || max_depth > 100) {
405 return absl::InvalidArgumentError(
"Depth must be between 1 and 100.");
411 int start_room = entrance.
room_ & 0xFF;
414 std::set<int> discovered_rooms;
415 std::vector<RoomEdge> edges;
416 std::queue<std::pair<int, int>> to_visit;
418 to_visit.push({start_room, 0});
419 discovered_rooms.insert(start_room);
421 while (!to_visit.empty()) {
422 auto [current_room, current_depth] = to_visit.front();
425 if (current_depth >= max_depth) {
433 for (
int i = 0; i < 4; ++i) {
435 if (dest != 0 && discovered_rooms.find(dest) == discovered_rooms.end()) {
436 discovered_rooms.insert(dest);
437 to_visit.push({dest, current_depth + 1});
441 edge.from_room = current_room;
443 edge.type = absl::StrFormat(
"stair%d", i + 1);
444 edges.push_back(edge);
450 discovered_rooms.find(room.
holewarp()) == discovered_rooms.end()) {
451 discovered_rooms.insert(room.
holewarp());
452 to_visit.push({room.
holewarp(), current_depth + 1});
456 edge.from_room = current_room;
458 edge.type =
"holewarp";
459 edges.push_back(edge);
465 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
466 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
469 formatter.
AddField(
"max_depth", max_depth);
470 formatter.
AddField(
"rooms_discovered",
471 static_cast<int>(discovered_rooms.size()));
475 std::vector<int> sorted_rooms(discovered_rooms.begin(),
476 discovered_rooms.end());
477 std::sort(sorted_rooms.begin(), sorted_rooms.end());
478 for (
int room_id : sorted_rooms) {
480 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", room_id));
482 if (room_id >= 0 && room_id < 297) {
485 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", room_id));
493 for (
const auto& edge : edges) {
495 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
496 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
497 formatter.
AddField(
"type", edge.type);
504 return absl::OkStatus();
510 auto entrance_id_str = parser.
GetString(
"entrance").value();
511 auto depth_opt = parser.
GetString(
"depth");
514 if (!ParseHexString(entrance_id_str, &entrance_id)) {
515 return absl::InvalidArgumentError(
516 "Invalid entrance ID format. Must be hex (e.g., 0x27).");
518 if (entrance_id < 0 || entrance_id > 0x84) {
519 return absl::InvalidArgumentError(absl::StrFormat(
520 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
524 if (depth_opt.has_value()) {
525 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
526 return absl::InvalidArgumentError(
527 "Invalid depth format. Must be an integer between 1 and 200.");
529 if (max_depth < 1 || max_depth > 200) {
530 return absl::InvalidArgumentError(
"Depth must be between 1 and 200.");
534 bool same_blockset_filter = parser.
HasFlag(
"same-blockset");
537 int start_room = entrance.
room_ & 0xFF;
540 uint8_t start_blockset = 0xFF;
541 if (same_blockset_filter) {
544 start_blockset = start_room_data.
blockset();
551 std::string door_type_name;
563 std::set<int> visited;
564 std::vector<DoorEdge> door_edges;
565 std::vector<StairEdge> stair_edges;
566 std::queue<std::pair<int, int>> to_visit;
568 to_visit.push({start_room, 0});
569 visited.insert(start_room);
571 while (!to_visit.empty()) {
572 auto [room_id, depth] = to_visit.front();
575 if (depth >= max_depth)
581 for (
const auto& door : room.
GetDoors()) {
584 if (!is_connection && !is_exit) {
590 is_connection ? NeighborRoomId(room_id, door.direction) : -1;
591 auto [tx, ty] = door.GetTileCoords();
594 edge.from_room = room_id;
595 edge.to_room = neighbor;
596 edge.type = DoorEdgeTypeName(door.direction);
597 edge.door_type_name = std::string(door.GetTypeName());
600 edge.is_exit = is_exit;
601 door_edges.push_back(edge);
605 if (is_connection && neighbor >= 0 &&
606 visited.find(neighbor) == visited.end() &&
607 RoomHasDoorIn(rom, neighbor, OppositeDir(door.direction))) {
609 if (same_blockset_filter) {
611 if (nbr.
blockset() != start_blockset)
614 visited.insert(neighbor);
615 to_visit.push({neighbor, depth + 1});
620 for (
int i = 0; i < 4; ++i) {
625 edge.from_room = room_id;
627 edge.type = absl::StrFormat(
"stair%d", i + 1);
628 stair_edges.push_back(edge);
629 if (visited.find(dest) == visited.end()) {
630 visited.insert(dest);
631 to_visit.push({dest, depth + 1});
639 edge.from_room = room_id;
641 edge.type =
"holewarp";
642 stair_edges.push_back(edge);
643 if (visited.find(hw) == visited.end()) {
645 to_visit.push({hw, depth + 1});
652 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
653 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
656 formatter.
AddField(
"rooms_discovered",
static_cast<int>(visited.size()));
659 std::vector<int> sorted_rooms(visited.begin(), visited.end());
660 std::sort(sorted_rooms.begin(), sorted_rooms.end());
661 for (
int rid : sorted_rooms) {
663 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", rid));
664 if (rid >= 0 && rid < 297) {
667 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", rid));
675 for (
const auto& edge : door_edges) {
677 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
678 if (edge.is_exit || edge.to_room < 0) {
681 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
683 formatter.
AddField(
"type", edge.type);
684 formatter.
AddField(
"door_type", edge.door_type_name);
685 formatter.
AddField(
"tile_x", edge.tile_x);
686 formatter.
AddField(
"tile_y", edge.tile_y);
687 formatter.
AddField(
"is_exit", edge.is_exit);
694 for (
const auto& edge : stair_edges) {
696 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
697 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
698 formatter.
AddField(
"type", edge.type);
704 for (
const auto& edge : door_edges) {
709 formatter.
AddField(
"door_edges",
static_cast<int>(door_edges.size()));
710 formatter.
AddField(
"exit_doors", exit_count);
711 formatter.
AddField(
"stair_edges",
static_cast<int>(stair_edges.size()));
715 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::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.
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_
uint8_t staircase_room(int index) const
const std::vector< Door > & GetDoors() const
constexpr const char * kEdgeTypeStair3
constexpr const char * kEdgeTypeStair4
constexpr const char * kEdgeTypeHolewarp
std::string DoorEdgeTypeName(zelda3::DoorDirection dir)
constexpr const char * kEdgeTypeStair1
constexpr const char * kEdgeTypeStair2
bool RoomHasDoorIn(Rom *rom, int room_id, zelda3::DoorDirection dir)
int GetRoomDungeonId(Rom *rom, int room_id)
absl::Status WriteDungeonSpawnPointReport(Rom *rom, int spawn_id, resources::OutputFormatter &formatter, std::string_view object_title)
bool ParseHexString(absl::string_view str, uint64_t *out)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
Room LoadRoomFromRom(Rom *rom, int room_id)
constexpr bool IsExitDoorType(DoorType type)
Return true for terminal exits that do not pair with another room.
constexpr std::array< std::string_view, 297 > kRoomNames
constexpr int kNumberOfRooms
DoorDirection
Door direction on room walls.
@ 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)