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 <iostream>
4
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"
9#include "rom/rom.h"
11#include "rom/hm_support.h"
13
14namespace yaze::cli {
15
16namespace {
17
18// ROM header locations (LoROM)
19// ROM header locations (LoROM)
20// kSnesHeaderBase, kChecksumComplementPos, kChecksumPos defined in diagnostic_types.h
21
22// Expected sizes
23constexpr size_t kVanillaSize = 0x100000; // 1MB
24constexpr size_t kExpandedSize = 0x200000; // 2MB
25
27 std::string title;
28 uint8_t map_mode = 0;
29 uint8_t rom_type = 0;
30 uint8_t rom_size = 0;
31 uint8_t sram_size = 0;
32 uint8_t country = 0;
33 uint8_t license = 0;
34 uint8_t version = 0;
35 uint16_t checksum_complement = 0;
36 uint16_t checksum = 0;
37 bool checksum_valid = false;
38};
39
41 RomHeaderInfo info;
42 const auto& data = rom->data();
43
44 if (rom->size() < yaze::cli::kSnesHeaderBase + 32) {
45 return info;
46 }
47
48 // Read title (21 bytes)
49 for (int i = 0; i < 21; ++i) {
50 char chr = static_cast<char>(data[yaze::cli::kSnesHeaderBase + i]);
51 if (chr >= 32 && chr < 127) {
52 info.title += chr;
53 }
54 }
55
56 // Trim trailing spaces
57 while (!info.title.empty() && info.title.back() == ' ') {
58 info.title.pop_back();
59 }
60
61 info.map_mode = data[yaze::cli::kSnesHeaderBase + 21];
62 info.rom_type = data[yaze::cli::kSnesHeaderBase + 22];
63 info.rom_size = data[yaze::cli::kSnesHeaderBase + 23];
64 info.sram_size = data[yaze::cli::kSnesHeaderBase + 24];
65 info.country = data[yaze::cli::kSnesHeaderBase + 25];
66 info.license = data[yaze::cli::kSnesHeaderBase + 26];
67 info.version = data[yaze::cli::kSnesHeaderBase + 27];
68
69 // Read checksums
72 info.checksum =
73 data[yaze::cli::kChecksumPos] | (data[yaze::cli::kChecksumPos + 1] << 8);
74
75 // Validate checksum (complement XOR checksum should be 0xFFFF)
76 info.checksum_valid =
77 ((info.checksum_complement ^ info.checksum) == 0xFFFF);
78
79 return info;
80}
81
82std::string GetMapModeName(uint8_t mode) {
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);
90 }
91}
92
93std::string GetCountryName(uint8_t country) {
94 switch (country) {
95 case 0x00: return "Japan";
96 case 0x01: return "USA";
97 case 0x02: return "Europe";
98 default: return absl::StrFormat("Unknown (0x%02X)", country);
99 }
100}
101
102void OutputTextBanner(bool is_json) {
103 if (is_json) return;
104 std::cout << "\n";
105 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
106 std::cout << "║ ROM DOCTOR ║\n";
107 std::cout << "║ File Integrity & Validation Tool ║\n";
108 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
109}
110
112 RomFeatures features;
113
114 if (kZSCustomVersionPos < rom->size()) {
115 features.zs_custom_version = rom->data()[kZSCustomVersionPos];
116 features.is_vanilla =
117 (features.zs_custom_version == 0xFF || features.zs_custom_version == 0x00);
118 features.is_v2 = (!features.is_vanilla && features.zs_custom_version == 2);
119 features.is_v3 = (!features.is_vanilla && features.zs_custom_version >= 3);
120 } else {
121 features.is_vanilla = true;
122 }
123
124 if (!features.is_vanilla) {
125 if (kMap16ExpandedFlagPos < rom->size()) {
126 uint8_t flag = rom->data()[kMap16ExpandedFlagPos];
127 features.has_expanded_tile16 = (flag != 0x0F);
128 }
129
130 if (kMap32ExpandedFlagPos < rom->size()) {
131 uint8_t flag = rom->data()[kMap32ExpandedFlagPos];
132 features.has_expanded_tile32 = (flag != 0x04);
133 }
134 }
135
136 if (kExpandedPtrTableMarker < rom->size()) {
139 }
140
141 return features;
142}
143
145 const auto* data = rom->data();
146 size_t size = rom->size();
147
148 // Check known problematic addresses
149 for (uint32_t addr : kProblemAddresses) {
150 if (addr < size) {
151 // Heuristic: If we find 0x00 or 0xFF in the middle of what should be Tile16 data,
152 // it might be suspicious, but we need a better heuristic.
153 // For now, let's just check if it's a known bad value from a specific bug.
154 // Example: A previous bug wrote 0x00 to these locations.
155 if (data[addr] == 0x00) {
156 DiagnosticFinding finding;
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.";
162 finding.fixable = false;
163 report.AddFinding(finding);
164 }
165 }
166 }
167
168 // Check for zero-filled blocks in critical code regions (Bank 00)
169 // 0x0000-0x7FFF is code/data. Large blocks of 0x00 might indicate erasure.
170 // We'll scan a small sample.
171 int zero_run = 0;
172 for (uint32_t i = 0x0000; i < 0x1000; ++i) {
173 if (data[i] == 0x00) zero_run++;
174 else zero_run = 0;
175
176 if (zero_run > 64) {
177 DiagnosticFinding finding;
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);
182 finding.suggested_action = "ROM is likely corrupted. Restore from backup.";
183 finding.fixable = false;
184 report.AddFinding(finding);
185 break;
186 }
187 }
188}
189
191 if (!report.features.has_expanded_tile16) return;
192
193 const auto* data = rom->data();
194 size_t size = rom->size();
195
196 // Check Tile16 expansion region (0x1E8000 - 0x1F0000)
197 // This region should contain data, not be all empty.
198 if (size >= kMap16TilesExpandedEnd) {
199 bool all_empty = true;
200 for (uint32_t i = kMap16TilesExpanded; i < kMap16TilesExpandedEnd; i += 256) {
201 if (data[i] != 0xFF && data[i] != 0x00) {
202 all_empty = false;
203 break;
204 }
205 }
206
207 if (all_empty) {
208 DiagnosticFinding finding;
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.";
214 finding.fixable = false;
215 report.AddFinding(finding);
216 }
217 }
218}
219
221 // 1. Search for "PARALLEL WORLDS" string in decoded messages
222 try {
223 std::vector<uint8_t> rom_data_copy = rom->vector(); // Copy for safety
224 auto messages = yaze::editor::ReadAllTextData(rom_data_copy.data());
225
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;
231 break;
232 }
233 }
234
235 if (pw_string_found) {
236 DiagnosticFinding finding;
237 finding.id = "parallel_worlds_string";
239 finding.message = "Found 'PARALLEL WORLDS' string in message data";
240 finding.location = "Message Data";
241 finding.suggested_action = "Confirmed Parallel Worlds ROM.";
242 finding.fixable = false;
243 report.AddFinding(finding);
244 }
245 } catch (...) {
246 // Ignore parsing errors
247 }
248}
249
251 const auto* data = rom->data();
252 size_t size = rom->size();
253
254 bool has_zscustom_features = false;
255 std::vector<std::string> features_found;
256
257 if (kCustomBGEnabledPos < size && data[kCustomBGEnabledPos] != 0x00 && data[kCustomBGEnabledPos] != 0xFF) {
258 has_zscustom_features = true;
259 features_found.push_back("Custom BG");
260 }
261 if (kCustomMainPalettePos < size && data[kCustomMainPalettePos] != 0x00 && data[kCustomMainPalettePos] != 0xFF) {
262 has_zscustom_features = true;
263 features_found.push_back("Custom Palette");
264 }
265
266 if (has_zscustom_features && report.features.is_vanilla) {
267 DiagnosticFinding finding;
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";
272 finding.suggested_action = "Treat as ZSCustom ROM.";
273 finding.fixable = false;
274 report.AddFinding(finding);
275
276 report.features.is_vanilla = false;
277 report.features.zs_custom_version = 0xFE; // Unknown/Detected
278 }
279}
280
281} // namespace
282
284 Rom* rom, const resources::ArgumentParser& parser,
285 resources::OutputFormatter& formatter) {
286 const bool verbose = parser.HasFlag("verbose");
287 const bool is_json = formatter.IsJson();
288
289 OutputTextBanner(is_json);
290
291 DiagnosticReport report;
292 report.rom_path = rom->filename();
293
294 // Basic ROM info
295 formatter.AddField("rom_path", rom->filename());
296 formatter.AddField("size_bytes", static_cast<int>(rom->size()));
297 formatter.AddHexField("size_hex", rom->size(), 6);
298
299 // Size validation
300 bool size_valid = (rom->size() == kVanillaSize || rom->size() == kExpandedSize);
301 formatter.AddField("size_valid", size_valid);
302
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");
307 } else {
308 formatter.AddField("size_type", "non_standard");
309
310 DiagnosticFinding finding;
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);
316 finding.location = "ROM file";
317 finding.fixable = false;
318 report.AddFinding(finding);
319 }
320
321 // Read and validate header
322 auto header = ReadRomHeader(rom);
323
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);
334
335 if (!header.checksum_valid) {
336 DiagnosticFinding finding;
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);
343 finding.location = absl::StrFormat("0x%04X", yaze::cli::kChecksumComplementPos);
344 finding.suggested_action = "ROM may be corrupted or modified without checksum update";
345 finding.fixable = true; // Could be fixed by recalculating
346 report.AddFinding(finding);
347 }
348
349 // Detect ZSCustomOverworld version
350 report.features = DetectRomFeaturesLocal(rom);
351 formatter.AddField("zs_custom_version", report.features.GetVersionString());
352 formatter.AddField("is_vanilla", report.features.is_vanilla);
353 formatter.AddField("expanded_tile16", report.features.has_expanded_tile16);
354 formatter.AddField("expanded_tile32", report.features.has_expanded_tile32);
355 formatter.AddField("expanded_pointer_tables", report.features.has_expanded_pointer_tables);
356
357 // Free space analysis (simplified)
358 if (rom->size() >= kExpandedSize) {
359 // Check for free space in expansion region
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) {
363 free_bytes++;
364 }
365 }
366 formatter.AddField("free_space_estimate", static_cast<int>(free_bytes));
367 formatter.AddField("free_space_region", "0x180000-0x1E0000");
368
369 DiagnosticFinding finding;
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";
376 finding.fixable = false;
377 report.AddFinding(finding);
378 }
379
380 // 3. Check for corruption heuristics
381 CheckCorruptionHeuristics(rom, report);
382
383 // 4. Hyrule Magic / Parallel Worlds Analysis
384 yaze::rom::HyruleMagicValidator hm_validator(rom);
385 if (hm_validator.IsParallelWorlds()) {
386 DiagnosticFinding finding;
387 finding.id = "parallel_worlds_detected";
389 finding.message = "Parallel Worlds (1.5MB) detected (Header check)";
390 finding.location = "ROM Header";
391 finding.suggested_action = "Use z3ed for editing. Custom pointer tables are supported.";
392 finding.fixable = false;
393 report.AddFinding(finding);
394 } else if (hm_validator.HasBank00Erasure()) {
395 DiagnosticFinding finding;
396 finding.id = "hm_corruption_detected";
398 finding.message = "Hyrule Magic corruption detected (Bank 00 erasure)";
399 finding.location = "Bank 00";
400 finding.suggested_action = "ROM is likely unstable. Restore from backup.";
401 finding.fixable = false;
402 report.AddFinding(finding);
403 }
404
405 // Advanced Heuristics
406 CheckParallelWorldsHeuristics(rom, report);
407 CheckZScreamHeuristics(rom, report);
408
409
410
411 // 5. Validate expanded tables
412 ValidateExpandedTables(rom, report);
413
414 // Output findings
415 formatter.BeginArray("findings");
416 for (const auto& finding : report.findings) {
417 formatter.AddArrayItem(finding.FormatJson());
418 }
419 formatter.EndArray();
420
421 // Summary
422 formatter.AddField("total_findings", report.TotalFindings());
423 formatter.AddField("critical_count", report.critical_count);
424 formatter.AddField("error_count", report.error_count);
425 formatter.AddField("warning_count", report.warning_count);
426 formatter.AddField("info_count", report.info_count);
427 formatter.AddField("has_problems", report.HasProblems());
428
429 // Text output
430 if (!is_json) {
431 std::cout << "\n";
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",
445 report.features.GetVersionString());
446 std::cout << absl::StrFormat("║ Expanded Tile16: %-43s ║\n",
447 report.features.has_expanded_tile16 ? "YES" : "NO");
448 std::cout << absl::StrFormat("║ Expanded Tile32: %-43s ║\n",
449 report.features.has_expanded_tile32 ? "YES" : "NO");
450 std::cout << absl::StrFormat("║ Expanded Ptr Tables: %-39s ║\n",
451 report.features.has_expanded_pointer_tables ? "YES" : "NO");
452 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
453 std::cout << absl::StrFormat("║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
454 report.TotalFindings(), report.error_count,
455 report.warning_count, report.info_count, "");
456 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
457
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";
462 }
463 }
464 }
465
466 return absl::OkStatus();
467}
468
469} // namespace yaze::cli
470
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
auto filename() const
Definition rom.h:141
const auto & vector() const
Definition rom.h:139
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
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 CheckParallelWorldsHeuristics(Rom *rom, DiagnosticReport &report)
void CheckCorruptionHeuristics(Rom *rom, DiagnosticReport &report)
void ValidateExpandedTables(Rom *rom, DiagnosticReport &report)
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.
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.