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)
json <<
",";
48 int errors = 0, warnings = 0, info = 0;
49 for (
const auto& issue : issues) {
50 switch (issue.severity) {
63 json <<
"\"summary\": {\"errors\": " << errors <<
", \"warnings\": " << warnings
64 <<
", \"info\": " << info <<
"}}\n";
70 const std::vector<ValidationIssue>& issues)
const {
71 std::ostringstream text;
73 for (
const auto& issue : issues) {
75 switch (issue.severity) {
86 prefix =
"[CRITICAL]";
90 text << prefix <<
" [" << issue.category <<
"] " << issue.message;
91 if (issue.address != 0) {
92 text << absl::StrFormat(
" (at 0x%06X)", issue.address);
98 int errors = 0, warnings = 0, info = 0;
99 for (
const auto& issue : issues) {
100 switch (issue.severity) {
114 text <<
"\nSummary: " << errors <<
" errors, " << warnings <<
" warnings, "
115 << info <<
" info\n";
127 std::vector<ValidationIssue> issues;
131 issues.insert(issues.end(), header_issues.begin(), header_issues.end());
134 issues.insert(issues.end(), checksum_issues.begin(), checksum_issues.end());
137 issues.insert(issues.end(), size_issues.begin(), size_issues.end());
139 std::string format = parser.
GetString(
"format").value_or(
"json");
140 if (format ==
"json") {
146 formatter.
AddField(
"status",
"complete");
147 return absl::OkStatus();
151 std::vector<ValidationIssue> issues;
154 std::string title = rom->
title();
157 "ROM title is empty", 0x7FC0});
160 "ROM title: " + title, 0x7FC0});
164 auto map_mode = rom->
ReadByte(0x7FD5);
166 uint8_t mode = *map_mode;
167 if ((mode & 0x01) == 0) {
169 "ROM is LoROM mapping", 0x7FD5});
172 "ROM is HiROM mapping", 0x7FD5});
177 auto rom_type = rom->
ReadByte(0x7FD6);
179 uint8_t type = *rom_type;
183 }
else if (type == 0x02) {
185 "ROM + SRAM", 0x7FD6});
193 std::vector<ValidationIssue> issues;
195 auto checksum = rom->
ReadWord(0x7FDC);
196 auto complement = rom->
ReadWord(0x7FDE);
198 if (checksum.ok() && complement.ok()) {
199 uint16_t sum = *checksum;
200 uint16_t comp = *complement;
203 if ((sum + comp) == 0xFFFF) {
205 absl::StrFormat(
"Header checksum valid (0x%04X)", sum),
211 "Header checksum invalid (0x%04X + 0x%04X != 0xFFFF)", sum,
217 uint32_t actual_sum = 0;
218 for (
size_t i = 0; i < rom->
size(); ++i) {
219 actual_sum += (*rom)[i];
221 actual_sum = (actual_sum & 0xFFFF);
223 if (
static_cast<uint16_t
>(actual_sum) == sum) {
225 "Calculated checksum matches header", 0});
230 "Calculated checksum (0x%04X) differs from header (0x%04X)",
236 "Failed to read checksum from header", 0x7FDC});
243 std::vector<ValidationIssue> issues;
245 size_t size = rom->
size();
248 if (size == 0x100000) {
250 "ROM size: 1MB (standard)", 0});
251 }
else if (size == 0x200000) {
253 "ROM size: 2MB (expanded)", 0});
254 }
else if (size == 0x400000) {
256 "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});
387 absl::StrFormat(
"Only %d/%d palettes accessible", valid_palettes,
396 std::vector<ValidationIssue> issues;
399 constexpr uint32_t kEntranceBase = 0x02C577;
400 constexpr int kNumEntrances = 0x85;
402 int valid_entrances = 0;
403 for (
int i = 0; i < kNumEntrances; ++i) {
404 auto room_id = rom->
ReadWord(kEntranceBase + (i * 2));
406 if (*room_id < 296) {
412 if (valid_entrances == kNumEntrances) {
414 "All entrance room IDs valid", kEntranceBase});
418 absl::StrFormat(
"%d/%d entrances have valid room IDs",
419 valid_entrances, kNumEntrances),
433 std::string patch_path = parser.
GetString(
"patch").value();
435 std::vector<ValidationIssue> issues;
439 issues.insert(issues.end(), space_issues.begin(), space_issues.end());
442 auto hook_issues =
CheckHooks(rom, patch_path);
443 issues.insert(issues.end(), hook_issues.begin(), hook_issues.end());
445 std::string format = parser.
GetString(
"format").value_or(
"json");
446 if (format ==
"json") {
452 formatter.
AddField(
"status",
"complete");
453 return absl::OkStatus();
457 std::vector<ValidationIssue> issues;
466 std::vector<FreeSpaceRegion> regions = {
467 {0x1F8000, 0x1FFFFF,
"Bank $3F"},
468 {0x0FFF00, 0x0FFFFF,
"Bank $1F end"},
471 for (
const auto& region : regions) {
472 if (region.end > rom->
size()) {
474 absl::StrFormat(
"%s not available (ROM too small)",
482 for (uint32_t addr = region.start; addr < region.end; ++addr) {
483 if ((*rom)[addr] == 0xFF || (*rom)[addr] == 0x00) {
488 int region_size = region.end - region.start;
489 int free_percent = (free_bytes * 100) / region_size;
491 if (free_percent > 80) {
494 absl::StrFormat(
"%s: %d%% free (%d bytes)", region.name,
495 free_percent, free_bytes),
500 absl::StrFormat(
"%s: only %d%% free (%d bytes)", region.name,
501 free_percent, free_bytes),
510 Rom* rom,
const std::string& patch_path) {
511 std::vector<ValidationIssue> issues;
514 std::ifstream patch_file(patch_path);
515 if (!patch_file.good()) {
517 "Patch file not found: " + patch_path, 0});
522 struct HookLocation {
525 uint8_t original_byte;
528 std::vector<HookLocation> hooks = {
529 {0x008027,
"Reset vector", 0x8C},
530 {0x008040,
"NMI vector", 0x5C},
531 {0x0080B5,
"IRQ vector", 0x8B},
534 for (
const auto& hook : hooks) {
535 if (hook.address < rom->
size()) {
536 auto byte = rom->
ReadByte(hook.address);
537 if (
byte.ok() && *
byte != hook.original_byte) {
540 absl::StrFormat(
"%s already modified (0x%02X != 0x%02X)",
541 hook.name, *
byte, hook.original_byte),
548 "Patch file exists: " + patch_path, 0});
560 std::vector<ValidationIssue> all_issues;
561 bool strict = parser.
HasFlag(
"strict");
566 all_issues.insert(all_issues.end(), header_issues.begin(),
567 header_issues.end());
569 all_issues.insert(all_issues.end(), checksum_issues.begin(),
570 checksum_issues.end());
572 all_issues.insert(all_issues.end(), size_issues.begin(), size_issues.end());
577 all_issues.insert(all_issues.end(), sprite_issues.begin(),
578 sprite_issues.end());
580 all_issues.insert(all_issues.end(), tile_issues.begin(), tile_issues.end());
582 all_issues.insert(all_issues.end(), palette_issues.begin(),
583 palette_issues.end());
585 all_issues.insert(all_issues.end(), entrance_issues.begin(),
586 entrance_issues.end());
590 for (
const auto& issue : all_issues) {
593 return absl::InvalidArgumentError(
594 "Validation failed: " + issue.message);
599 std::string format = parser.
GetString(
"format").value_or(
"json");
600 if (format ==
"json") {
606 formatter.
AddField(
"status",
"complete");
607 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.