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