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"
31 uint8_t sram_size = 0;
35 uint16_t checksum_complement = 0;
36 uint16_t checksum = 0;
37 bool checksum_valid =
false;
42 const auto& data = rom->
data();
49 for (
int i = 0; i < 21; ++i) {
51 if (chr >= 32 && chr < 127) {
57 while (!info.
title.empty() && info.
title.back() ==
' ') {
58 info.
title.pop_back();
82 switch (mode & 0x0F) {
88 return "LoROM + S-DD1";
90 return "LoROM + SA-1";
94 return absl::StrFormat(
"Unknown (0x%02X)", mode);
107 return absl::StrFormat(
"Unknown (0x%02X)", country);
111void OutputTextBanner(
bool is_json) {
116 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
118 <<
"║ ROM DOCTOR ║\n";
120 <<
"║ File Integrity & Validation Tool ║\n";
122 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
128 if (kZSCustomVersionPos < rom->size()) {
139 if (kMap16ExpandedFlagPos < rom->size()) {
144 if (kMap32ExpandedFlagPos < rom->size()) {
150 if (kExpandedPtrTableMarker < rom->size()) {
159 const auto* data = rom->
data();
160 size_t size = rom->
size();
169 if (data[addr] == 0x00) {
171 finding.
id =
"known_corruption_pattern";
173 finding.
message = absl::StrFormat(
174 "Potential corruption detected at known problematic address 0x%06X",
176 finding.
location = absl::StrFormat(
"0x%06X", addr);
178 "Check if this byte should be 0x00. If not, restore from backup.";
189 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
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);
202 "ROM is likely corrupted. Restore from backup.";
214 const auto* data = rom->
data();
215 size_t size = rom->
size();
220 bool all_empty =
true;
223 if (data[i] != 0xFF && data[i] != 0x00) {
231 finding.
id =
"empty_expanded_tile16";
234 "Expanded Tile16 region appears to be empty/uninitialized";
235 finding.
location =
"0x1E8000-0x1F0000";
237 "Re-save Tile16 data from editor or re-apply expansion patch.";
247 std::vector<uint8_t> rom_data_copy = rom->
vector();
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;
259 if (pw_string_found) {
261 finding.
id =
"parallel_worlds_string";
263 finding.
message =
"Found 'PARALLEL WORLDS' string in message data";
275 const auto* data = rom->
data();
276 size_t size = rom->
size();
278 bool has_zscustom_features =
false;
279 std::vector<std::string> features_found;
283 has_zscustom_features =
true;
284 features_found.push_back(
"Custom BG");
288 has_zscustom_features =
true;
289 features_found.push_back(
"Custom Palette");
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";
314 const bool verbose = parser.
HasFlag(
"verbose");
315 const bool is_json = formatter.
IsJson();
317 OutputTextBanner(is_json);
324 formatter.
AddField(
"size_bytes",
static_cast<int>(rom->
size()));
329 (rom->
size() == kVanillaSize || rom->
size() == kExpandedSize);
330 formatter.
AddField(
"size_valid", size_valid);
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");
337 formatter.
AddField(
"size_type",
"non_standard");
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);
351 auto header = ReadRomHeader(rom);
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));
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);
365 if (!header.checksum_valid) {
367 finding.
id =
"invalid_checksum";
369 finding.
message = absl::StrFormat(
370 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, "
372 header.checksum_complement, header.checksum,
373 header.checksum_complement ^ header.checksum);
377 "ROM may be corrupted or modified without checksum update";
383 report.
features = DetectRomFeaturesLocal(rom);
388 formatter.
AddField(
"expanded_pointer_tables",
392 if (rom->
size() >= kExpandedSize) {
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) {
400 formatter.
AddField(
"free_space_estimate",
static_cast<int>(free_bytes));
401 formatter.
AddField(
"free_space_region",
"0x180000-0x1E0000");
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";
415 CheckCorruptionHeuristics(rom, report);
421 finding.
id =
"parallel_worlds_detected";
423 finding.
message =
"Parallel Worlds (1.5MB) detected (Header check)";
426 "Use z3ed for editing. Custom pointer tables are supported.";
431 finding.
id =
"hm_corruption_detected";
433 finding.
message =
"Hyrule Magic corruption detected (Bank 00 erasure)";
441 CheckParallelWorldsHeuristics(rom, report);
442 CheckZScreamHeuristics(rom, report);
445 ValidateExpandedTables(rom, report);
449 for (
const auto& finding : report.
findings) {
465 std::cout <<
"╔════════════════════════════════════════════════════════════"
467 std::cout <<
"║ DIAGNOSTIC SUMMARY "
469 std::cout <<
"╠════════════════════════════════════════════════════════════"
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 <<
"╠════════════════════════════════════════════════════════════"
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",
486 std::cout << absl::StrFormat(
487 "║ Expanded Tile16: %-43s ║\n",
489 std::cout << absl::StrFormat(
490 "║ Expanded Tile32: %-43s ║\n",
492 std::cout << absl::StrFormat(
493 "║ Expanded Ptr Tables: %-39s ║\n",
495 std::cout <<
"╠════════════════════════════════════════════════════════════"
497 std::cout << absl::StrFormat(
498 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
501 std::cout <<
"╚════════════════════════════════════════════════════════════"
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";
512 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)
std::string GetCountryName(uint8_t country)
void CheckParallelWorldsHeuristics(Rom *rom, DiagnosticReport &report)
constexpr size_t kVanillaSize
void CheckCorruptionHeuristics(Rom *rom, DiagnosticReport &report)
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 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.
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