7#include "absl/status/status.h"
8#include "absl/strings/match.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_join.h"
33 uint8_t sram_size = 0;
37 uint16_t checksum_complement = 0;
38 uint16_t checksum = 0;
39 bool checksum_valid =
false;
44 const auto& data = rom->
data();
51 for (
int i = 0; i < 21; ++i) {
53 if (chr >= 32 && chr < 127) {
59 while (!info.
title.empty() && info.
title.back() ==
' ') {
60 info.
title.pop_back();
84 switch (mode & 0x0F) {
90 return "LoROM + S-DD1";
92 return "LoROM + SA-1";
96 return absl::StrFormat(
"Unknown (0x%02X)", mode);
109 return absl::StrFormat(
"Unknown (0x%02X)", country);
113void OutputTextBanner(
bool is_json) {
118 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
120 <<
"║ ROM DOCTOR ║\n";
122 <<
"║ File Integrity & Validation Tool ║\n";
124 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
130 if (kZSCustomVersionPos < rom->size()) {
141 if (kMap16ExpandedFlagPos < rom->size()) {
146 if (kMap32ExpandedFlagPos < rom->size()) {
152 if (kExpandedPtrTableMarker < rom->size()) {
163 std::array<size_t, 256> counts = {0};
164 for (
size_t i = 0; i < size; ++i) {
168 double entropy = 0.0;
169 for (
size_t count : counts) {
171 double p =
static_cast<double>(count) / size;
172 entropy -= p * std::log2(p);
179 const auto* data = rom->
data();
180 size_t size = rom->
size();
185 if (data[addr] == 0x00) {
187 finding.
id =
"known_corruption_pattern";
189 finding.
message = absl::StrFormat(
190 "Potential corruption detected at known problematic address 0x%06X",
192 finding.
location = absl::StrFormat(
"0x%06X", addr);
194 "Check if this byte should be 0x00. If not, restore from backup.";
203 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
211 finding.
id =
"bank00_erasure";
213 finding.
message =
"Large block of zeros detected in Bank 00 code region";
214 finding.
location = absl::StrFormat(
"Around 0x%06X", i);
216 "ROM is likely corrupted. Restore from backup.";
225 for (uint32_t bank = 0; bank < size / 0x8000; ++bank) {
229 finding.
id =
"low_entropy_bank";
231 finding.
message = absl::StrFormat(
232 "Very low entropy (%.2f) detected in Bank %02X. Region might be erased or uninitialized.",
234 finding.
location = absl::StrFormat(
"Bank %02X", bank);
242 uint32_t high_table =
249 if (high_table + map_count < size && low_table + map_count < size) {
250 for (
int i = 0; i < map_count; ++i) {
251 uint8_t high = data[high_table + i];
252 uint16_t low = data[low_table + i] | (data[low_table + i + map_count] << 8);
253 uint32_t target = (high << 16) | low;
256 uint32_t pc_addr = 0;
257 if ((target & 0x7FFF) >= 0 && target < 0xFF0000) {
258 pc_addr = ((target & 0x7F0000) >> 1) | (target & 0x7FFF);
261 if (pc_addr >= size && target != 0) {
263 finding.
id =
"invalid_map_pointer";
265 finding.
message = absl::StrFormat(
266 "Map %02X points to invalid SNES address 0x%06X", i, target);
267 finding.
location = absl::StrFormat(
"Map %02X Pointer", i);
281 const auto* data = rom->
data();
282 size_t size = rom->
size();
286 bool all_empty =
true;
289 if (data[i] != 0xFF && data[i] != 0x00) {
297 finding.
id =
"empty_expanded_tile16";
300 "Expanded Tile16 region appears to be empty/uninitialized";
301 finding.
location =
"0x1E8000-0x1F0000";
303 "Re-save Tile16 data from editor or re-apply expansion patch.";
313 std::vector<uint8_t> rom_data_copy = rom->
vector();
318 static_cast<int>(rom_data_copy.size()));
320 bool pw_string_found =
false;
321 for (
const auto& msg : messages) {
322 if (absl::StrContains(msg.ContentsParsed,
"PARALLEL WORLDS") ||
323 absl::StrContains(msg.ContentsParsed,
"Parallel Worlds")) {
324 pw_string_found =
true;
329 if (pw_string_found) {
331 finding.
id =
"parallel_worlds_string";
333 finding.
message =
"Found 'PARALLEL WORLDS' string in message data";
345 const auto* data = rom->
data();
346 size_t size = rom->
size();
348 bool has_zscustom_features =
false;
349 std::vector<std::string> features_found;
353 has_zscustom_features =
true;
354 features_found.push_back(
"Custom BG");
358 has_zscustom_features =
true;
359 features_found.push_back(
"Custom Palette");
364 finding.
id =
"zscustom_features_detected";
366 finding.
message = absl::StrFormat(
367 "ZSCustom features detected despite missing version header: %s",
368 absl::StrJoin(features_found,
", "));
369 finding.
location =
"ZSCustom Flags";
384 const bool verbose = parser.
HasFlag(
"verbose");
385 const bool deep = parser.
HasFlag(
"deep");
386 const bool is_json = formatter.
IsJson();
388 OutputTextBanner(is_json);
395 formatter.
AddField(
"size_bytes",
static_cast<int>(rom->
size()));
400 (rom->
size() == kVanillaSize || rom->
size() == kExpandedSize);
401 formatter.
AddField(
"size_valid", size_valid);
403 if (rom->
size() == kVanillaSize) {
404 formatter.
AddField(
"size_type",
"vanilla_1mb");
405 }
else if (rom->
size() == kExpandedSize) {
406 formatter.
AddField(
"size_type",
"expanded_2mb");
408 formatter.
AddField(
"size_type",
"non_standard");
411 finding.
id =
"non_standard_size";
413 finding.
message = absl::StrFormat(
414 "Non-standard ROM size: 0x%zX bytes (expected 0x%zX or 0x%zX)",
415 rom->
size(), kVanillaSize, kExpandedSize);
422 auto header = ReadRomHeader(rom);
424 formatter.
AddField(
"title", header.title);
425 formatter.
AddField(
"map_mode", GetMapModeName(header.map_mode));
426 formatter.
AddHexField(
"rom_type", header.rom_type, 2);
427 formatter.
AddField(
"rom_size_header", 1 << (header.rom_size + 10));
429 header.sram_size > 0 ? (1 << (header.sram_size + 10)) : 0);
430 formatter.
AddField(
"country", GetCountryName(header.country));
431 formatter.
AddField(
"version", header.version);
432 formatter.
AddHexField(
"checksum_complement", header.checksum_complement, 4);
433 formatter.
AddHexField(
"checksum", header.checksum, 4);
434 formatter.
AddField(
"checksum_valid", header.checksum_valid);
436 if (!header.checksum_valid) {
438 finding.
id =
"invalid_checksum";
440 finding.
message = absl::StrFormat(
441 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, "
443 header.checksum_complement, header.checksum,
444 header.checksum_complement ^ header.checksum);
448 "ROM may be corrupted or modified without checksum update";
454 report.
features = DetectRomFeaturesLocal(rom);
459 formatter.
AddField(
"expanded_pointer_tables",
463 if (rom->
size() >= kExpandedSize) {
465 size_t free_bytes = 0;
466 for (
size_t i = 0x180000; i < 0x1E0000 && i < rom->
size(); ++i) {
467 if (rom->
data()[i] == 0x00 || rom->
data()[i] == 0xFF) {
471 formatter.
AddField(
"free_space_estimate",
static_cast<int>(free_bytes));
472 formatter.
AddField(
"free_space_region",
"0x180000-0x1E0000");
475 finding.
id =
"free_space_info";
477 finding.
message = absl::StrFormat(
478 "Estimated free space in expansion region: %zu bytes (%.1f KB)",
479 free_bytes, free_bytes / 1024.0);
480 finding.
location =
"0x180000-0x1E0000";
486 CheckCorruptionHeuristics(rom, report, deep);
492 finding.
id =
"parallel_worlds_detected";
494 finding.
message =
"Parallel Worlds (1.5MB) detected (Header check)";
497 "Use z3ed for editing. Custom pointer tables are supported.";
502 finding.
id =
"hm_corruption_detected";
504 finding.
message =
"Hyrule Magic corruption detected (Bank 00 erasure)";
512 CheckParallelWorldsHeuristics(rom, report);
513 CheckZScreamHeuristics(rom, report);
516 ValidateExpandedTables(rom, report);
526 if (!zones_or.ok()) {
528 finding.
id =
"water_fill_table_invalid";
530 finding.
message = absl::StrFormat(
"WaterFill table parse failed: %s",
531 zones_or.status().message());
532 finding.
location =
"Custom collision bank ($13:xxxx)";
534 "Restore from a known-good ROM or fix custom collision layout. "
535 "This must be resolved before using WaterFill authoring.";
538 }
else if (verbose) {
539 formatter.
AddField(
"water_fill_zone_count",
540 static_cast<int>(zones_or.value().size()));
546 for (
const auto& finding : report.
findings) {
562 std::cout <<
"╔════════════════════════════════════════════════════════════"
564 std::cout <<
"║ DIAGNOSTIC SUMMARY "
566 std::cout <<
"╠════════════════════════════════════════════════════════════"
568 std::cout << absl::StrFormat(
"║ ROM Title: %-49s ║\n", header.title);
569 std::cout << absl::StrFormat(
"║ Size: 0x%06zX bytes (%zu KB)%-26s ║\n",
570 rom->
size(), rom->
size() / 1024,
"");
571 std::cout << absl::StrFormat(
"║ Map Mode: %-50s ║\n",
572 GetMapModeName(header.map_mode));
573 std::cout << absl::StrFormat(
"║ Country: %-51s ║\n",
574 GetCountryName(header.country));
575 std::cout <<
"╠════════════════════════════════════════════════════════════"
577 std::cout << absl::StrFormat(
578 "║ Checksum: 0x%04X (complement: 0x%04X) - %s%-14s ║\n",
579 header.checksum, header.checksum_complement,
580 header.checksum_valid ?
"VALID" :
"INVALID",
"");
581 std::cout << absl::StrFormat(
"║ ZSCustomOverworld: %-41s ║\n",
583 std::cout << absl::StrFormat(
584 "║ Expanded Tile16: %-43s ║\n",
586 std::cout << absl::StrFormat(
587 "║ Expanded Tile32: %-43s ║\n",
589 std::cout << absl::StrFormat(
590 "║ Expanded Ptr Tables: %-39s ║\n",
592 std::cout <<
"╠════════════════════════════════════════════════════════════"
594 std::cout << absl::StrFormat(
595 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
598 std::cout <<
"╚════════════════════════════════════════════════════════════"
601 if (verbose && !report.
findings.empty()) {
602 std::cout <<
"\n=== Detailed Findings ===\n";
603 for (
const auto& finding : report.
findings) {
604 std::cout <<
" " << finding.FormatText() <<
"\n";
609 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
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.
bool HasBank00Erasure() const
bool IsParallelWorlds() const
constexpr size_t kExpandedSize
RomHeaderInfo ReadRomHeader(Rom *rom)
void CheckCorruptionHeuristics(Rom *rom, DiagnosticReport &report, bool deep)
std::string GetCountryName(uint8_t country)
double CalculateEntropy(const uint8_t *data, size_t size)
void CheckParallelWorldsHeuristics(Rom *rom, DiagnosticReport &report)
constexpr size_t kVanillaSize
void ValidateExpandedTables(Rom *rom, DiagnosticReport &report)
RomFeatures DetectRomFeaturesLocal(Rom *rom)
std::string GetMapModeName(uint8_t mode)
void CheckZScreamHeuristics(Rom *rom, DiagnosticReport &report)
Namespace for the command line interface.
constexpr uint32_t kExpandedPtrTableLow
constexpr uint32_t kCustomBGEnabledPos
constexpr uint32_t kExpandedPtrTableMarker
constexpr uint32_t kChecksumPos
constexpr uint32_t kPtrTableHighBase
constexpr uint8_t kExpandedPtrTableMagic
constexpr uint32_t kMap32ExpandedFlagPos
constexpr uint32_t kZSCustomVersionPos
constexpr int kExpandedMapCount
constexpr uint32_t kMap16ExpandedFlagPos
constexpr uint32_t kExpandedPtrTableHigh
const uint32_t kProblemAddresses[]
constexpr uint32_t kPtrTableLowBase
constexpr uint32_t kMap16TilesExpanded
constexpr uint32_t kMap16TilesExpandedEnd
constexpr uint32_t kSnesHeaderBase
constexpr int kVanillaMapCount
constexpr uint32_t kChecksumComplementPos
constexpr uint32_t kCustomMainPalettePos
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
A single diagnostic finding.
std::string suggested_action
DiagnosticSeverity severity
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.
bool has_expanded_pointer_tables
std::string GetVersionString() const
Get version as human-readable string.
uint8_t zs_custom_version
uint16_t checksum_complement