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