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 <cmath>
4#include <iostream>
5#include <numeric>
6
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"
13#include "rom/hm_support.h"
14#include "rom/rom.h"
16
17namespace yaze::cli {
18
19namespace {
20
21// ROM header locations (LoROM)
22// kSnesHeaderBase, kChecksumComplementPos, kChecksumPos defined in diagnostic_types.h
23
24// Expected sizes
25constexpr size_t kVanillaSize = 0x100000; // 1MB
26constexpr size_t kExpandedSize = 0x200000; // 2MB
27
29 std::string title;
30 uint8_t map_mode = 0;
31 uint8_t rom_type = 0;
32 uint8_t rom_size = 0;
33 uint8_t sram_size = 0;
34 uint8_t country = 0;
35 uint8_t license = 0;
36 uint8_t version = 0;
37 uint16_t checksum_complement = 0;
38 uint16_t checksum = 0;
39 bool checksum_valid = false;
40};
41
43 RomHeaderInfo info;
44 const auto& data = rom->data();
45
46 if (rom->size() < yaze::cli::kSnesHeaderBase + 32) {
47 return info;
48 }
49
50 // Read title (21 bytes)
51 for (int i = 0; i < 21; ++i) {
52 char chr = static_cast<char>(data[yaze::cli::kSnesHeaderBase + i]);
53 if (chr >= 32 && chr < 127) {
54 info.title += chr;
55 }
56 }
57
58 // Trim trailing spaces
59 while (!info.title.empty() && info.title.back() == ' ') {
60 info.title.pop_back();
61 }
62
63 info.map_mode = data[yaze::cli::kSnesHeaderBase + 21];
64 info.rom_type = data[yaze::cli::kSnesHeaderBase + 22];
65 info.rom_size = data[yaze::cli::kSnesHeaderBase + 23];
66 info.sram_size = data[yaze::cli::kSnesHeaderBase + 24];
67 info.country = data[yaze::cli::kSnesHeaderBase + 25];
68 info.license = data[yaze::cli::kSnesHeaderBase + 26];
69 info.version = data[yaze::cli::kSnesHeaderBase + 27];
70
71 // Read checksums
73 (data[yaze::cli::kChecksumComplementPos + 1] << 8);
74 info.checksum =
75 data[yaze::cli::kChecksumPos] | (data[yaze::cli::kChecksumPos + 1] << 8);
76
77 // Validate checksum (complement XOR checksum should be 0xFFFF)
78 info.checksum_valid = ((info.checksum_complement ^ info.checksum) == 0xFFFF);
79
80 return info;
81}
82
83std::string GetMapModeName(uint8_t mode) {
84 switch (mode & 0x0F) {
85 case 0x00:
86 return "LoROM";
87 case 0x01:
88 return "HiROM";
89 case 0x02:
90 return "LoROM + S-DD1";
91 case 0x03:
92 return "LoROM + SA-1";
93 case 0x05:
94 return "ExHiROM";
95 default:
96 return absl::StrFormat("Unknown (0x%02X)", mode);
97 }
98}
99
100std::string GetCountryName(uint8_t country) {
101 switch (country) {
102 case 0x00:
103 return "Japan";
104 case 0x01:
105 return "USA";
106 case 0x02:
107 return "Europe";
108 default:
109 return absl::StrFormat("Unknown (0x%02X)", country);
110 }
111}
112
113void OutputTextBanner(bool is_json) {
114 if (is_json)
115 return;
116 std::cout << "\n";
117 std::cout
118 << "╔═══════════════════════════════════════════════════════════════╗\n";
119 std::cout
120 << "║ ROM DOCTOR ║\n";
121 std::cout
122 << "║ File Integrity & Validation Tool ║\n";
123 std::cout
124 << "╚═══════════════════════════════════════════════════════════════╝\n";
125}
126
128 RomFeatures features;
129
130 if (kZSCustomVersionPos < rom->size()) {
131 features.zs_custom_version = rom->data()[kZSCustomVersionPos];
132 features.is_vanilla = (features.zs_custom_version == 0xFF ||
133 features.zs_custom_version == 0x00);
134 features.is_v2 = (!features.is_vanilla && features.zs_custom_version == 2);
135 features.is_v3 = (!features.is_vanilla && features.zs_custom_version >= 3);
136 } else {
137 features.is_vanilla = true;
138 }
139
140 if (!features.is_vanilla) {
141 if (kMap16ExpandedFlagPos < rom->size()) {
142 uint8_t flag = rom->data()[kMap16ExpandedFlagPos];
143 features.has_expanded_tile16 = (flag != 0x0F);
144 }
145
146 if (kMap32ExpandedFlagPos < rom->size()) {
147 uint8_t flag = rom->data()[kMap32ExpandedFlagPos];
148 features.has_expanded_tile32 = (flag != 0x04);
149 }
150 }
151
152 if (kExpandedPtrTableMarker < rom->size()) {
155 }
156
157 return features;
158}
159
160double CalculateEntropy(const uint8_t* data, size_t size) {
161 if (size == 0)
162 return 0.0;
163 std::array<size_t, 256> counts = {0};
164 for (size_t i = 0; i < size; ++i) {
165 counts[data[i]]++;
166 }
167
168 double entropy = 0.0;
169 for (size_t count : counts) {
170 if (count > 0) {
171 double p = static_cast<double>(count) / size;
172 entropy -= p * std::log2(p);
173 }
174 }
175 return entropy;
176}
177
178void CheckCorruptionHeuristics(Rom* rom, DiagnosticReport& report, bool deep) {
179 const auto* data = rom->data();
180 size_t size = rom->size();
181
182 // Check for zero-filled blocks in critical code regions (Bank 00)
183 int zero_run = 0;
184 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
185 if (data[i] == 0x00)
186 zero_run++;
187 else
188 zero_run = 0;
189
190 if (zero_run > 64) {
191 DiagnosticFinding finding;
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);
196 finding.suggested_action =
197 "ROM is likely corrupted. Restore from backup.";
198 finding.fixable = false;
199 report.AddFinding(finding);
200 break;
201 }
202 }
203
204 if (deep) {
205 // Perform full ROM entropy scan per 32KB bank
206 for (uint32_t bank = 0; bank < size / 0x8000; ++bank) {
207 double entropy = CalculateEntropy(data + (bank * 0x8000), 0x8000);
208 if (entropy < 0.5) {
209 DiagnosticFinding finding;
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.",
215 entropy, bank);
216 finding.location = absl::StrFormat("Bank %02X", bank);
217 finding.suggested_action = "Verify if this bank should contain data.";
218 finding.fixable = false;
219 report.AddFinding(finding);
220 }
221 }
222
223 // Check for pointer chain integrity in overworld maps
224 uint32_t high_table = report.features.has_expanded_pointer_tables
227 uint32_t low_table = report.features.has_expanded_pointer_tables
230 int map_count = report.features.has_expanded_pointer_tables
233
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];
237 uint16_t low =
238 data[low_table + i] | (data[low_table + i + map_count] << 8);
239 uint32_t target = (high << 16) | low;
240
241 // LoROM address translation (simplified check)
242 uint32_t pc_addr = 0;
243 if ((target & 0x7FFF) >= 0 && target < 0xFF0000) {
244 pc_addr = ((target & 0x7F0000) >> 1) | (target & 0x7FFF);
245 }
246
247 if (pc_addr >= size && target != 0) {
248 DiagnosticFinding finding;
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);
254 finding.suggested_action = "Fix the pointer in the overworld editor.";
255 finding.fixable = false;
256 report.AddFinding(finding);
257 }
258 }
259 }
260 }
261}
262
264 if (!report.features.has_expanded_tile16)
265 return;
266
267 const auto* data = rom->data();
268 size_t size = rom->size();
269
270 // Check Tile16 expansion region (0x1E8000 - 0x1F0000)
271 if (size >= kMap16TilesExpandedEnd) {
272 bool all_empty = true;
273 for (uint32_t i = kMap16TilesExpanded; i < kMap16TilesExpandedEnd;
274 i += 256) {
275 if (data[i] != 0xFF && data[i] != 0x00) {
276 all_empty = false;
277 break;
278 }
279 }
280
281 if (all_empty) {
282 DiagnosticFinding finding;
283 finding.id = "empty_expanded_tile16";
285 finding.message =
286 "Expanded Tile16 region appears to be empty/uninitialized";
287 finding.location = "0x1E8000-0x1F0000";
288 finding.suggested_action =
289 "Re-save Tile16 data from editor or re-apply expansion patch.";
290 finding.fixable = false;
291 report.AddFinding(finding);
292 }
293 }
294}
295
297 // 1. Search for "PARALLEL WORLDS" string in decoded messages
298 try {
299 std::vector<uint8_t> rom_data_copy = rom->vector(); // Copy for safety
300 // Safety: dummy/invalid ROMs may not contain proper message terminators.
301 // Bound parsing to the ROM size to avoid out-of-bounds reads (signals).
302 auto messages = yaze::editor::ReadAllTextData(
303 rom_data_copy.data(), yaze::editor::kTextData,
304 static_cast<int>(rom_data_copy.size()));
305
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;
311 break;
312 }
313 }
314
315 if (pw_string_found) {
316 DiagnosticFinding finding;
317 finding.id = "parallel_worlds_string";
319 finding.message = "Found 'PARALLEL WORLDS' string in message data";
320 finding.location = "Message Data";
321 finding.suggested_action = "Confirmed Parallel Worlds ROM.";
322 finding.fixable = false;
323 report.AddFinding(finding);
324 }
325 } catch (...) {
326 // Ignore parsing errors
327 }
328}
329
331 const auto* data = rom->data();
332 size_t size = rom->size();
333
334 bool has_zscustom_features = false;
335 std::vector<std::string> features_found;
336
337 if (kCustomBGEnabledPos < size && data[kCustomBGEnabledPos] != 0x00 &&
338 data[kCustomBGEnabledPos] != 0xFF) {
339 has_zscustom_features = true;
340 features_found.push_back("Custom BG");
341 }
342 if (kCustomMainPalettePos < size && data[kCustomMainPalettePos] != 0x00 &&
343 data[kCustomMainPalettePos] != 0xFF) {
344 has_zscustom_features = true;
345 features_found.push_back("Custom Palette");
346 }
347
348 if (has_zscustom_features && report.features.is_vanilla) {
349 DiagnosticFinding finding;
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";
356 finding.suggested_action = "Treat as ZSCustom ROM.";
357 finding.fixable = false;
358 report.AddFinding(finding);
359
360 report.features.is_vanilla = false;
361 report.features.zs_custom_version = 0xFE; // Unknown/Detected
362 }
363}
364
365} // namespace
366
368 Rom* rom, const resources::ArgumentParser& parser,
369 resources::OutputFormatter& formatter) {
370 const bool verbose = parser.HasFlag("verbose");
371 const bool deep = parser.HasFlag("deep");
372 const bool is_json = formatter.IsJson();
373
374 OutputTextBanner(is_json);
375
376 DiagnosticReport report;
377 report.rom_path = rom->filename();
378
379 // Basic ROM info
380 formatter.AddField("rom_path", rom->filename());
381 formatter.AddField("size_bytes", static_cast<int>(rom->size()));
382 formatter.AddHexField("size_hex", rom->size(), 6);
383 // Sizes above exclude a copier header stripped at load.
384 formatter.AddField("copier_header", rom->has_copier_header());
385 formatter.AddField("copier_header_detection",
387
388 // Size validation
389 bool size_valid =
390 (rom->size() == kVanillaSize || rom->size() == kExpandedSize);
391 formatter.AddField("size_valid", size_valid);
392
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");
397 } else {
398 formatter.AddField("size_type", "non_standard");
399
400 DiagnosticFinding finding;
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);
406 finding.location = "ROM file";
407 finding.fixable = false;
408 report.AddFinding(finding);
409 }
410
411 // Read and validate header
412 auto header = ReadRomHeader(rom);
413
414 formatter.AddField("title", header.title);
415 formatter.AddField("map_mode", GetMapModeName(header.map_mode));
416 formatter.AddHexField("rom_type", header.rom_type, 2);
417 // Header size codes mean 0x400 << code bytes. Codes past 0x0F (32 MB) are
418 // not SNES sizes and would overflow the shift, so report them as 0.
419 auto header_size_bytes = [](uint8_t code) {
420 return code <= 0x0F ? (0x400 << code) : 0;
421 };
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)
425 : 0);
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);
431
432 if (!header.checksum_valid) {
433 DiagnosticFinding finding;
434 finding.id = "invalid_checksum";
436 finding.message = absl::StrFormat(
437 "Invalid SNES checksum: complement=0x%04X checksum=0x%04X (XOR=0x%04X, "
438 "expected 0xFFFF)",
439 header.checksum_complement, header.checksum,
440 header.checksum_complement ^ header.checksum);
441 finding.location =
442 absl::StrFormat("0x%04X", yaze::cli::kChecksumComplementPos);
443 finding.suggested_action =
444 "ROM may be corrupted or modified without checksum update";
445 finding.fixable = true; // Could be fixed by recalculating
446 report.AddFinding(finding);
447 }
448
449 // Detect ZSCustomOverworld version
450 report.features = DetectRomFeaturesLocal(rom);
451 formatter.AddField("zs_custom_version", report.features.GetVersionString());
452 formatter.AddField("is_vanilla", report.features.is_vanilla);
453 formatter.AddField("expanded_tile16", report.features.has_expanded_tile16);
454 formatter.AddField("expanded_tile32", report.features.has_expanded_tile32);
455 formatter.AddField("expanded_pointer_tables",
457
458 // Free space analysis (simplified)
459 if (rom->size() >= kExpandedSize) {
460 // Check for free space in expansion region
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) {
464 free_bytes++;
465 }
466 }
467 formatter.AddField("free_space_estimate", static_cast<int>(free_bytes));
468 formatter.AddField("free_space_region", "0x180000-0x1E0000");
469
470 DiagnosticFinding finding;
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";
477 finding.fixable = false;
478 report.AddFinding(finding);
479 }
480
481 // 3. Check for corruption heuristics
482 CheckCorruptionHeuristics(rom, report, deep);
483
484 // 4. Hyrule Magic / Parallel Worlds Analysis
485 yaze::rom::HyruleMagicValidator hm_validator(rom);
486 if (hm_validator.IsParallelWorlds()) {
487 DiagnosticFinding finding;
488 finding.id = "parallel_worlds_detected";
490 finding.message = "Parallel Worlds (1.5MB) detected (Header check)";
491 finding.location = "ROM Header";
492 finding.suggested_action =
493 "Use z3ed for editing. Custom pointer tables are supported.";
494 finding.fixable = false;
495 report.AddFinding(finding);
496 } else if (hm_validator.HasBank00Erasure()) {
497 DiagnosticFinding finding;
498 finding.id = "hm_corruption_detected";
500 finding.message = "Hyrule Magic corruption detected (Bank 00 erasure)";
501 finding.location = "Bank 00";
502 finding.suggested_action = "ROM is likely unstable. Restore from backup.";
503 finding.fixable = false;
504 report.AddFinding(finding);
505 }
506
507 // Advanced Heuristics
508 CheckParallelWorldsHeuristics(rom, report);
509 CheckZScreamHeuristics(rom, report);
510
511 // 5. Validate expanded tables
512 ValidateExpandedTables(rom, report);
513
514 // 6. Oracle of Secrets: validate WaterFill reserved region integrity.
515 //
516 // This is a ROM safety check for editor-authored water fill zones which
517 // reserve a tail region inside the expanded custom collision bank. If custom
518 // collision data overlaps that reserved tail, the ROM layout is incompatible
519 // with the WaterFill table format and yaze must not attempt to use it.
520 {
521 auto zones_or = yaze::zelda3::LoadWaterFillTable(rom);
522 if (!zones_or.ok()) {
523 DiagnosticFinding finding;
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)";
529 finding.suggested_action =
530 "Restore from a known-good ROM or fix custom collision layout. "
531 "This must be resolved before using WaterFill authoring.";
532 finding.fixable = false;
533 report.AddFinding(finding);
534 } else if (verbose) {
535 formatter.AddField("water_fill_zone_count",
536 static_cast<int>(zones_or.value().size()));
537 }
538 }
539
540 // Output findings
541 formatter.BeginArray("findings");
542 for (const auto& finding : report.findings) {
543 formatter.AddArrayItem(finding.FormatJson());
544 }
545 formatter.EndArray();
546
547 // Summary
548 formatter.AddField("total_findings", report.TotalFindings());
549 formatter.AddField("critical_count", report.critical_count);
550 formatter.AddField("error_count", report.error_count);
551 formatter.AddField("warning_count", report.warning_count);
552 formatter.AddField("info_count", report.info_count);
553 formatter.AddField("has_problems", report.HasProblems());
554
555 // Text output
556 if (!is_json) {
557 std::cout << "\n";
558 std::cout << "╔════════════════════════════════════════════════════════════"
559 "═══╗\n";
560 std::cout << "║ DIAGNOSTIC SUMMARY "
561 " ║\n";
562 std::cout << "╠════════════════════════════════════════════════════════════"
563 "═══╣\n";
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 << "╠════════════════════════════════════════════════════════════"
572 "═══╣\n";
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",
578 report.features.GetVersionString());
579 std::cout << absl::StrFormat(
580 "║ Expanded Tile16: %-43s ║\n",
581 report.features.has_expanded_tile16 ? "YES" : "NO");
582 std::cout << absl::StrFormat(
583 "║ Expanded Tile32: %-43s ║\n",
584 report.features.has_expanded_tile32 ? "YES" : "NO");
585 std::cout << absl::StrFormat(
586 "║ Expanded Ptr Tables: %-39s ║\n",
587 report.features.has_expanded_pointer_tables ? "YES" : "NO");
588 std::cout << "╠════════════════════════════════════════════════════════════"
589 "═══╣\n";
590 std::cout << absl::StrFormat(
591 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
592 report.TotalFindings(), report.error_count, report.warning_count,
593 report.info_count, "");
594 std::cout << "╚════════════════════════════════════════════════════════════"
595 "═══╝\n";
596
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";
601 }
602 }
603 }
604
605 return absl::OkStatus();
606}
607
608} // 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:49
CopierHeaderReason copier_header_reason() const
Definition rom.h:215
auto filename() const
Definition rom.h:207
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool has_copier_header() const
Definition rom.h:213
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 CheckCorruptionHeuristics(Rom *rom, DiagnosticReport &report, bool deep)
double CalculateEntropy(const uint8_t *data, size_t size)
void CheckParallelWorldsHeuristics(Rom *rom, DiagnosticReport &report)
void ValidateExpandedTables(Rom *rom, DiagnosticReport &report)
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
constexpr int kTextData
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)
Definition rom.cc:402
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.