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
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 RETURN_IF_ERROR(entrance.load_status());
131
132 int dungeon_id = entrance.dungeon_id_;
133 int room_id = entrance.room_;
134
135 // Track the room's dungeon assignment
136 room_to_dungeon[room_id] = dungeon_id;
137
138 // Add room to dungeon's room set
139 if (dungeons.find(dungeon_id) == dungeons.end()) {
140 // Create entry for custom dungeons discovered via entrances
141 DungeonInfo info;
142 info.id = dungeon_id;
143 info.name = GetDungeonName(dungeon_id);
144 info.start_room = -1;
145 info.boss_room = -1;
146 dungeons[dungeon_id] = info;
147 }
148 dungeons[dungeon_id].rooms.insert(room_id);
149 }
150
151 // Also scan spawn points for additional room mappings
152 for (int spawn_id = 0; spawn_id < zelda3::kNumDungeonSpawnPoints;
153 ++spawn_id) {
154 auto spawn_or = zelda3::DungeonSpawnPoint::Load(*rom, spawn_id);
155 if (!spawn_or.ok()) {
156 return spawn_or.status();
157 }
158 const auto& spawn = spawn_or.value();
159
160 int dungeon_id = spawn.dungeon_id;
161 int room_id = spawn.room_id;
162
163 room_to_dungeon[room_id] = dungeon_id;
164
165 if (dungeons.find(dungeon_id) != dungeons.end()) {
166 dungeons[dungeon_id].rooms.insert(room_id);
167 }
168 }
169
170 // Find rooms not assigned to any dungeon
171 std::set<int> unassigned_rooms;
172 for (int room_id = 0; room_id < zelda3::kNumberOfRooms; ++room_id) {
173 if (room_to_dungeon.find(room_id) == room_to_dungeon.end()) {
174 unassigned_rooms.insert(room_id);
175 }
176 }
177
178 // Output
179 formatter.BeginObject("dungeon_groups");
180
181 // List mode - compact dungeon listing
182 if (list_only) {
183 formatter.BeginArray("dungeons");
184 for (const auto& [id, info] : dungeons) {
185 if (dungeon_filter >= 0 && id != dungeon_filter) {
186 continue;
187 }
188 formatter.BeginObject();
189 formatter.AddField("id", absl::StrFormat("0x%02X", id));
190 formatter.AddField("name", info.name);
191 formatter.AddField("room_count", static_cast<int>(info.rooms.size()));
192 formatter.EndObject();
193 }
194 formatter.EndArray();
195 } else {
196 // Full output with room lists
197 formatter.BeginArray("dungeons");
198 for (const auto& [id, info] : dungeons) {
199 if (dungeon_filter >= 0 && id != dungeon_filter) {
200 continue;
201 }
202
203 // Skip empty dungeons unless specifically requested
204 if (info.rooms.empty() && dungeon_filter < 0) {
205 continue;
206 }
207
208 formatter.BeginObject();
209 formatter.AddField("id", absl::StrFormat("0x%02X", id));
210 formatter.AddField("name", info.name);
211
212 if (info.start_room >= 0) {
213 formatter.AddField("start_room",
214 absl::StrFormat("0x%02X", info.start_room));
215 } else {
216 formatter.AddField("start_room", "null");
217 }
218
219 if (info.boss_room >= 0) {
220 formatter.AddField("boss_room",
221 absl::StrFormat("0x%02X", info.boss_room));
222 } else {
223 formatter.AddField("boss_room", "null");
224 }
225
226 // Room list
227 formatter.BeginArray("rooms");
228 for (int room_id : info.rooms) {
229 formatter.BeginObject();
230 formatter.AddField("id", absl::StrFormat("0x%02X", room_id));
231 // Bounds check for kRoomNames (array size is 297)
232 if (room_id >= 0 && room_id < 297) {
233 formatter.AddField("name", std::string(zelda3::kRoomNames[room_id]));
234 } else {
235 formatter.AddField("name", absl::StrFormat("Room 0x%02X", room_id));
236 }
237 formatter.EndObject();
238 }
239 formatter.EndArray();
240
241 formatter.EndObject();
242 }
243 formatter.EndArray();
244
245 // Unassigned rooms (only in full output)
246 if (dungeon_filter < 0 && !unassigned_rooms.empty()) {
247 formatter.BeginArray("unassigned_rooms");
248 for (int room_id : unassigned_rooms) {
249 formatter.AddArrayItem(absl::StrFormat("0x%02X", room_id));
250 }
251 formatter.EndArray();
252 }
253 }
254
255 // Statistics
256 formatter.BeginObject("stats");
257 int total_assigned = 0;
258 for (const auto& [id, info] : dungeons) {
259 total_assigned += static_cast<int>(info.rooms.size());
260 }
261 formatter.AddField("total_dungeons", static_cast<int>(dungeons.size()));
262 formatter.AddField("assigned_rooms", total_assigned);
263 formatter.AddField("unassigned_rooms",
264 static_cast<int>(unassigned_rooms.size()));
265 formatter.EndObject();
266
267 formatter.EndObject();
268
269 return absl::OkStatus();
270}
271
272} // namespace handlers
273} // namespace cli
274} // 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:49
auto data() const
Definition rom.h:201
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.
const absl::Status & load_status() const
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
constexpr int kNumRegularDungeonEntrances
constexpr std::array< std::string_view, 297 > kRoomNames
Definition room.h:1478
constexpr int kNumDungeonSpawnPoints
constexpr int kNumberOfRooms
constexpr int kEntranceDungeon
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22