10#include "absl/strings/numbers.h"
11#include "absl/strings/str_format.h"
35 uint8_t staircase_rooms[4];
50 for (
int i = 0; i < 0x84; ++i) {
52 if (entrance.
room_ == room_id) {
83 neighbor = room_id - 0x10;
86 neighbor = room_id + 0x10;
89 neighbor = room_id - 0x01;
92 neighbor = room_id + 0x01;
122 for (
const auto& door : neighbor_room.
GetDoors()) {
140 return "door_unknown";
150 auto room_id_opt = parser.
GetString(
"room");
151 auto dungeon_id_opt = parser.
GetString(
"dungeon");
153 int room_filter = -1;
154 int dungeon_filter = -1;
156 if (room_id_opt.has_value()) {
157 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
158 return absl::InvalidArgumentError(
159 "Invalid room ID format. Must be hex (e.g., 0x07).");
163 if (dungeon_id_opt.has_value()) {
164 if (!ParseHexString(dungeon_id_opt.value(), &dungeon_filter)) {
165 return absl::InvalidArgumentError(
166 "Invalid dungeon ID format. Must be hex (e.g., 0x02).");
171 std::vector<RoomNode> nodes;
172 std::vector<RoomEdge> edges;
173 std::set<int> rooms_with_edges;
176 int start_room = (room_filter >= 0) ? room_filter : 0;
179 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
184 if (dungeon_filter >= 0) {
185 int room_dungeon = GetRoomDungeonId(rom, room_id);
186 if (room_dungeon != dungeon_filter) {
192 node.room_id = room_id;
194 if (room_id >= 0 && room_id < 297) {
197 node.name = absl::StrFormat(
"Room 0x%02X", room_id);
200 node.has_connections =
false;
203 for (
int i = 0; i < 4; ++i) {
207 if (node.staircase_rooms[i] != 0) {
209 edge.from_room = room_id;
210 edge.to_room = node.staircase_rooms[i];
214 edge.type = kEdgeTypeStair1;
217 edge.type = kEdgeTypeStair2;
220 edge.type = kEdgeTypeStair3;
223 edge.type = kEdgeTypeStair4;
227 edges.push_back(edge);
228 node.has_connections =
true;
229 rooms_with_edges.insert(room_id);
230 rooms_with_edges.insert(node.staircase_rooms[i]);
235 if (node.holewarp != 0) {
237 edge.from_room = room_id;
238 edge.to_room = node.holewarp;
239 edge.type = kEdgeTypeHolewarp;
240 edges.push_back(edge);
241 node.has_connections =
true;
242 rooms_with_edges.insert(room_id);
243 rooms_with_edges.insert(node.holewarp);
246 nodes.push_back(node);
254 for (
const auto& node : nodes) {
256 if (room_filter >= 0 || node.has_connections ||
257 rooms_with_edges.count(node.room_id)) {
259 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", node.room_id));
260 formatter.
AddField(
"name", node.name);
264 for (
int i = 0; i < 4; ++i) {
266 absl::StrFormat(
"0x%02X", node.staircase_rooms[i]));
270 formatter.
AddField(
"holewarp", absl::StrFormat(
"0x%02X", node.holewarp));
278 for (
const auto& edge : edges) {
280 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
281 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
282 formatter.
AddField(
"type", edge.type);
289 formatter.
AddField(
"total_rooms_scanned",
290 static_cast<int>(end_room - start_room + 1));
291 formatter.
AddField(
"total_nodes",
static_cast<int>(rooms_with_edges.size()));
292 formatter.
AddField(
"total_edges",
static_cast<int>(edges.size()));
297 for (
const auto& edge : edges) {
298 if (edge.type == kEdgeTypeHolewarp) {
304 formatter.
AddField(
"staircase_connections", stair_edges);
305 formatter.
AddField(
"holewarp_connections", hole_edges);
310 return absl::OkStatus();
316 auto entrance_id_str = parser.
GetString(
"entrance").value();
317 bool is_spawn_point = parser.
HasFlag(
"spawn");
320 if (!ParseHexString(entrance_id_str, &entrance_id)) {
321 return absl::InvalidArgumentError(
322 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
326 if (entrance_id < 0 || entrance_id > 0x84) {
327 return absl::InvalidArgumentError(absl::StrFormat(
328 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
335 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
336 formatter.
AddField(
"is_spawn_point", is_spawn_point);
338 absl::StrFormat(
"0x%02X", entrance.
room_ & 0xFF));
339 formatter.
AddField(
"room_id_full", absl::StrFormat(
"0x%04X", entrance.
room_));
342 formatter.
AddField(
"exit_id", absl::StrFormat(
"0x%04X", entrance.
exit_));
358 formatter.
AddField(
"floor", absl::StrFormat(
"0x%02X", entrance.
floor_));
359 formatter.
AddField(
"door", absl::StrFormat(
"0x%02X", entrance.
door_));
361 absl::StrFormat(
"0x%02X", entrance.
ladder_bg_));
363 absl::StrFormat(
"0x%02X", entrance.
scrolling_));
364 formatter.
AddField(
"scroll_quadrant",
366 formatter.
AddField(
"music", absl::StrFormat(
"0x%02X", entrance.
music_));
390 return absl::OkStatus();
396 auto entrance_id_str = parser.
GetString(
"entrance").value();
397 auto depth_opt = parser.
GetString(
"depth");
400 if (!ParseHexString(entrance_id_str, &entrance_id)) {
401 return absl::InvalidArgumentError(
402 "Invalid entrance ID format. Must be hex (e.g., 0x08).");
406 if (entrance_id < 0 || entrance_id > 0x84) {
407 return absl::InvalidArgumentError(absl::StrFormat(
408 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
412 if (depth_opt.has_value()) {
413 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
414 return absl::InvalidArgumentError(
415 "Invalid depth format. Must be an integer between 1 and 100.");
417 if (max_depth < 1 || max_depth > 100) {
418 return absl::InvalidArgumentError(
"Depth must be between 1 and 100.");
424 int start_room = entrance.
room_ & 0xFF;
427 std::set<int> discovered_rooms;
428 std::vector<RoomEdge> edges;
429 std::queue<std::pair<int, int>> to_visit;
431 to_visit.push({start_room, 0});
432 discovered_rooms.insert(start_room);
434 while (!to_visit.empty()) {
435 auto [current_room, current_depth] = to_visit.front();
438 if (current_depth >= max_depth) {
446 for (
int i = 0; i < 4; ++i) {
448 if (dest != 0 && discovered_rooms.find(dest) == discovered_rooms.end()) {
449 discovered_rooms.insert(dest);
450 to_visit.push({dest, current_depth + 1});
454 edge.from_room = current_room;
456 edge.type = absl::StrFormat(
"stair%d", i + 1);
457 edges.push_back(edge);
463 discovered_rooms.find(room.
holewarp()) == discovered_rooms.end()) {
464 discovered_rooms.insert(room.
holewarp());
465 to_visit.push({room.
holewarp(), current_depth + 1});
469 edge.from_room = current_room;
471 edge.type =
"holewarp";
472 edges.push_back(edge);
478 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
479 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
482 formatter.
AddField(
"max_depth", max_depth);
483 formatter.
AddField(
"rooms_discovered",
484 static_cast<int>(discovered_rooms.size()));
488 std::vector<int> sorted_rooms(discovered_rooms.begin(),
489 discovered_rooms.end());
490 std::sort(sorted_rooms.begin(), sorted_rooms.end());
491 for (
int room_id : sorted_rooms) {
493 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", room_id));
495 if (room_id >= 0 && room_id < 297) {
498 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", room_id));
506 for (
const auto& edge : edges) {
508 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
509 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
510 formatter.
AddField(
"type", edge.type);
517 return absl::OkStatus();
523 auto entrance_id_str = parser.
GetString(
"entrance").value();
524 auto depth_opt = parser.
GetString(
"depth");
527 if (!ParseHexString(entrance_id_str, &entrance_id)) {
528 return absl::InvalidArgumentError(
529 "Invalid entrance ID format. Must be hex (e.g., 0x27).");
531 if (entrance_id < 0 || entrance_id > 0x84) {
532 return absl::InvalidArgumentError(absl::StrFormat(
533 "Entrance ID 0x%02X out of range (0x00-0x84).", entrance_id));
537 if (depth_opt.has_value()) {
538 if (!absl::SimpleAtoi(depth_opt.value(), &max_depth)) {
539 return absl::InvalidArgumentError(
540 "Invalid depth format. Must be an integer between 1 and 200.");
542 if (max_depth < 1 || max_depth > 200) {
543 return absl::InvalidArgumentError(
"Depth must be between 1 and 200.");
547 bool same_blockset_filter = parser.
HasFlag(
"same-blockset");
550 int start_room = entrance.
room_ & 0xFF;
553 uint8_t start_blockset = 0xFF;
554 if (same_blockset_filter) {
557 start_blockset = start_room_data.
blockset();
564 std::string door_type_name;
576 std::set<int> visited;
577 std::vector<DoorEdge> door_edges;
578 std::vector<StairEdge> stair_edges;
579 std::queue<std::pair<int, int>> to_visit;
581 to_visit.push({start_room, 0});
582 visited.insert(start_room);
584 while (!to_visit.empty()) {
585 auto [room_id, depth] = to_visit.front();
588 if (depth >= max_depth)
594 for (
const auto& door : room.
GetDoors()) {
595 bool is_exit = IsExitDoorType(door.type);
596 int neighbor = is_exit ? -1 : NeighborRoomId(room_id, door.direction);
597 auto [tx, ty] = door.GetTileCoords();
600 edge.from_room = room_id;
601 edge.to_room = neighbor;
602 edge.type = DoorEdgeTypeName(door.direction);
603 edge.door_type_name = std::string(door.GetTypeName());
606 edge.is_exit = is_exit;
607 door_edges.push_back(edge);
611 if (!is_exit && neighbor >= 0 &&
612 visited.find(neighbor) == visited.end() &&
613 RoomHasDoorIn(rom, neighbor, OppositeDir(door.direction))) {
615 if (same_blockset_filter) {
617 if (nbr.
blockset() != start_blockset)
620 visited.insert(neighbor);
621 to_visit.push({neighbor, depth + 1});
626 for (
int i = 0; i < 4; ++i) {
631 edge.from_room = room_id;
633 edge.type = absl::StrFormat(
"stair%d", i + 1);
634 stair_edges.push_back(edge);
635 if (visited.find(dest) == visited.end()) {
636 visited.insert(dest);
637 to_visit.push({dest, depth + 1});
645 edge.from_room = room_id;
647 edge.type =
"holewarp";
648 stair_edges.push_back(edge);
649 if (visited.find(hw) == visited.end()) {
651 to_visit.push({hw, depth + 1});
658 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
659 formatter.
AddField(
"start_room", absl::StrFormat(
"0x%02X", start_room));
662 formatter.
AddField(
"rooms_discovered",
static_cast<int>(visited.size()));
665 std::vector<int> sorted_rooms(visited.begin(), visited.end());
666 std::sort(sorted_rooms.begin(), sorted_rooms.end());
667 for (
int rid : sorted_rooms) {
669 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", rid));
670 if (rid >= 0 && rid < 297) {
673 formatter.
AddField(
"name", absl::StrFormat(
"Room 0x%02X", rid));
681 for (
const auto& edge : door_edges) {
683 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
684 if (edge.is_exit || edge.to_room < 0) {
687 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
689 formatter.
AddField(
"type", edge.type);
690 formatter.
AddField(
"door_type", edge.door_type_name);
691 formatter.
AddField(
"tile_x", edge.tile_x);
692 formatter.
AddField(
"tile_y", edge.tile_y);
693 formatter.
AddField(
"is_exit", edge.is_exit);
700 for (
const auto& edge : stair_edges) {
702 formatter.
AddField(
"from", absl::StrFormat(
"0x%02X", edge.from_room));
703 formatter.
AddField(
"to", absl::StrFormat(
"0x%02X", edge.to_room));
704 formatter.
AddField(
"type", edge.type);
710 for (
const auto& edge : door_edges) {
715 formatter.
AddField(
"door_edges",
static_cast<int>(door_edges.size()));
716 formatter.
AddField(
"exit_doors", exit_count);
717 formatter.
AddField(
"stair_edges",
static_cast<int>(stair_edges.size()));
721 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
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
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 NeighborRoomId(int room_id, zelda3::DoorDirection dir)
int GetRoomDungeonId(Rom *rom, int room_id)
bool IsExitDoorType(zelda3::DoorType type)
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)