yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_doctor_commands.cc
Go to the documentation of this file.
2
3#include <iostream>
4#include <vector>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
9#include "rom/rom.h"
11#include "zelda3/dungeon/room.h"
12
13namespace yaze::cli {
14
15namespace {
16
17// Number of rooms in vanilla ALTTP
18constexpr int kNumRooms = 296;
19
20// Room header pointer table location
21constexpr uint32_t kRoomHeaderPointer = 0x882D;
22
24 int room_id = 0;
25 bool header_valid = false;
26 bool objects_valid = false;
27 bool sprites_valid = false;
28 int object_count = 0;
29 int sprite_count = 0;
30 int chest_count = 0;
31 std::vector<DiagnosticFinding> findings;
32
33 bool IsValid() const {
34 return header_valid && objects_valid && sprites_valid;
35 }
36
37 std::string FormatJson() const {
38 std::string findings_json = "[";
39 for (size_t i = 0; i < findings.size(); ++i) {
40 if (i > 0)
41 findings_json += ",";
42 findings_json += findings[i].FormatJson();
43 }
44 findings_json += "]";
45
46 return absl::StrFormat(
47 R"({"room_id":%d,"header_valid":%s,"objects_valid":%s,"sprites_valid":%s,)"
48 R"("object_count":%d,"sprite_count":%d,"chest_count":%d,"findings":%s})",
49 room_id, header_valid ? "true" : "false",
50 objects_valid ? "true" : "false", sprites_valid ? "true" : "false",
51 object_count, sprite_count, chest_count, findings_json);
52 }
53};
54
55RoomDiagnostic DiagnoseRoom(Rom* rom, int room_id) {
56 RoomDiagnostic diag;
57 diag.room_id = room_id;
58
59 // Try to load room header
61
62 // Check if header loaded correctly
63 diag.header_valid =
64 true; // LoadRoomHeaderFromRom doesn't fail, it just returns empty
65
66 // Load objects
67 room.LoadObjects();
68 diag.object_count = static_cast<int>(room.GetTileObjects().size());
69
70 // Load sprites
71 room.LoadSprites();
72 diag.sprite_count = static_cast<int>(room.GetSprites().size());
73
74 // Use DungeonValidator for detailed checks
76 auto result = validator.ValidateRoom(room);
77
78 diag.objects_valid = result.is_valid;
79 diag.sprites_valid = result.is_valid;
80
81 // Convert validation warnings to findings
82 for (const auto& warning : result.warnings) {
83 DiagnosticFinding finding;
84 finding.id = "room_warning";
86 finding.message = warning;
87 finding.location = absl::StrFormat("Room 0x%02X", room_id);
88 finding.fixable = false;
89 diag.findings.push_back(finding);
90 }
91
92 // Convert validation errors to findings
93 for (const auto& error : result.errors) {
94 DiagnosticFinding finding;
95 finding.id = "room_error";
97 finding.message = error;
98 finding.location = absl::StrFormat("Room 0x%02X", room_id);
99 finding.fixable = false;
100 diag.findings.push_back(finding);
101 diag.objects_valid = false;
102 }
103
104 // Count chests
105 for (const auto& obj : room.GetTileObjects()) {
106 if (obj.id_ >= 0xF9 && obj.id_ <= 0xFD) {
107 diag.chest_count++;
108 }
109 }
110
111 return diag;
112}
113
114void OutputTextBanner(bool is_json) {
115 if (is_json)
116 return;
117 std::cout << "\n";
118 std::cout
119 << "╔═══════════════════════════════════════════════════════════════╗\n";
120 std::cout
121 << "║ DUNGEON DOCTOR ║\n";
122 std::cout
123 << "║ Room Data Integrity Tool ║\n";
124 std::cout
125 << "╚═══════════════════════════════════════════════════════════════╝\n";
126}
127
128void OutputTextSummary(int total_rooms, int valid_rooms, int warning_rooms,
129 int error_rooms, int total_objects, int total_sprites) {
130 std::cout << "\n";
131 std::cout
132 << "╔═══════════════════════════════════════════════════════════════╗\n";
133 std::cout
134 << "║ DIAGNOSTIC SUMMARY ║\n";
135 std::cout
136 << "╠═══════════════════════════════════════════════════════════════╣\n";
137 std::cout << absl::StrFormat("║ Rooms Analyzed: %-44d ║\n", total_rooms);
138 std::cout << absl::StrFormat("║ Valid Rooms: %-47d ║\n", valid_rooms);
139 std::cout << absl::StrFormat("║ Rooms with Warnings: %-39d ║\n",
140 warning_rooms);
141 std::cout << absl::StrFormat("║ Rooms with Errors: %-41d ║\n", error_rooms);
142 std::cout
143 << "╠═══════════════════════════════════════════════════════════════╣\n";
144 std::cout << absl::StrFormat("║ Total Objects: %-45d ║\n", total_objects);
145 std::cout << absl::StrFormat("║ Total Sprites: %-45d ║\n", total_sprites);
146 std::cout
147 << "╚═══════════════════════════════════════════════════════════════╝\n";
148}
149
150void CheckUnusedRooms(const std::vector<RoomDiagnostic>& diagnostics,
151 std::vector<DiagnosticFinding>& findings) {
152 for (const auto& diag : diagnostics) {
153 if (diag.object_count == 0 && diag.sprite_count == 0) {
154 DiagnosticFinding finding;
155 finding.id = "unused_room";
157 finding.message = "Room appears to be empty (0 objects, 0 sprites)";
158 finding.location = absl::StrFormat("Room 0x%02X", diag.room_id);
159 finding.suggested_action = "Verify if this room is intended to be empty.";
160 finding.fixable = false;
161 findings.push_back(finding);
162 }
163 }
164}
165
166} // namespace
167
169 Rom* rom, const resources::ArgumentParser& parser,
170 resources::OutputFormatter& formatter) {
171 bool verbose = parser.HasFlag("verbose");
172 bool all_rooms = parser.HasFlag("all");
173 bool deep_scan = parser.HasFlag("deep");
174 auto room_id_arg = parser.GetInt("room");
175 bool is_json = formatter.IsJson();
176
177 if (deep_scan)
178 all_rooms = true;
179
180 OutputTextBanner(is_json);
181
182 std::vector<RoomDiagnostic> diagnostics;
183 int total_objects = 0;
184 int total_sprites = 0;
185 int valid_rooms = 0;
186 int warning_rooms = 0;
187 int error_rooms = 0;
188
189 if (room_id_arg.ok()) {
190 // Single room mode
191 int room_id = room_id_arg.value();
192 if (room_id < 0 || room_id >= kNumRooms) {
193 return absl::InvalidArgumentError(
194 absl::StrFormat("Room ID must be between 0 and %d", kNumRooms - 1));
195 }
196
197 auto diag = DiagnoseRoom(rom, room_id);
198 diagnostics.push_back(diag);
199 total_objects = diag.object_count;
200 total_sprites = diag.sprite_count;
201
202 if (diag.IsValid() && diag.findings.empty()) {
203 valid_rooms = 1;
204 } else {
205 bool has_errors = false;
206 bool has_warnings = false;
207 for (const auto& finding : diag.findings) {
208 if (finding.severity == DiagnosticSeverity::kError ||
209 finding.severity == DiagnosticSeverity::kCritical) {
210 has_errors = true;
211 } else if (finding.severity == DiagnosticSeverity::kWarning) {
212 has_warnings = true;
213 }
214 }
215 if (has_errors)
216 error_rooms = 1;
217 else if (has_warnings)
218 warning_rooms = 1;
219 else
220 valid_rooms = 1;
221 }
222
223 } else if (all_rooms) {
224 // All rooms mode
225 if (!is_json) {
226 std::cout << "\nAnalyzing all " << kNumRooms << " rooms...\n";
227 }
228
229 for (int room_id = 0; room_id < kNumRooms; ++room_id) {
230 auto diag = DiagnoseRoom(rom, room_id);
231 diagnostics.push_back(diag);
232 total_objects += diag.object_count;
233 total_sprites += diag.sprite_count;
234
235 bool has_errors = false;
236 bool has_warnings = false;
237 for (const auto& finding : diag.findings) {
238 if (finding.severity == DiagnosticSeverity::kError ||
239 finding.severity == DiagnosticSeverity::kCritical) {
240 has_errors = true;
241 } else if (finding.severity == DiagnosticSeverity::kWarning) {
242 has_warnings = true;
243 }
244 }
245
246 if (has_errors) {
247 error_rooms++;
248 } else if (has_warnings) {
249 warning_rooms++;
250 } else {
251 valid_rooms++;
252 }
253 }
254 } else {
255 // Default: sample key rooms
256 std::vector<int> sample_rooms = {0, 1, 2, 3,
257 4, 5, 6, 7, // Eastern Palace
258 32, 33, 34, 35, // Desert Palace
259 64, 65, 66, 67, // Tower of Hera
260 128, 129, 130}; // Dark rooms
261
262 if (!is_json) {
263 std::cout << "\nAnalyzing " << sample_rooms.size()
264 << " sample rooms...\n";
265 std::cout << "(Use --all to analyze all " << kNumRooms << " rooms)\n";
266 }
267
268 for (int room_id : sample_rooms) {
269 if (room_id >= kNumRooms)
270 continue;
271 auto diag = DiagnoseRoom(rom, room_id);
272 diagnostics.push_back(diag);
273 total_objects += diag.object_count;
274 total_sprites += diag.sprite_count;
275
276 bool has_errors = false;
277 bool has_warnings = false;
278 for (const auto& finding : diag.findings) {
279 if (finding.severity == DiagnosticSeverity::kError ||
280 finding.severity == DiagnosticSeverity::kCritical) {
281 has_errors = true;
282 } else if (finding.severity == DiagnosticSeverity::kWarning) {
283 has_warnings = true;
284 }
285 }
286
287 if (has_errors) {
288 error_rooms++;
289 } else if (has_warnings) {
290 warning_rooms++;
291 } else {
292 valid_rooms++;
293 }
294 }
295 }
296
297 // Deep scan analysis
298 std::vector<DiagnosticFinding> deep_findings;
299 if (deep_scan) {
300 CheckUnusedRooms(diagnostics, deep_findings);
301 }
302
303 // Output results
304 formatter.AddField("total_rooms", static_cast<int>(diagnostics.size()));
305 formatter.AddField("valid_rooms", valid_rooms);
306 formatter.AddField("warning_rooms", warning_rooms);
307 formatter.AddField("error_rooms", error_rooms);
308 formatter.AddField("total_objects", total_objects);
309 formatter.AddField("total_sprites", total_sprites);
310
311 formatter.BeginArray("rooms");
312 for (const auto& diag : diagnostics) {
313 if (is_json) {
314 formatter.AddArrayItem(diag.FormatJson());
315 } else if (verbose || !diag.findings.empty()) {
316 // In text mode, show rooms with issues or in verbose mode
317 std::string status =
318 diag.IsValid() && diag.findings.empty() ? "OK" : "ISSUES";
319 formatter.AddArrayItem(absl::StrFormat(
320 "Room 0x%02X: %s (objects=%d, sprites=%d, chests=%d)", diag.room_id,
321 status, diag.object_count, diag.sprite_count, diag.chest_count));
322 }
323 }
324 formatter.EndArray();
325
326 // Collect all findings
327 std::vector<DiagnosticFinding> all_findings;
328 for (const auto& diag : diagnostics) {
329 for (const auto& finding : diag.findings) {
330 all_findings.push_back(finding);
331 }
332 }
333 // Add deep scan findings
334 for (const auto& finding : deep_findings) {
335 all_findings.push_back(finding);
336 }
337
338 formatter.BeginArray("findings");
339 for (const auto& finding : all_findings) {
340 formatter.AddArrayItem(finding.FormatJson());
341 }
342 formatter.EndArray();
343
344 // Text summary
345 if (!is_json) {
346 OutputTextSummary(static_cast<int>(diagnostics.size()), valid_rooms,
347 warning_rooms, error_rooms, total_objects, total_sprites);
348
349 if (!all_findings.empty()) {
350 std::cout << "\n=== Issues Found ===\n";
351 for (const auto& finding : all_findings) {
352 std::cout << " " << finding.FormatText() << "\n";
353 }
354 }
355 }
356
357 return absl::OkStatus();
358}
359
360} // namespace yaze::cli
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
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.
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
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 AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
ValidationResult ValidateRoom(const Room &room)
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:246
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:346
void LoadObjects()
Definition room.cc:1133
void LoadSprites()
Definition room.cc:1621
void CheckUnusedRooms(const std::vector< RoomDiagnostic > &diagnostics, std::vector< DiagnosticFinding > &findings)
void OutputTextSummary(int total_rooms, int valid_rooms, int warning_rooms, int error_rooms, int total_objects, int total_sprites)
Namespace for the command line interface.
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:205
A single diagnostic finding.