yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_group_commands.cc
Go to the documentation of this file.
2
3#include <cstdint>
4#include <map>
5#include <set>
6#include <string>
7#include <vector>
8
9#include "absl/strings/str_format.h"
10#include "cli/util/hex_util.h"
11#include "rom/rom.h"
13#include "zelda3/dungeon/room.h"
15
16namespace yaze {
17namespace cli {
18namespace handlers {
19
21
22namespace {
23
24// ROM addresses for dungeon tables
25constexpr int kEntranceDungeon = 0x1548B; // Dungeon ID per entrance
26constexpr int kDungeonsStartRooms = 0x7939; // Start room per dungeon
27constexpr int kDungeonsEndRooms = 0x792D; // End room per dungeon (unused)
28constexpr int kDungeonsBossRooms = 0x10954; // Boss room per dungeon
29constexpr int kNumberOfDungeons = 14; // Vanilla dungeons
30constexpr int kNumberOfEntrances = 0x84; // Total entrances
31
32// Dungeon names (vanilla + common custom slots)
33const std::vector<std::string> kDungeonNames = {
34 "Sewers", // 0x00
35 "Hyrule Castle", // 0x01
36 "Eastern Palace", // 0x02
37 "Desert Palace", // 0x03
38 "Agahnim's Tower", // 0x04
39 "Swamp Palace", // 0x05
40 "Palace of Darkness", // 0x06
41 "Misery Mire", // 0x07
42 "Skull Woods", // 0x08
43 "Ice Palace", // 0x09
44 "Tower of Hera", // 0x0A
45 "Thieves' Town", // 0x0B
46 "Turtle Rock", // 0x0C
47 "Ganon's Tower", // 0x0D
48 "Custom Dungeon 0E", // 0x0E
49 "Custom Dungeon 0F", // 0x0F
50 "Custom Dungeon 10", // 0x10
51 "Custom Dungeon 11", // 0x11
52 "Custom Dungeon 12", // 0x12
53 "Custom Dungeon 13", // 0x13
54 "Custom Dungeon 14", // 0x14 (Oracle custom)
55 "Custom Dungeon 15", // 0x15
56 "Custom Dungeon 16", // 0x16
57 "Custom Dungeon 17", // 0x17
58 "Custom Dungeon 18", // 0x18
59 "Custom Dungeon 19", // 0x19
60 "Custom Dungeon 1A", // 0x1A
61 "Custom Dungeon 1B", // 0x1B
62 "Custom Dungeon 1C", // 0x1C
63 "Custom Dungeon 1D", // 0x1D
64 "Custom Dungeon 1E", // 0x1E
65 "Custom Dungeon 1F", // 0x1F
66};
67
69 int id;
70 std::string name;
73 std::set<int> rooms;
74};
75
76std::string GetDungeonName(int id) {
77 if (id >= 0 && id < static_cast<int>(kDungeonNames.size())) {
78 return kDungeonNames[id];
79 }
80 return absl::StrFormat("Dungeon %02X", id);
81}
82
83} // namespace
84
86 Rom* rom, const resources::ArgumentParser& parser,
87 resources::OutputFormatter& formatter) {
88 // Parse optional filters
89 auto dungeon_id_opt = parser.GetString("dungeon");
90 bool list_only = parser.HasFlag("list");
91
92 int dungeon_filter = -1;
93 if (dungeon_id_opt.has_value()) {
94 if (!ParseHexString(dungeon_id_opt.value(), &dungeon_filter)) {
95 return absl::InvalidArgumentError(
96 "Invalid dungeon ID format. Must be hex (e.g., 0x02).");
97 }
98 }
99
100 // Build dungeon info from ROM tables
101 std::map<int, DungeonInfo> dungeons;
102
103 // Read start rooms and boss rooms from ROM
104 for (int i = 0; i < kNumberOfDungeons; ++i) {
105 DungeonInfo info;
106 info.id = i;
107 info.name = GetDungeonName(i);
108
109 // Read start room (1 byte per dungeon)
110 info.start_room = rom->data()[kDungeonsStartRooms + i];
111
112 // Read boss room (2 bytes per dungeon)
113 uint16_t boss_room_addr = kDungeonsBossRooms + (i * 2);
114 info.boss_room =
115 rom->data()[boss_room_addr] | (rom->data()[boss_room_addr + 1] << 8);
116
117 // Boss room 0xFFFF means no boss
118 if (info.boss_room == 0xFFFF) {
119 info.boss_room = -1;
120 }
121
122 dungeons[i] = info;
123 }
124
125 // Scan entrances to find room->dungeon mappings
126 std::map<int, int> room_to_dungeon;
127 for (int entrance_id = 0; entrance_id < kNumberOfEntrances; ++entrance_id) {
128 zelda3::RoomEntrance entrance(rom, static_cast<uint8_t>(entrance_id),
129 false);
130
131 int dungeon_id = entrance.dungeon_id_;
132 int room_id = entrance.room_;
133
134 // Track the room's dungeon assignment
135 room_to_dungeon[room_id] = dungeon_id;
136
137 // Add room to dungeon's room set
138 if (dungeons.find(dungeon_id) == dungeons.end()) {
139 // Create entry for custom dungeons discovered via entrances
140 DungeonInfo info;
141 info.id = dungeon_id;
142 info.name = GetDungeonName(dungeon_id);
143 info.start_room = -1;
144 info.boss_room = -1;
145 dungeons[dungeon_id] = info;
146 }
147 dungeons[dungeon_id].rooms.insert(room_id);
148 }
149
150 // Also scan spawn points for additional room mappings
151 for (int spawn_id = 0; spawn_id < zelda3::kNumDungeonSpawnPoints;
152 ++spawn_id) {
153 auto spawn_or = zelda3::DungeonSpawnPoint::Load(*rom, spawn_id);
154 if (!spawn_or.ok()) {
155 return spawn_or.status();
156 }
157 const auto& spawn = spawn_or.value();
158
159 int dungeon_id = spawn.dungeon_id;
160 int room_id = spawn.room_id;
161
162 room_to_dungeon[room_id] = dungeon_id;
163
164 if (dungeons.find(dungeon_id) != dungeons.end()) {
165 dungeons[dungeon_id].rooms.insert(room_id);
166 }
167 }
168
169 // Find rooms not assigned to any dungeon
170 std::set<int> unassigned_rooms;
171 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
172 if (room_to_dungeon.find(room_id) == room_to_dungeon.end()) {
173 unassigned_rooms.insert(room_id);
174 }
175 }
176
177 // Output
178 formatter.BeginObject("dungeon_groups");
179
180 // List mode - compact dungeon listing
181 if (list_only) {
182 formatter.BeginArray("dungeons");
183 for (const auto& [id, info] : dungeons) {
184 if (dungeon_filter >= 0 && id != dungeon_filter) {
185 continue;
186 }
187 formatter.BeginObject();
188 formatter.AddField("id", absl::StrFormat("0x%02X", id));
189 formatter.AddField("name", info.name);
190 formatter.AddField("room_count", static_cast<int>(info.rooms.size()));
191 formatter.EndObject();
192 }
193 formatter.EndArray();
194 } else {
195 // Full output with room lists
196 formatter.BeginArray("dungeons");
197 for (const auto& [id, info] : dungeons) {
198 if (dungeon_filter >= 0 && id != dungeon_filter) {
199 continue;
200 }
201
202 // Skip empty dungeons unless specifically requested
203 if (info.rooms.empty() && dungeon_filter < 0) {
204 continue;
205 }
206
207 formatter.BeginObject();
208 formatter.AddField("id", absl::StrFormat("0x%02X", id));
209 formatter.AddField("name", info.name);
210
211 if (info.start_room >= 0) {
212 formatter.AddField("start_room",
213 absl::StrFormat("0x%02X", info.start_room));
214 } else {
215 formatter.AddField("start_room", "null");
216 }
217
218 if (info.boss_room >= 0) {
219 formatter.AddField("boss_room",
220 absl::StrFormat("0x%02X", info.boss_room));
221 } else {
222 formatter.AddField("boss_room", "null");
223 }
224
225 // Room list
226 formatter.BeginArray("rooms");
227 for (int room_id : info.rooms) {
228 formatter.BeginObject();
229 formatter.AddField("id", absl::StrFormat("0x%02X", room_id));
230 // Bounds check for kRoomNames (array size is 297)
231 if (room_id >= 0 && room_id < 297) {
232 formatter.AddField("name", std::string(zelda3::kRoomNames[room_id]));
233 } else {
234 formatter.AddField("name", absl::StrFormat("Room 0x%02X", room_id));
235 }
236 formatter.EndObject();
237 }
238 formatter.EndArray();
239
240 formatter.EndObject();
241 }
242 formatter.EndArray();
243
244 // Unassigned rooms (only in full output)
245 if (dungeon_filter < 0 && !unassigned_rooms.empty()) {
246 formatter.BeginArray("unassigned_rooms");
247 for (int room_id : unassigned_rooms) {
248 formatter.AddArrayItem(absl::StrFormat("0x%02X", room_id));
249 }
250 formatter.EndArray();
251 }
252 }
253
254 // Statistics
255 formatter.BeginObject("stats");
256 int total_assigned = 0;
257 for (const auto& [id, info] : dungeons) {
258 total_assigned += static_cast<int>(info.rooms.size());
259 }
260 formatter.AddField("total_dungeons", static_cast<int>(dungeons.size()));
261 formatter.AddField("assigned_rooms", total_assigned);
262 formatter.AddField("unassigned_rooms",
263 static_cast<int>(unassigned_rooms.size()));
264 formatter.EndObject();
265
266 formatter.EndObject();
267
268 return absl::OkStatus();
269}
270
271} // namespace handlers
272} // namespace cli
273} // 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:28
auto data() const
Definition rom.h:151
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.
static absl::StatusOr< DungeonSpawnPoint > Load(const Rom &rom, int spawn_id)
Dungeon Room Entrance or Spawn Point.
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
constexpr std::array< std::string_view, 297 > kRoomNames
Definition room.h:1204
constexpr int kNumDungeonSpawnPoints
constexpr int kNumberOfRooms
constexpr int kEntranceDungeon