5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
7#include "absl/strings/match.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();
83 switch (mode & 0x0F) {
84 case 0x00:
return "LoROM";
85 case 0x01:
return "HiROM";
86 case 0x02:
return "LoROM + S-DD1";
87 case 0x03:
return "LoROM + SA-1";
88 case 0x05:
return "ExHiROM";
89 default:
return absl::StrFormat(
"Unknown (0x%02X)", mode);
95 case 0x00:
return "Japan";
96 case 0x01:
return "USA";
97 case 0x02:
return "Europe";
98 default:
return absl::StrFormat(
"Unknown (0x%02X)", country);
102void OutputTextBanner(
bool is_json) {
105 std::cout <<
"╔═══════════════════════════════════════════════════════════════╗\n";
106 std::cout <<
"║ ROM DOCTOR ║\n";
107 std::cout <<
"║ File Integrity & Validation Tool ║\n";
108 std::cout <<
"╚═══════════════════════════════════════════════════════════════╝\n";
114 if (kZSCustomVersionPos < rom->size()) {
125 if (kMap16ExpandedFlagPos < rom->size()) {
130 if (kMap32ExpandedFlagPos < rom->size()) {
136 if (kExpandedPtrTableMarker < rom->size()) {
145 const auto* data = rom->
data();
146 size_t size = rom->
size();
155 if (data[addr] == 0x00) {
157 finding.
id =
"known_corruption_pattern";
159 finding.
message = absl::StrFormat(
"Potential corruption detected at known problematic address 0x%06X", addr);
160 finding.
location = absl::StrFormat(
"0x%06X", addr);
161 finding.
suggested_action =
"Check if this byte should be 0x00. If not, restore from backup.";
172 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
173 if (data[i] == 0x00) zero_run++;
178 finding.
id =
"bank00_erasure";
180 finding.
message =
"Large block of zeros detected in Bank 00 code region";
181 finding.
location = absl::StrFormat(
"Around 0x%06X", i);
193 const auto* data = rom->
data();
194 size_t size = rom->
size();
199 bool all_empty =
true;
201 if (data[i] != 0xFF && data[i] != 0x00) {
209 finding.
id =
"empty_expanded_tile16";
211 finding.
message =
"Expanded Tile16 region appears to be empty/uninitialized";
212 finding.
location =
"0x1E8000-0x1F0000";
213 finding.
suggested_action =
"Re-save Tile16 data from editor or re-apply expansion patch.";
223 std::vector<uint8_t> rom_data_copy = rom->
vector();
226 bool pw_string_found =
false;
227 for (
const auto& msg : messages) {
228 if (absl::StrContains(msg.ContentsParsed,
"PARALLEL WORLDS") ||
229 absl::StrContains(msg.ContentsParsed,
"Parallel Worlds")) {
230 pw_string_found =
true;
235 if (pw_string_found) {
237 finding.
id =
"parallel_worlds_string";
239 finding.
message =
"Found 'PARALLEL WORLDS' string in message data";
251 const auto* data = rom->
data();
252 size_t size = rom->
size();
254 bool has_zscustom_features =
false;
255 std::vector<std::string> features_found;
258 has_zscustom_features =
true;
259 features_found.push_back(
"Custom BG");
262 has_zscustom_features =
true;
263 features_found.push_back(
"Custom Palette");
268 finding.
id =
"zscustom_features_detected";
270 finding.
message = absl::StrFormat(
"ZSCustom features detected despite missing version header: %s", absl::StrJoin(features_found,
", "));
271 finding.
location =
"ZSCustom Flags";
286 const bool verbose = parser.
HasFlag(
"verbose");
287 const bool is_json = formatter.
IsJson();
289 OutputTextBanner(is_json);
296 formatter.
AddField(
"size_bytes",
static_cast<int>(rom->
size()));
300 bool size_valid = (rom->
size() == kVanillaSize || rom->
size() == kExpandedSize);
301 formatter.
AddField(
"size_valid", size_valid);
303 if (rom->
size() == kVanillaSize) {
304 formatter.
AddField(
"size_type",
"vanilla_1mb");
305 }
else if (rom->
size() == kExpandedSize) {
306 formatter.
AddField(
"size_type",
"expanded_2mb");
308 formatter.
AddField(
"size_type",
"non_standard");
311 finding.
id =
"non_standard_size";
313 finding.
message = absl::StrFormat(
314 "Non-standard ROM size: 0x%zX bytes (expected 0x%zX or 0x%zX)",
315 rom->
size(), kVanillaSize, kExpandedSize);
322 auto header = ReadRomHeader(rom);
324 formatter.
AddField(
"title", header.title);
325 formatter.
AddField(
"map_mode", GetMapModeName(header.map_mode));
326 formatter.
AddHexField(
"rom_type", header.rom_type, 2);
327 formatter.
AddField(
"rom_size_header", 1 << (header.rom_size + 10));
328 formatter.
AddField(
"sram_size", header.sram_size > 0 ? (1 << (header.sram_size + 10)) : 0);
329 formatter.
AddField(
"country", GetCountryName(header.country));
330 formatter.
AddField(
"version", header.version);
331 formatter.
AddHexField(
"checksum_complement", header.checksum_complement, 4);
332 formatter.
AddHexField(
"checksum", header.checksum, 4);
333 formatter.
AddField(
"checksum_valid", header.checksum_valid);
335 if (!header.checksum_valid) {
337 finding.
id =
"invalid_checksum";
339 finding.
message = absl::StrFormat(
340 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, expected 0xFFFF)",
341 header.checksum_complement, header.checksum,
342 header.checksum_complement ^ header.checksum);
344 finding.
suggested_action =
"ROM may be corrupted or modified without checksum update";
350 report.
features = DetectRomFeaturesLocal(rom);
358 if (rom->
size() >= kExpandedSize) {
360 size_t free_bytes = 0;
361 for (
size_t i = 0x180000; i < 0x1E0000 && i < rom->
size(); ++i) {
362 if (rom->
data()[i] == 0x00 || rom->
data()[i] == 0xFF) {
366 formatter.
AddField(
"free_space_estimate",
static_cast<int>(free_bytes));
367 formatter.
AddField(
"free_space_region",
"0x180000-0x1E0000");
370 finding.
id =
"free_space_info";
372 finding.
message = absl::StrFormat(
373 "Estimated free space in expansion region: %zu bytes (%.1f KB)",
374 free_bytes, free_bytes / 1024.0);
375 finding.
location =
"0x180000-0x1E0000";
381 CheckCorruptionHeuristics(rom, report);
387 finding.
id =
"parallel_worlds_detected";
389 finding.
message =
"Parallel Worlds (1.5MB) detected (Header check)";
391 finding.
suggested_action =
"Use z3ed for editing. Custom pointer tables are supported.";
396 finding.
id =
"hm_corruption_detected";
398 finding.
message =
"Hyrule Magic corruption detected (Bank 00 erasure)";
406 CheckParallelWorldsHeuristics(rom, report);
407 CheckZScreamHeuristics(rom, report);
412 ValidateExpandedTables(rom, report);
416 for (
const auto& finding : report.
findings) {
432 std::cout <<
"╔═══════════════════════════════════════════════════════════════╗\n";
433 std::cout <<
"║ DIAGNOSTIC SUMMARY ║\n";
434 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
435 std::cout << absl::StrFormat(
"║ ROM Title: %-49s ║\n", header.title);
436 std::cout << absl::StrFormat(
"║ Size: 0x%06zX bytes (%zu KB)%-26s ║\n",
437 rom->
size(), rom->
size() / 1024,
"");
438 std::cout << absl::StrFormat(
"║ Map Mode: %-50s ║\n", GetMapModeName(header.map_mode));
439 std::cout << absl::StrFormat(
"║ Country: %-51s ║\n", GetCountryName(header.country));
440 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
441 std::cout << absl::StrFormat(
"║ Checksum: 0x%04X (complement: 0x%04X) - %s%-14s ║\n",
442 header.checksum, header.checksum_complement,
443 header.checksum_valid ?
"VALID" :
"INVALID",
"");
444 std::cout << absl::StrFormat(
"║ ZSCustomOverworld: %-41s ║\n",
446 std::cout << absl::StrFormat(
"║ Expanded Tile16: %-43s ║\n",
448 std::cout << absl::StrFormat(
"║ Expanded Tile32: %-43s ║\n",
450 std::cout << absl::StrFormat(
"║ Expanded Ptr Tables: %-39s ║\n",
452 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
453 std::cout << absl::StrFormat(
"║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
456 std::cout <<
"╚═══════════════════════════════════════════════════════════════╝\n";
458 if (verbose && !report.
findings.empty()) {
459 std::cout <<
"\n=== Detailed Findings ===\n";
460 for (
const auto& finding : report.
findings) {
461 std::cout <<
" " << finding.FormatText() <<
"\n";
466 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