14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
28 const std::vector<ValidationIssue>& issues)
const {
29 std::ostringstream
json;
30 json <<
"{\"issues\": [\n";
32 for (
size_t i = 0; i < issues.size(); ++i) {
33 const auto& issue = issues[i];
34 json <<
" {\"severity\": \"" << issue.SeverityString() <<
"\", "
35 <<
"\"category\": \"" << issue.category <<
"\", "
36 <<
"\"message\": \"" << issue.message <<
"\"";
37 if (issue.address != 0) {
38 json << absl::StrFormat(
", \"address\": \"0x%06X\"", issue.address);
41 if (i < issues.size() - 1)
49 int errors = 0, warnings = 0, info = 0;
50 for (
const auto& issue : issues) {
51 switch (issue.severity) {
64 json <<
"\"summary\": {\"errors\": " << errors
65 <<
", \"warnings\": " << warnings <<
", \"info\": " << info <<
"}}\n";
71 const std::vector<ValidationIssue>& issues)
const {
72 std::ostringstream text;
74 for (
const auto& issue : issues) {
76 switch (issue.severity) {
87 prefix =
"[CRITICAL]";
91 text << prefix <<
" [" << issue.category <<
"] " << issue.message;
92 if (issue.address != 0) {
93 text << absl::StrFormat(
" (at 0x%06X)", issue.address);
99 int errors = 0, warnings = 0, info = 0;
100 for (
const auto& issue : issues) {
101 switch (issue.severity) {
115 text <<
"\nSummary: " << errors <<
" errors, " << warnings <<
" warnings, "
116 << info <<
" info\n";
128 std::vector<ValidationIssue> issues;
132 issues.insert(issues.end(), header_issues.begin(), header_issues.end());
135 issues.insert(issues.end(), checksum_issues.begin(), checksum_issues.end());
138 issues.insert(issues.end(), size_issues.begin(), size_issues.end());
140 std::string format = parser.
GetString(
"format").value_or(
"json");
141 if (format ==
"json") {
147 formatter.
AddField(
"status",
"complete");
148 return absl::OkStatus();
152 std::vector<ValidationIssue> issues;
155 std::string title = rom->
title();
158 "ROM title is empty", 0x7FC0});
161 "ROM title: " + title, 0x7FC0});
165 auto map_mode = rom->
ReadByte(0x7FD5);
167 uint8_t mode = *map_mode;
168 if ((mode & 0x01) == 0) {
170 "ROM is LoROM mapping", 0x7FD5});
173 "ROM is HiROM mapping", 0x7FD5});
178 auto rom_type = rom->
ReadByte(0x7FD6);
180 uint8_t type = *rom_type;
184 }
else if (type == 0x02) {
194 std::vector<ValidationIssue> issues;
196 auto checksum = rom->
ReadWord(0x7FDC);
197 auto complement = rom->
ReadWord(0x7FDE);
199 if (checksum.ok() && complement.ok()) {
200 uint16_t sum = *checksum;
201 uint16_t comp = *complement;
204 if ((sum + comp) == 0xFFFF) {
206 absl::StrFormat(
"Header checksum valid (0x%04X)", sum),
212 "Header checksum invalid (0x%04X + 0x%04X != 0xFFFF)", sum,
218 uint32_t actual_sum = 0;
219 for (
size_t i = 0; i < rom->
size(); ++i) {
220 actual_sum += (*rom)[i];
222 actual_sum = (actual_sum & 0xFFFF);
224 if (
static_cast<uint16_t
>(actual_sum) == sum) {
226 "Calculated checksum matches header", 0});
231 "Calculated checksum (0x%04X) differs from header (0x%04X)",
237 "Failed to read checksum from header", 0x7FDC});
244 std::vector<ValidationIssue> issues;
246 size_t size = rom->
size();
249 if (size == 0x100000) {
251 "ROM size: 1MB (standard)", 0});
252 }
else if (size == 0x200000) {
254 "ROM size: 2MB (expanded)", 0});
255 }
else if (size == 0x400000) {
257 "ROM size: 4MB (fully expanded)", 0});
260 absl::StrFormat(
"Unusual ROM size: %zu bytes", size), 0});
264 if ((size & 0x200) != 0) {
266 "ROM has 512-byte copier header", 0});
279 std::vector<ValidationIssue> issues;
280 std::string type = parser.
GetString(
"type").value();
282 if (type ==
"sprites" || type ==
"all") {
284 issues.insert(issues.end(), sprite_issues.begin(), sprite_issues.end());
287 if (type ==
"tiles" || type ==
"all") {
289 issues.insert(issues.end(), tile_issues.begin(), tile_issues.end());
292 if (type ==
"palettes" || type ==
"all") {
294 issues.insert(issues.end(), palette_issues.begin(), palette_issues.end());
297 if (type ==
"entrances" || type ==
"all") {
299 issues.insert(issues.end(), entrance_issues.begin(), entrance_issues.end());
302 std::string format = parser.
GetString(
"format").value_or(
"json");
303 if (format ==
"json") {
309 formatter.
AddField(
"status",
"complete");
310 return absl::OkStatus();
314 std::vector<ValidationIssue> issues;
317 constexpr uint32_t kOverworldSpriteBase = 0x09C901;
318 constexpr int kNumMaps = 64;
320 int invalid_sprites = 0;
321 for (
int map = 0; map < kNumMaps; ++map) {
322 auto ptr_low = rom->
ReadByte(0x09C881 + map);
323 auto ptr_high = rom->
ReadByte(0x09C8C1 + map);
324 if (ptr_low.ok() && ptr_high.ok()) {
325 uint32_t addr = kOverworldSpriteBase + (*ptr_low | (*ptr_high << 8));
326 if (addr >= rom->
size()) {
332 if (invalid_sprites > 0) {
335 absl::StrFormat(
"%d overworld maps have invalid sprite pointers",
340 "All overworld sprite pointers valid", 0});
347 std::vector<ValidationIssue> issues;
351 constexpr uint32_t kTileGfxPtr = 0x00E800;
353 auto gfx_ptr = rom->
ReadWord(kTileGfxPtr);
356 "Tile graphics pointer accessible", kTileGfxPtr});
359 "Failed to read tile graphics pointer", kTileGfxPtr});
366 std::vector<ValidationIssue> issues;
369 constexpr uint32_t kPaletteBase = 0x0DD218;
370 constexpr int kNumPalettes = 8;
371 constexpr int kPaletteSize = 32;
373 int valid_palettes = 0;
374 for (
int i = 0; i < kNumPalettes; ++i) {
375 uint32_t addr = kPaletteBase + (i * kPaletteSize);
376 if (addr + kPaletteSize <= rom->size()) {
381 if (valid_palettes == kNumPalettes) {
383 "All main palettes accessible", kPaletteBase});
386 absl::StrFormat(
"Only %d/%d palettes accessible",
387 valid_palettes, kNumPalettes),
395 std::vector<ValidationIssue> issues;
398 constexpr uint32_t kEntranceBase = 0x02C577;
399 constexpr int kNumEntrances = 0x85;
401 int valid_entrances = 0;
402 for (
int i = 0; i < kNumEntrances; ++i) {
403 auto room_id = rom->
ReadWord(kEntranceBase + (i * 2));
405 if (*room_id < 296) {
411 if (valid_entrances == kNumEntrances) {
413 "All entrance room IDs valid", kEntranceBase});
416 absl::StrFormat(
"%d/%d entrances have valid room IDs",
417 valid_entrances, kNumEntrances),
431 std::string patch_path = parser.
GetString(
"patch").value();
433 std::vector<ValidationIssue> issues;
437 issues.insert(issues.end(), space_issues.begin(), space_issues.end());
440 auto hook_issues =
CheckHooks(rom, patch_path);
441 issues.insert(issues.end(), hook_issues.begin(), hook_issues.end());
443 std::string format = parser.
GetString(
"format").value_or(
"json");
444 if (format ==
"json") {
450 formatter.
AddField(
"status",
"complete");
451 return absl::OkStatus();
455 std::vector<ValidationIssue> issues;
464 std::vector<FreeSpaceRegion> regions = {
465 {0x1F8000, 0x1FFFFF,
"Bank $3F"},
466 {0x0FFF00, 0x0FFFFF,
"Bank $1F end"},
469 for (
const auto& region : regions) {
470 if (region.end > rom->
size()) {
473 absl::StrFormat(
"%s not available (ROM too small)", region.name),
480 for (uint32_t addr = region.start; addr < region.end; ++addr) {
481 if ((*rom)[addr] == 0xFF || (*rom)[addr] == 0x00) {
486 int region_size = region.end - region.start;
487 int free_percent = (free_bytes * 100) / region_size;
489 if (free_percent > 80) {
491 absl::StrFormat(
"%s: %d%% free (%d bytes)", region.name,
492 free_percent, free_bytes),
496 absl::StrFormat(
"%s: only %d%% free (%d bytes)",
497 region.name, free_percent, free_bytes),
506 Rom* rom,
const std::string& patch_path) {
507 std::vector<ValidationIssue> issues;
510 std::ifstream patch_file(patch_path);
511 if (!patch_file.good()) {
513 "Patch file not found: " + patch_path, 0});
518 struct HookLocation {
521 uint8_t original_byte;
524 std::vector<HookLocation> hooks = {
525 {0x008027,
"Reset vector", 0x8C},
526 {0x008040,
"NMI vector", 0x5C},
527 {0x0080B5,
"IRQ vector", 0x8B},
530 for (
const auto& hook : hooks) {
531 if (hook.address < rom->
size()) {
532 auto byte = rom->
ReadByte(hook.address);
533 if (
byte.ok() && *
byte != hook.original_byte) {
536 absl::StrFormat(
"%s already modified (0x%02X != 0x%02X)",
537 hook.name, *
byte, hook.original_byte),
544 "Patch file exists: " + patch_path, 0});
556 std::vector<ValidationIssue> all_issues;
557 bool strict = parser.
HasFlag(
"strict");
562 all_issues.insert(all_issues.end(), header_issues.begin(),
563 header_issues.end());
565 all_issues.insert(all_issues.end(), checksum_issues.begin(),
566 checksum_issues.end());
568 all_issues.insert(all_issues.end(), size_issues.begin(), size_issues.end());
573 all_issues.insert(all_issues.end(), sprite_issues.begin(),
574 sprite_issues.end());
576 all_issues.insert(all_issues.end(), tile_issues.begin(), tile_issues.end());
578 all_issues.insert(all_issues.end(), palette_issues.begin(),
579 palette_issues.end());
581 all_issues.insert(all_issues.end(), entrance_issues.begin(),
582 entrance_issues.end());
586 for (
const auto& issue : all_issues) {
589 return absl::InvalidArgumentError(
"Validation failed: " +
595 std::string format = parser.
GetString(
"format").value_or(
"json");
596 if (format ==
"json") {
602 formatter.
AddField(
"status",
"complete");
603 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< uint16_t > ReadWord(int offset)
absl::StatusOr< uint8_t > ReadByte(int offset)
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.