yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_doctor_commands.cc
Go to the documentation of this file.
2
3#include <iostream>
4
5#include "absl/status/status.h"
6#include "absl/strings/match.h"
7#include "absl/strings/str_format.h"
8#include "absl/strings/str_join.h"
11#include "rom/hm_support.h"
12#include "rom/rom.h"
13
14namespace yaze::cli {
15
16namespace {
17
18// ROM header locations (LoROM)
19// ROM header locations (LoROM)
20// kSnesHeaderBase, kChecksumComplementPos, kChecksumPos defined in diagnostic_types.h
21
22// Expected sizes
23constexpr size_t kVanillaSize = 0x100000; // 1MB
24constexpr size_t kExpandedSize = 0x200000; // 2MB
25
27 std::string title;
28 uint8_t map_mode = 0;
29 uint8_t rom_type = 0;
30 uint8_t rom_size = 0;
31 uint8_t sram_size = 0;
32 uint8_t country = 0;
33 uint8_t license = 0;
34 uint8_t version = 0;
35 uint16_t checksum_complement = 0;
36 uint16_t checksum = 0;
37 bool checksum_valid = false;
38};
39
41 RomHeaderInfo info;
42 const auto& data = rom->data();
43
44 if (rom->size() < yaze::cli::kSnesHeaderBase + 32) {
45 return info;
46 }
47
48 // Read title (21 bytes)
49 for (int i = 0; i < 21; ++i) {
50 char chr = static_cast<char>(data[yaze::cli::kSnesHeaderBase + i]);
51 if (chr >= 32 && chr < 127) {
52 info.title += chr;
53 }
54 }
55
56 // Trim trailing spaces
57 while (!info.title.empty() && info.title.back() == ' ') {
58 info.title.pop_back();
59 }
60
61 info.map_mode = data[yaze::cli::kSnesHeaderBase + 21];
62 info.rom_type = data[yaze::cli::kSnesHeaderBase + 22];
63 info.rom_size = data[yaze::cli::kSnesHeaderBase + 23];
64 info.sram_size = data[yaze::cli::kSnesHeaderBase + 24];
65 info.country = data[yaze::cli::kSnesHeaderBase + 25];
66 info.license = data[yaze::cli::kSnesHeaderBase + 26];
67 info.version = data[yaze::cli::kSnesHeaderBase + 27];
68
69 // Read checksums
71 (data[yaze::cli::kChecksumComplementPos + 1] << 8);
72 info.checksum =
73 data[yaze::cli::kChecksumPos] | (data[yaze::cli::kChecksumPos + 1] << 8);
74
75 // Validate checksum (complement XOR checksum should be 0xFFFF)
76 info.checksum_valid = ((info.checksum_complement ^ info.checksum) == 0xFFFF);
77
78 return info;
79}
80
81std::string GetMapModeName(uint8_t mode) {
82 switch (mode & 0x0F) {
83 case 0x00:
84 return "LoROM";
85 case 0x01:
86 return "HiROM";
87 case 0x02:
88 return "LoROM + S-DD1";
89 case 0x03:
90 return "LoROM + SA-1";
91 case 0x05:
92 return "ExHiROM";
93 default:
94 return absl::StrFormat("Unknown (0x%02X)", mode);
95 }
96}
97
98std::string GetCountryName(uint8_t country) {
99 switch (country) {
100 case 0x00:
101 return "Japan";
102 case 0x01:
103 return "USA";
104 case 0x02:
105 return "Europe";
106 default:
107 return absl::StrFormat("Unknown (0x%02X)", country);
108 }
109}
110
111void OutputTextBanner(bool is_json) {
112 if (is_json)
113 return;
114 std::cout << "\n";
115 std::cout
116 << "╔═══════════════════════════════════════════════════════════════╗\n";
117 std::cout
118 << "║ ROM DOCTOR ║\n";
119 std::cout
120 << "║ File Integrity & Validation Tool ║\n";
121 std::cout
122 << "╚═══════════════════════════════════════════════════════════════╝\n";
123}
124
126 RomFeatures features;
127
128 if (kZSCustomVersionPos < rom->size()) {
129 features.zs_custom_version = rom->data()[kZSCustomVersionPos];
130 features.is_vanilla = (features.zs_custom_version == 0xFF ||
131 features.zs_custom_version == 0x00);
132 features.is_v2 = (!features.is_vanilla && features.zs_custom_version == 2);
133 features.is_v3 = (!features.is_vanilla && features.zs_custom_version >= 3);
134 } else {
135 features.is_vanilla = true;
136 }
137
138 if (!features.is_vanilla) {
139 if (kMap16ExpandedFlagPos < rom->size()) {
140 uint8_t flag = rom->data()[kMap16ExpandedFlagPos];
141 features.has_expanded_tile16 = (flag != 0x0F);
142 }
143
144 if (kMap32ExpandedFlagPos < rom->size()) {
145 uint8_t flag = rom->data()[kMap32ExpandedFlagPos];
146 features.has_expanded_tile32 = (flag != 0x04);
147 }
148 }
149
150 if (kExpandedPtrTableMarker < rom->size()) {
153 }
154
155 return features;
156}
157
159 const auto* data = rom->data();
160 size_t size = rom->size();
161
162 // Check known problematic addresses
163 for (uint32_t addr : kProblemAddresses) {
164 if (addr < size) {
165 // Heuristic: If we find 0x00 or 0xFF in the middle of what should be Tile16 data,
166 // it might be suspicious, but we need a better heuristic.
167 // For now, let's just check if it's a known bad value from a specific bug.
168 // Example: A previous bug wrote 0x00 to these locations.
169 if (data[addr] == 0x00) {
170 DiagnosticFinding finding;
171 finding.id = "known_corruption_pattern";
173 finding.message = absl::StrFormat(
174 "Potential corruption detected at known problematic address 0x%06X",
175 addr);
176 finding.location = absl::StrFormat("0x%06X", addr);
177 finding.suggested_action =
178 "Check if this byte should be 0x00. If not, restore from backup.";
179 finding.fixable = false;
180 report.AddFinding(finding);
181 }
182 }
183 }
184
185 // Check for zero-filled blocks in critical code regions (Bank 00)
186 // 0x0000-0x7FFF is code/data. Large blocks of 0x00 might indicate erasure.
187 // We'll scan a small sample.
188 int zero_run = 0;
189 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
190 if (data[i] == 0x00)
191 zero_run++;
192 else
193 zero_run = 0;
194
195 if (zero_run > 64) {
196 DiagnosticFinding finding;
197 finding.id = "bank00_erasure";
199 finding.message = "Large block of zeros detected in Bank 00 code region";
200 finding.location = absl::StrFormat("Around 0x%06X", i);
201 finding.suggested_action =
202 "ROM is likely corrupted. Restore from backup.";
203 finding.fixable = false;
204 report.AddFinding(finding);
205 break;
206 }
207 }
208}
209
211 if (!report.features.has_expanded_tile16)
212 return;
213
214 const auto* data = rom->data();
215 size_t size = rom->size();
216
217 // Check Tile16 expansion region (0x1E8000 - 0x1F0000)
218 // This region should contain data, not be all empty.
219 if (size >= kMap16TilesExpandedEnd) {
220 bool all_empty = true;
221 for (uint32_t i = kMap16TilesExpanded; i < kMap16TilesExpandedEnd;
222 i += 256) {
223 if (data[i] != 0xFF && data[i] != 0x00) {
224 all_empty = false;
225 break;
226 }
227 }
228
229 if (all_empty) {
230 DiagnosticFinding finding;
231 finding.id = "empty_expanded_tile16";
233 finding.message =
234 "Expanded Tile16 region appears to be empty/uninitialized";
235 finding.location = "0x1E8000-0x1F0000";
236 finding.suggested_action =
237 "Re-save Tile16 data from editor or re-apply expansion patch.";
238 finding.fixable = false;
239 report.AddFinding(finding);
240 }
241 }
242}
243
245 // 1. Search for "PARALLEL WORLDS" string in decoded messages
246 try {
247 std::vector<uint8_t> rom_data_copy = rom->vector(); // Copy for safety
248 auto messages = yaze::editor::ReadAllTextData(rom_data_copy.data());
249
250 bool pw_string_found = false;
251 for (const auto& msg : messages) {
252 if (absl::StrContains(msg.ContentsParsed, "PARALLEL WORLDS") ||
253 absl::StrContains(msg.ContentsParsed, "Parallel Worlds")) {
254 pw_string_found = true;
255 break;
256 }
257 }
258
259 if (pw_string_found) {
260 DiagnosticFinding finding;
261 finding.id = "parallel_worlds_string";
263 finding.message = "Found 'PARALLEL WORLDS' string in message data";
264 finding.location = "Message Data";
265 finding.suggested_action = "Confirmed Parallel Worlds ROM.";
266 finding.fixable = false;
267 report.AddFinding(finding);
268 }
269 } catch (...) {
270 // Ignore parsing errors
271 }
272}
273
275 const auto* data = rom->data();
276 size_t size = rom->size();
277
278 bool has_zscustom_features = false;
279 std::vector<std::string> features_found;
280
281 if (kCustomBGEnabledPos < size && data[kCustomBGEnabledPos] != 0x00 &&
282 data[kCustomBGEnabledPos] != 0xFF) {
283 has_zscustom_features = true;
284 features_found.push_back("Custom BG");
285 }
286 if (kCustomMainPalettePos < size && data[kCustomMainPalettePos] != 0x00 &&
287 data[kCustomMainPalettePos] != 0xFF) {
288 has_zscustom_features = true;
289 features_found.push_back("Custom Palette");
290 }
291
292 if (has_zscustom_features && report.features.is_vanilla) {
293 DiagnosticFinding finding;
294 finding.id = "zscustom_features_detected";
296 finding.message = absl::StrFormat(
297 "ZSCustom features detected despite missing version header: %s",
298 absl::StrJoin(features_found, ", "));
299 finding.location = "ZSCustom Flags";
300 finding.suggested_action = "Treat as ZSCustom ROM.";
301 finding.fixable = false;
302 report.AddFinding(finding);
303
304 report.features.is_vanilla = false;
305 report.features.zs_custom_version = 0xFE; // Unknown/Detected
306 }
307}
308
309} // namespace
310
312 Rom* rom, const resources::ArgumentParser& parser,
313 resources::OutputFormatter& formatter) {
314 const bool verbose = parser.HasFlag("verbose");
315 const bool is_json = formatter.IsJson();
316
317 OutputTextBanner(is_json);
318
319 DiagnosticReport report;
320 report.rom_path = rom->filename();
321
322 // Basic ROM info
323 formatter.AddField("rom_path", rom->filename());
324 formatter.AddField("size_bytes", static_cast<int>(rom->size()));
325 formatter.AddHexField("size_hex", rom->size(), 6);
326
327 // Size validation
328 bool size_valid =
329 (rom->size() == kVanillaSize || rom->size() == kExpandedSize);
330 formatter.AddField("size_valid", size_valid);
331
332 if (rom->size() == kVanillaSize) {
333 formatter.AddField("size_type", "vanilla_1mb");
334 } else if (rom->size() == kExpandedSize) {
335 formatter.AddField("size_type", "expanded_2mb");
336 } else {
337 formatter.AddField("size_type", "non_standard");
338
339 DiagnosticFinding finding;
340 finding.id = "non_standard_size";
342 finding.message = absl::StrFormat(
343 "Non-standard ROM size: 0x%zX bytes (expected 0x%zX or 0x%zX)",
344 rom->size(), kVanillaSize, kExpandedSize);
345 finding.location = "ROM file";
346 finding.fixable = false;
347 report.AddFinding(finding);
348 }
349
350 // Read and validate header
351 auto header = ReadRomHeader(rom);
352
353 formatter.AddField("title", header.title);
354 formatter.AddField("map_mode", GetMapModeName(header.map_mode));
355 formatter.AddHexField("rom_type", header.rom_type, 2);
356 formatter.AddField("rom_size_header", 1 << (header.rom_size + 10));
357 formatter.AddField("sram_size",
358 header.sram_size > 0 ? (1 << (header.sram_size + 10)) : 0);
359 formatter.AddField("country", GetCountryName(header.country));
360 formatter.AddField("version", header.version);
361 formatter.AddHexField("checksum_complement", header.checksum_complement, 4);
362 formatter.AddHexField("checksum", header.checksum, 4);
363 formatter.AddField("checksum_valid", header.checksum_valid);
364
365 if (!header.checksum_valid) {
366 DiagnosticFinding finding;
367 finding.id = "invalid_checksum";
369 finding.message = absl::StrFormat(
370 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, "
371 "expected 0xFFFF)",
372 header.checksum_complement, header.checksum,
373 header.checksum_complement ^ header.checksum);
374 finding.location =
375 absl::StrFormat("0x%04X", yaze::cli::kChecksumComplementPos);
376 finding.suggested_action =
377 "ROM may be corrupted or modified without checksum update";
378 finding.fixable = true; // Could be fixed by recalculating
379 report.AddFinding(finding);
380 }
381
382 // Detect ZSCustomOverworld version
383 report.features = DetectRomFeaturesLocal(rom);
384 formatter.AddField("zs_custom_version", report.features.GetVersionString());
385 formatter.AddField("is_vanilla", report.features.is_vanilla);
386 formatter.AddField("expanded_tile16", report.features.has_expanded_tile16);
387 formatter.AddField("expanded_tile32", report.features.has_expanded_tile32);
388 formatter.AddField("expanded_pointer_tables",
390
391 // Free space analysis (simplified)
392 if (rom->size() >= kExpandedSize) {
393 // Check for free space in expansion region
394 size_t free_bytes = 0;
395 for (size_t i = 0x180000; i < 0x1E0000 && i < rom->size(); ++i) {
396 if (rom->data()[i] == 0x00 || rom->data()[i] == 0xFF) {
397 free_bytes++;
398 }
399 }
400 formatter.AddField("free_space_estimate", static_cast<int>(free_bytes));
401 formatter.AddField("free_space_region", "0x180000-0x1E0000");
402
403 DiagnosticFinding finding;
404 finding.id = "free_space_info";
406 finding.message = absl::StrFormat(
407 "Estimated free space in expansion region: %zu bytes (%.1f KB)",
408 free_bytes, free_bytes / 1024.0);
409 finding.location = "0x180000-0x1E0000";
410 finding.fixable = false;
411 report.AddFinding(finding);
412 }
413
414 // 3. Check for corruption heuristics
415 CheckCorruptionHeuristics(rom, report);
416
417 // 4. Hyrule Magic / Parallel Worlds Analysis
418 yaze::rom::HyruleMagicValidator hm_validator(rom);
419 if (hm_validator.IsParallelWorlds()) {
420 DiagnosticFinding finding;
421 finding.id = "parallel_worlds_detected";
423 finding.message = "Parallel Worlds (1.5MB) detected (Header check)";
424 finding.location = "ROM Header";
425 finding.suggested_action =
426 "Use z3ed for editing. Custom pointer tables are supported.";
427 finding.fixable = false;
428 report.AddFinding(finding);
429 } else if (hm_validator.HasBank00Erasure()) {
430 DiagnosticFinding finding;
431 finding.id = "hm_corruption_detected";
433 finding.message = "Hyrule Magic corruption detected (Bank 00 erasure)";
434 finding.location = "Bank 00";
435 finding.suggested_action = "ROM is likely unstable. Restore from backup.";
436 finding.fixable = false;
437 report.AddFinding(finding);
438 }
439
440 // Advanced Heuristics
441 CheckParallelWorldsHeuristics(rom, report);
442 CheckZScreamHeuristics(rom, report);
443
444 // 5. Validate expanded tables
445 ValidateExpandedTables(rom, report);
446
447 // Output findings
448 formatter.BeginArray("findings");
449 for (const auto& finding : report.findings) {
450 formatter.AddArrayItem(finding.FormatJson());
451 }
452 formatter.EndArray();
453
454 // Summary
455 formatter.AddField("total_findings", report.TotalFindings());
456 formatter.AddField("critical_count", report.critical_count);
457 formatter.AddField("error_count", report.error_count);
458 formatter.AddField("warning_count", report.warning_count);
459 formatter.AddField("info_count", report.info_count);
460 formatter.AddField("has_problems", report.HasProblems());
461
462 // Text output
463 if (!is_json) {
464 std::cout << "\n";
465 std::cout << "╔════════════════════════════════════════════════════════════"
466 "═══╗\n";
467 std::cout << "║ DIAGNOSTIC SUMMARY "
468 " ║\n";
469 std::cout << "╠════════════════════════════════════════════════════════════"
470 "═══╣\n";
471 std::cout << absl::StrFormat("║ ROM Title: %-49s ║\n", header.title);
472 std::cout << absl::StrFormat("║ Size: 0x%06zX bytes (%zu KB)%-26s ║\n",
473 rom->size(), rom->size() / 1024, "");
474 std::cout << absl::StrFormat("║ Map Mode: %-50s ║\n",
475 GetMapModeName(header.map_mode));
476 std::cout << absl::StrFormat("║ Country: %-51s ║\n",
477 GetCountryName(header.country));
478 std::cout << "╠════════════════════════════════════════════════════════════"
479 "═══╣\n";
480 std::cout << absl::StrFormat(
481 "║ Checksum: 0x%04X (complement: 0x%04X) - %s%-14s ║\n",
482 header.checksum, header.checksum_complement,
483 header.checksum_valid ? "VALID" : "INVALID", "");
484 std::cout << absl::StrFormat("║ ZSCustomOverworld: %-41s ║\n",
485 report.features.GetVersionString());
486 std::cout << absl::StrFormat(
487 "║ Expanded Tile16: %-43s ║\n",
488 report.features.has_expanded_tile16 ? "YES" : "NO");
489 std::cout << absl::StrFormat(
490 "║ Expanded Tile32: %-43s ║\n",
491 report.features.has_expanded_tile32 ? "YES" : "NO");
492 std::cout << absl::StrFormat(
493 "║ Expanded Ptr Tables: %-39s ║\n",
494 report.features.has_expanded_pointer_tables ? "YES" : "NO");
495 std::cout << "╠════════════════════════════════════════════════════════════"
496 "═══╣\n";
497 std::cout << absl::StrFormat(
498 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
499 report.TotalFindings(), report.error_count, report.warning_count,
500 report.info_count, "");
501 std::cout << "╚════════════════════════════════════════════════════════════"
502 "═══╝\n";
503
504 if (verbose && !report.findings.empty()) {
505 std::cout << "\n=== Detailed Findings ===\n";
506 for (const auto& finding : report.findings) {
507 std::cout << " " << finding.FormatText() << "\n";
508 }
509 }
510 }
511
512 return absl::OkStatus();
513}
514
515} // 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
auto filename() const
Definition rom.h:141
const auto & vector() const
Definition rom.h:139
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
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.
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.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
void CheckParallelWorldsHeuristics(Rom *rom, DiagnosticReport &report)
void CheckCorruptionHeuristics(Rom *rom, DiagnosticReport &report)
void ValidateExpandedTables(Rom *rom, DiagnosticReport &report)
void CheckZScreamHeuristics(Rom *rom, DiagnosticReport &report)
Namespace for the command line interface.
constexpr uint32_t kCustomBGEnabledPos
constexpr uint32_t kExpandedPtrTableMarker
constexpr uint32_t kChecksumPos
constexpr uint8_t kExpandedPtrTableMagic
constexpr uint32_t kMap32ExpandedFlagPos
constexpr uint32_t kZSCustomVersionPos
constexpr uint32_t kMap16ExpandedFlagPos
const uint32_t kProblemAddresses[]
constexpr uint32_t kMap16TilesExpanded
constexpr uint32_t kMap16TilesExpandedEnd
constexpr uint32_t kSnesHeaderBase
constexpr uint32_t kChecksumComplementPos
constexpr uint32_t kCustomMainPalettePos
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos)
A single diagnostic finding.
Complete diagnostic report.
std::vector< DiagnosticFinding > findings
int TotalFindings() const
Get total finding count.
bool HasProblems() const
Check if report has any critical or error findings.
void AddFinding(const DiagnosticFinding &finding)
Add a finding and update counts.
ROM feature detection results.
std::string GetVersionString() const
Get version as human-readable string.