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) {
84 neighbor = room_id - 0x10;
87 neighbor = room_id + 0x10;
90 neighbor = room_id - 0x01;
93 neighbor = room_id + 0x01;
123 for (
const auto& door : neighbor_room.
GetDoors()) {
124 if (door.direction == dir && !IsExitDoorType(door.type))
141 return "door_unknown";
151 auto room_id_opt = parser.
GetString(
"room");
152 auto dungeon_id_opt = parser.
GetString(
"dungeon");
154 int room_filter = -1;
155 int dungeon_filter = -1;
157 if (room_id_opt.has_value()) {
158 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
159 return absl::InvalidArgumentError(
160 "Invalid room ID format. Must be hex (e.g., 0x07).");
164 if (dungeon_id_opt.has_value()) {
165 if (!ParseHexString(dungeon_id_opt.value(), &dungeon_filter)) {
166 return absl::InvalidArgumentError(
167 "Invalid dungeon ID format. Must be hex (e.g., 0x02).");
172 std::vector<RoomNode> nodes;
173 std::vector<RoomEdge> edges;
174 std::set<int> rooms_with_edges;
177 int start_room = (room_filter >= 0) ? room_filter : 0;
180 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
185 if (dungeon_filter >= 0) {
186 int room_dungeon = GetRoomDungeonId(rom, room_id);
187 if (room_dungeon != dungeon_filter) {
193 node.room_id = room_id;
195 if (room_id >= 0 && room_id < 297) {
198 node.name = absl::StrFormat(
"Room 0x%02X", room_id);
201 node.has_connections =
false;
204 for (
int i = 0; i < 4; ++i) {
208 if (node.staircase_rooms[i] != 0) {
210 edge.from_room = room_id;
211 edge.to_room = node.staircase_rooms[i];
215 edge.type = kEdgeTypeStair1;
218 edge.type = kEdgeTypeStair2;
221 edge.type = kEdgeTypeStair3;
224 edge.type = kEdgeTypeStair4;
228 edges.push_back(edge);
229 node.has_connections =
true;
230 rooms_with_edges.insert(room_id);
231 rooms_with_edges.insert(node.staircase_rooms[i]);
236 if (node.holewarp != 0) {
238 edge.from_room = room_id;
239 edge.to_room = node.holewarp;
240 edge.type = kEdgeTypeHolewarp;
241 edges.push_back(edge);
242 node.has_connections =
true;
243 rooms_with_edges.insert(room_id);
244 rooms_with_edges.insert(node.holewarp);
247 nodes.push_back(node);
255 for (
const auto& node : nodes) {
257 if (room_filter >= 0 || node.has_connections ||
258 rooms_with_edges.count(node.room_id)) {
260 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", node.room_id));
261 formatter.
AddField(
"name", node.name);
265 for (
int i = 0; i < 4; ++i) {
267 absl::StrFormat(
"0x%02X", node.staircase_rooms[i]));
271 formatter.
AddField(
"holewarp", absl::StrFormat(
"0x%02X", node.holewarp));
279 for (
const auto& edge : edges) {
281 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
282 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
283 formatter.
AddField(
"type", edge.type);
290 formatter.
AddField(
"total_rooms_scanned",
291 static_cast<int>(end_room - start_room + 1));
292 formatter.
AddField(
"total_nodes",
static_cast<int>(rooms_with_edges.size()));
293 formatter.
AddField(
"total_edges",
static_cast<int>(edges.size()));
298 for (
const auto& edge : edges) {
299 if (edge.type == kEdgeTypeHolewarp) {
305 formatter.
AddField(
"staircase_connections", stair_edges);
306 formatter.
AddField(
"holewarp_connections", hole_edges);
311 return absl::OkStatus();
317 auto entrance_id_str = parser.
GetString(
"entrance").value();
318 bool is_spawn_point = parser.
HasFlag(
"spawn");
321 if (!ParseHexString(entrance_id_str, &entrance_id)) {
322 return absl::InvalidArgumentError(
323 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
326 if (is_spawn_point) {
332 if (entrance_id < 0 || entrance_id > 0x84) {
333 return absl::InvalidArgumentError(absl::StrFormat(
334 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
340 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
341 formatter.
AddField(
"is_spawn_point", is_spawn_point);
343 absl::StrFormat(
"0x%02X", entrance.
room_ & 0xFF));
344 formatter.
AddField(
"room_id_full", absl::StrFormat(
"0x%04X", entrance.
room_));
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).");
411 if (entrance_id < 0 || entrance_id > 0x84) {
412 return absl::InvalidArgumentError(absl::StrFormat(
413 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
417 if (depth_opt.has_value()) {
418 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
419 return absl::InvalidArgumentError(
420 "Invalid depth format. Must be an integer between 1 and 100.");
422 if (max_depth < 1 || max_depth > 100) {
423 return absl::InvalidArgumentError(
"Depth must be between 1 and 100.");
429 int start_room = entrance.
room_ & 0xFF;
432 std::set<int> discovered_rooms;
433 std::vector<RoomEdge> edges;
434 std::queue<std::pair<int, int>> to_visit;
436 to_visit.push({start_room, 0});
437 discovered_rooms.insert(start_room);
439 while (!to_visit.empty()) {
440 auto [current_room, current_depth] = to_visit.front();
443 if (current_depth >= max_depth) {
451 for (
int i = 0; i < 4; ++i) {
453 if (dest != 0 && discovered_rooms.find(dest) == discovered_rooms.end()) {
454 discovered_rooms.insert(dest);
455 to_visit.push({dest, current_depth + 1});
459 edge.from_room = current_room;
461 edge.type = absl::StrFormat(
"stair%d", i + 1);
462 edges.push_back(edge);
468 discovered_rooms.find(room.
holewarp()) == discovered_rooms.end()) {
469 discovered_rooms.insert(room.
holewarp());
470 to_visit.push({room.
holewarp(), current_depth + 1});
474 edge.from_room = current_room;
476 edge.type =
"holewarp";
477 edges.push_back(edge);
483 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
484 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
487 formatter.
AddField(
"max_depth", max_depth);
488 formatter.
AddField(
"rooms_discovered",
489 static_cast<int>(discovered_rooms.size()));
493 std::vector<int> sorted_rooms(discovered_rooms.begin(),
494 discovered_rooms.end());
495 std::sort(sorted_rooms.begin(), sorted_rooms.end());
496 for (
int room_id : sorted_rooms) {
498 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", room_id));
500 if (room_id >= 0 && room_id < 297) {
503 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", room_id));
511 for (
const auto& edge : edges) {
513 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
514 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
515 formatter.
AddField(
"type", edge.type);
522 return absl::OkStatus();
528 auto entrance_id_str = parser.
GetString(
"entrance").value();
529 auto depth_opt = parser.
GetString(
"depth");
532 if (!ParseHexString(entrance_id_str, &entrance_id)) {
533 return absl::InvalidArgumentError(
534 "Invalid entrance ID format. Must be hex (e.g., 0x27).");
536 if (entrance_id < 0 || entrance_id > 0x84) {
537 return absl::InvalidArgumentError(absl::StrFormat(
538 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
542 if (depth_opt.has_value()) {
543 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
544 return absl::InvalidArgumentError(
545 "Invalid depth format. Must be an integer between 1 and 200.");
547 if (max_depth < 1 || max_depth > 200) {
548 return absl::InvalidArgumentError(
"Depth must be between 1 and 200.");
552 bool same_blockset_filter = parser.
HasFlag(
"same-blockset");
555 int start_room = entrance.
room_ & 0xFF;
558 uint8_t start_blockset = 0xFF;
559 if (same_blockset_filter) {
562 start_blockset = start_room_data.
blockset();
569 std::string door_type_name;
581 std::set<int> visited;
582 std::vector<DoorEdge> door_edges;
583 std::vector<StairEdge> stair_edges;
584 std::queue<std::pair<int, int>> to_visit;
586 to_visit.push({start_room, 0});
587 visited.insert(start_room);
589 while (!to_visit.empty()) {
590 auto [room_id, depth] = to_visit.front();
593 if (depth >= max_depth)
599 for (
const auto& door : room.
GetDoors()) {
600 bool is_exit = IsExitDoorType(door.type);
601 int neighbor = is_exit ? -1 : NeighborRoomId(room_id, door.direction);
602 auto [tx, ty] = door.GetTileCoords();
605 edge.from_room = room_id;
606 edge.to_room = neighbor;
607 edge.type = DoorEdgeTypeName(door.direction);
608 edge.door_type_name = std::string(door.GetTypeName());
611 edge.is_exit = is_exit;
612 door_edges.push_back(edge);
616 if (!is_exit && neighbor >= 0 &&
617 visited.find(neighbor) == visited.end() &&
618 RoomHasDoorIn(rom, neighbor, OppositeDir(door.direction))) {
620 if (same_blockset_filter) {
622 if (nbr.
blockset() != start_blockset)
625 visited.insert(neighbor);
626 to_visit.push({neighbor, depth + 1});
631 for (
int i = 0; i < 4; ++i) {
636 edge.from_room = room_id;
638 edge.type = absl::StrFormat(
"stair%d", i + 1);
639 stair_edges.push_back(edge);
640 if (visited.find(dest) == visited.end()) {
641 visited.insert(dest);
642 to_visit.push({dest, depth + 1});
650 edge.from_room = room_id;
652 edge.type =
"holewarp";
653 stair_edges.push_back(edge);
654 if (visited.find(hw) == visited.end()) {
656 to_visit.push({hw, depth + 1});
663 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
664 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
667 formatter.
AddField(
"rooms_discovered",
static_cast<int>(visited.size()));
670 std::vector<int> sorted_rooms(visited.begin(), visited.end());
671 std::sort(sorted_rooms.begin(), sorted_rooms.end());
672 for (
int rid : sorted_rooms) {
674 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", rid));
675 if (rid >= 0 && rid < 297) {
678 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", rid));
686 for (
const auto& edge : door_edges) {
688 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
689 if (edge.is_exit || edge.to_room < 0) {
692 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
694 formatter.
AddField(
"type", edge.type);
695 formatter.
AddField(
"door_type", edge.door_type_name);
696 formatter.
AddField(
"tile_x", edge.tile_x);
697 formatter.
AddField(
"tile_y", edge.tile_y);
698 formatter.
AddField(
"is_exit", edge.is_exit);
705 for (
const auto& edge : stair_edges) {
707 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
708 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
709 formatter.
AddField(
"type", edge.type);
715 for (
const auto& edge : door_edges) {
720 formatter.
AddField(
"door_edges",
static_cast<int>(door_edges.size()));
721 formatter.
AddField(
"exit_doors", exit_count);
722 formatter.
AddField(
"stair_edges",
static_cast<int>(stair_edges.size()));
726 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, int *out)
DoorType
Door types from ALTTP.
@ FancyDungeonExitLower
Fancy dungeon exit (lower layer)
@ FancyDungeonExit
Fancy dungeon exit.
@ ExitLower
Exit (lower layer)
@ BombableCaveExit
Bombable cave exit.
@ UnusedCaveExit
Unused cave exit (lower layer)
@ LitCaveExitLower
Lit cave exit (lower layer)
@ WaterfallDoor
Waterfall door.
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
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)