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();
184 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
192 finding.
id =
"bank00_erasure";
194 finding.
message =
"Large block of zeros detected in Bank 00 code region";
195 finding.
location = absl::StrFormat(
"Around 0x%06X", i);
197 "ROM is likely corrupted. Restore from backup.";
206 for (uint32_t bank = 0; bank < size / 0x8000; ++bank) {
210 finding.
id =
"low_entropy_bank";
212 finding.
message = absl::StrFormat(
213 "Very low entropy (%.2f) detected in Bank %02X. Region might be "
214 "erased or uninitialized.",
216 finding.
location = absl::StrFormat(
"Bank %02X", bank);
234 if (high_table + map_count < size && low_table + map_count < size) {
235 for (
int i = 0; i < map_count; ++i) {
236 uint8_t high = data[high_table + i];
238 data[low_table + i] | (data[low_table + i + map_count] << 8);
239 uint32_t target = (high << 16) | low;
242 uint32_t pc_addr = 0;
243 if ((target & 0x7FFF) >= 0 && target < 0xFF0000) {
244 pc_addr = ((target & 0x7F0000) >> 1) | (target & 0x7FFF);
247 if (pc_addr >= size && target != 0) {
249 finding.
id =
"invalid_map_pointer";
251 finding.
message = absl::StrFormat(
252 "Map %02X points to invalid SNES address 0x%06X", i, target);
253 finding.
location = absl::StrFormat(
"Map %02X Pointer", i);
267 const auto* data = rom->
data();
268 size_t size = rom->
size();
272 bool all_empty =
true;
275 if (data[i] != 0xFF && data[i] != 0x00) {
283 finding.
id =
"empty_expanded_tile16";
286 "Expanded Tile16 region appears to be empty/uninitialized";
287 finding.
location =
"0x1E8000-0x1F0000";
289 "Re-save Tile16 data from editor or re-apply expansion patch.";
299 std::vector<uint8_t> rom_data_copy = rom->
vector();
304 static_cast<int>(rom_data_copy.size()));
306 bool pw_string_found =
false;
307 for (
const auto& msg : messages) {
308 if (absl::StrContains(msg.ContentsParsed,
"PARALLEL WORLDS") ||
309 absl::StrContains(msg.ContentsParsed,
"Parallel Worlds")) {
310 pw_string_found =
true;
315 if (pw_string_found) {
317 finding.
id =
"parallel_worlds_string";
319 finding.
message =
"Found 'PARALLEL WORLDS' string in message data";
331 const auto* data = rom->
data();
332 size_t size = rom->
size();
334 bool has_zscustom_features =
false;
335 std::vector<std::string> features_found;
339 has_zscustom_features =
true;
340 features_found.push_back(
"Custom BG");
344 has_zscustom_features =
true;
345 features_found.push_back(
"Custom Palette");
350 finding.
id =
"zscustom_features_detected";
352 finding.
message = absl::StrFormat(
353 "ZSCustom features detected despite missing version header: %s",
354 absl::StrJoin(features_found,
", "));
355 finding.
location =
"ZSCustom Flags";
370 const bool verbose = parser.
HasFlag(
"verbose");
371 const bool deep = parser.
HasFlag(
"deep");
372 const bool is_json = formatter.
IsJson();
374 OutputTextBanner(is_json);
381 formatter.
AddField(
"size_bytes",
static_cast<int>(rom->
size()));
385 formatter.
AddField(
"copier_header_detection",
390 (rom->
size() == kVanillaSize || rom->
size() == kExpandedSize);
391 formatter.
AddField(
"size_valid", size_valid);
393 if (rom->
size() == kVanillaSize) {
394 formatter.
AddField(
"size_type",
"vanilla_1mb");
395 }
else if (rom->
size() == kExpandedSize) {
396 formatter.
AddField(
"size_type",
"expanded_2mb");
398 formatter.
AddField(
"size_type",
"non_standard");
401 finding.
id =
"non_standard_size";
403 finding.
message = absl::StrFormat(
404 "Non-standard ROM size: 0x%zX bytes (expected 0x%zX or 0x%zX)",
405 rom->
size(), kVanillaSize, kExpandedSize);
412 auto header = ReadRomHeader(rom);
414 formatter.
AddField(
"title", header.title);
415 formatter.
AddField(
"map_mode", GetMapModeName(header.map_mode));
416 formatter.
AddHexField(
"rom_type", header.rom_type, 2);
419 auto header_size_bytes = [](uint8_t code) {
420 return code <= 0x0F ? (0x400 << code) : 0;
422 formatter.
AddField(
"rom_size_header", header_size_bytes(header.rom_size));
423 formatter.
AddField(
"sram_size", header.sram_size > 0
424 ? header_size_bytes(header.sram_size)
426 formatter.
AddField(
"country", GetCountryName(header.country));
427 formatter.
AddField(
"version", header.version);
428 formatter.
AddHexField(
"checksum_complement", header.checksum_complement, 4);
429 formatter.
AddHexField(
"checksum", header.checksum, 4);
430 formatter.
AddField(
"checksum_valid", header.checksum_valid);
432 if (!header.checksum_valid) {
434 finding.
id =
"invalid_checksum";
436 finding.
message = absl::StrFormat(
437 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, "
439 header.checksum_complement, header.checksum,
440 header.checksum_complement ^ header.checksum);
444 "ROM may be corrupted or modified without checksum update";
450 report.
features = DetectRomFeaturesLocal(rom);
455 formatter.
AddField(
"expanded_pointer_tables",
459 if (rom->
size() >= kExpandedSize) {
461 size_t free_bytes = 0;
462 for (
size_t i = 0x180000; i < 0x1E0000 && i < rom->
size(); ++i) {
463 if (rom->
data()[i] == 0x00 || rom->
data()[i] == 0xFF) {
467 formatter.
AddField(
"free_space_estimate",
static_cast<int>(free_bytes));
468 formatter.
AddField(
"free_space_region",
"0x180000-0x1E0000");
471 finding.
id =
"free_space_info";
473 finding.
message = absl::StrFormat(
474 "Estimated free space in expansion region: %zu bytes (%.1f KB)",
475 free_bytes, free_bytes / 1024.0);
476 finding.
location =
"0x180000-0x1E0000";
482 CheckCorruptionHeuristics(rom, report, deep);
488 finding.
id =
"parallel_worlds_detected";
490 finding.
message =
"Parallel Worlds (1.5MB) detected (Header check)";
493 "Use z3ed for editing. Custom pointer tables are supported.";
498 finding.
id =
"hm_corruption_detected";
500 finding.
message =
"Hyrule Magic corruption detected (Bank 00 erasure)";
508 CheckParallelWorldsHeuristics(rom, report);
509 CheckZScreamHeuristics(rom, report);
512 ValidateExpandedTables(rom, report);
522 if (!zones_or.ok()) {
524 finding.
id =
"water_fill_table_invalid";
526 finding.
message = absl::StrFormat(
"WaterFill table parse failed: %s",
527 zones_or.status().message());
528 finding.
location =
"Custom collision bank ($13:xxxx)";
530 "Restore from a known-good ROM or fix custom collision layout. "
531 "This must be resolved before using WaterFill authoring.";
534 }
else if (verbose) {
535 formatter.
AddField(
"water_fill_zone_count",
536 static_cast<int>(zones_or.value().size()));
542 for (
const auto& finding : report.
findings) {
558 std::cout <<
"╔════════════════════════════════════════════════════════════"
560 std::cout <<
"║ DIAGNOSTIC SUMMARY "
562 std::cout <<
"╠════════════════════════════════════════════════════════════"
564 std::cout << absl::StrFormat(
"║ ROM Title: %-49s ║\n", header.title);
565 std::cout << absl::StrFormat(
"║ Size: 0x%06zX bytes (%zu KB)%-26s ║\n",
566 rom->
size(), rom->
size() / 1024,
"");
567 std::cout << absl::StrFormat(
"║ Map Mode: %-50s ║\n",
568 GetMapModeName(header.map_mode));
569 std::cout << absl::StrFormat(
"║ Country: %-51s ║\n",
570 GetCountryName(header.country));
571 std::cout <<
"╠════════════════════════════════════════════════════════════"
573 std::cout << absl::StrFormat(
574 "║ Checksum: 0x%04X (complement: 0x%04X) - %s%-14s ║\n",
575 header.checksum, header.checksum_complement,
576 header.checksum_valid ?
"VALID" :
"INVALID",
"");
577 std::cout << absl::StrFormat(
"║ ZSCustomOverworld: %-41s ║\n",
579 std::cout << absl::StrFormat(
580 "║ Expanded Tile16: %-43s ║\n",
582 std::cout << absl::StrFormat(
583 "║ Expanded Tile32: %-43s ║\n",
585 std::cout << absl::StrFormat(
586 "║ Expanded Ptr Tables: %-39s ║\n",
588 std::cout <<
"╠════════════════════════════════════════════════════════════"
590 std::cout << absl::StrFormat(
591 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
594 std::cout <<
"╚════════════════════════════════════════════════════════════"
597 if (verbose && !report.
findings.empty()) {
598 std::cout <<
"\n=== Detailed Findings ===\n";
599 for (
const auto& finding : report.
findings) {
600 std::cout <<
" " << finding.FormatText() <<
"\n";
605 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
CopierHeaderReason copier_header_reason() const
const auto & vector() const
bool has_copier_header() 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)
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
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, bool allow_bank_switch)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
const char * CopierHeaderReasonName(CopierHeaderReason reason)
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