yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
validation_tool.cc
Go to the documentation of this file.
1
7
8#include <cstdint>
9#include <fstream>
10#include <sstream>
11#include <string>
12#include <vector>
13
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
16#include "rom/rom.h"
17
18namespace yaze {
19namespace cli {
20namespace agent {
21namespace tools {
22
23// =============================================================================
24// ValidationToolBase
25// =============================================================================
26
28 const std::vector<ValidationIssue>& issues) const {
29 std::ostringstream json;
30 json << "{\"issues\": [\n";
31
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);
39 }
40 json << "}";
41 if (i < issues.size() - 1)
42 json << ",";
43 json << "\n";
44 }
45
46 json << "], ";
47
48 // Summary
49 int errors = 0, warnings = 0, info = 0;
50 for (const auto& issue : issues) {
51 switch (issue.severity) {
54 errors++;
55 break;
57 warnings++;
58 break;
60 info++;
61 break;
62 }
63 }
64 json << "\"summary\": {\"errors\": " << errors
65 << ", \"warnings\": " << warnings << ", \"info\": " << info << "}}\n";
66
67 return json.str();
68}
69
71 const std::vector<ValidationIssue>& issues) const {
72 std::ostringstream text;
73
74 for (const auto& issue : issues) {
75 std::string prefix;
76 switch (issue.severity) {
78 prefix = "[INFO]";
79 break;
81 prefix = "[WARN]";
82 break;
84 prefix = "[ERROR]";
85 break;
87 prefix = "[CRITICAL]";
88 break;
89 }
90
91 text << prefix << " [" << issue.category << "] " << issue.message;
92 if (issue.address != 0) {
93 text << absl::StrFormat(" (at 0x%06X)", issue.address);
94 }
95 text << "\n";
96 }
97
98 // Summary
99 int errors = 0, warnings = 0, info = 0;
100 for (const auto& issue : issues) {
101 switch (issue.severity) {
104 errors++;
105 break;
107 warnings++;
108 break;
110 info++;
111 break;
112 }
113 }
114
115 text << "\nSummary: " << errors << " errors, " << warnings << " warnings, "
116 << info << " info\n";
117
118 return text.str();
119}
120
121// =============================================================================
122// RomValidateTool
123// =============================================================================
124
126 const resources::ArgumentParser& parser,
127 resources::OutputFormatter& formatter) {
128 std::vector<ValidationIssue> issues;
129
130 // Run all header validations
131 auto header_issues = ValidateHeader(rom);
132 issues.insert(issues.end(), header_issues.begin(), header_issues.end());
133
134 auto checksum_issues = ValidateChecksum(rom);
135 issues.insert(issues.end(), checksum_issues.begin(), checksum_issues.end());
136
137 auto size_issues = ValidateSize(rom);
138 issues.insert(issues.end(), size_issues.begin(), size_issues.end());
139
140 std::string format = parser.GetString("format").value_or("json");
141 if (format == "json") {
142 std::cout << FormatIssuesAsJson(issues);
143 } else {
144 std::cout << FormatIssuesAsText(issues);
145 }
146
147 formatter.AddField("status", "complete");
148 return absl::OkStatus();
149}
150
151std::vector<ValidationIssue> RomValidateTool::ValidateHeader(Rom* rom) {
152 std::vector<ValidationIssue> issues;
153
154 // Check ROM title
155 std::string title = rom->title();
156 if (title.empty()) {
157 issues.push_back({ValidationIssue::Severity::kWarning, "header",
158 "ROM title is empty", 0x7FC0});
159 } else {
160 issues.push_back({ValidationIssue::Severity::kInfo, "header",
161 "ROM title: " + title, 0x7FC0});
162 }
163
164 // Check map mode
165 auto map_mode = rom->ReadByte(0x7FD5);
166 if (map_mode.ok()) {
167 uint8_t mode = *map_mode;
168 if ((mode & 0x01) == 0) {
169 issues.push_back({ValidationIssue::Severity::kInfo, "header",
170 "ROM is LoROM mapping", 0x7FD5});
171 } else {
172 issues.push_back({ValidationIssue::Severity::kInfo, "header",
173 "ROM is HiROM mapping", 0x7FD5});
174 }
175 }
176
177 // Check ROM type
178 auto rom_type = rom->ReadByte(0x7FD6);
179 if (rom_type.ok()) {
180 uint8_t type = *rom_type;
181 if (type == 0x00) {
182 issues.push_back(
183 {ValidationIssue::Severity::kInfo, "header", "ROM only", 0x7FD6});
184 } else if (type == 0x02) {
185 issues.push_back(
186 {ValidationIssue::Severity::kInfo, "header", "ROM + SRAM", 0x7FD6});
187 }
188 }
189
190 return issues;
191}
192
193std::vector<ValidationIssue> RomValidateTool::ValidateChecksum(Rom* rom) {
194 std::vector<ValidationIssue> issues;
195
196 auto checksum = rom->ReadWord(0x7FDC);
197 auto complement = rom->ReadWord(0x7FDE);
198
199 if (checksum.ok() && complement.ok()) {
200 uint16_t sum = *checksum;
201 uint16_t comp = *complement;
202
203 // Checksum + Complement should equal 0xFFFF
204 if ((sum + comp) == 0xFFFF) {
205 issues.push_back({ValidationIssue::Severity::kInfo, "checksum",
206 absl::StrFormat("Header checksum valid (0x%04X)", sum),
207 0x7FDC});
208 } else {
209 issues.push_back(
211 absl::StrFormat(
212 "Header checksum invalid (0x%04X + 0x%04X != 0xFFFF)", sum,
213 comp),
214 0x7FDC});
215 }
216
217 // Calculate actual checksum
218 uint32_t actual_sum = 0;
219 for (size_t i = 0; i < rom->size(); ++i) {
220 actual_sum += (*rom)[i];
221 }
222 actual_sum = (actual_sum & 0xFFFF);
223
224 if (static_cast<uint16_t>(actual_sum) == sum) {
225 issues.push_back({ValidationIssue::Severity::kInfo, "checksum",
226 "Calculated checksum matches header", 0});
227 } else {
228 issues.push_back(
230 absl::StrFormat(
231 "Calculated checksum (0x%04X) differs from header (0x%04X)",
232 actual_sum, sum),
233 0});
234 }
235 } else {
236 issues.push_back({ValidationIssue::Severity::kError, "checksum",
237 "Failed to read checksum from header", 0x7FDC});
238 }
239
240 return issues;
241}
242
243std::vector<ValidationIssue> RomValidateTool::ValidateSize(Rom* rom) {
244 std::vector<ValidationIssue> issues;
245
246 size_t size = rom->size();
247
248 // Check for common sizes
249 if (size == 0x100000) { // 1MB
250 issues.push_back({ValidationIssue::Severity::kInfo, "size",
251 "ROM size: 1MB (standard)", 0});
252 } else if (size == 0x200000) { // 2MB
253 issues.push_back({ValidationIssue::Severity::kInfo, "size",
254 "ROM size: 2MB (expanded)", 0});
255 } else if (size == 0x400000) { // 4MB
256 issues.push_back({ValidationIssue::Severity::kInfo, "size",
257 "ROM size: 4MB (fully expanded)", 0});
258 } else {
259 issues.push_back({ValidationIssue::Severity::kWarning, "size",
260 absl::StrFormat("Unusual ROM size: %zu bytes", size), 0});
261 }
262
263 // Check for header presence
264 if ((size & 0x200) != 0) {
265 issues.push_back({ValidationIssue::Severity::kInfo, "size",
266 "ROM has 512-byte copier header", 0});
267 }
268
269 return issues;
270}
271
272// =============================================================================
273// DataValidateTool
274// =============================================================================
275
277 const resources::ArgumentParser& parser,
278 resources::OutputFormatter& formatter) {
279 std::vector<ValidationIssue> issues;
280 std::string type = parser.GetString("type").value();
281
282 if (type == "sprites" || type == "all") {
283 auto sprite_issues = ValidateSprites(rom);
284 issues.insert(issues.end(), sprite_issues.begin(), sprite_issues.end());
285 }
286
287 if (type == "tiles" || type == "all") {
288 auto tile_issues = ValidateTiles(rom);
289 issues.insert(issues.end(), tile_issues.begin(), tile_issues.end());
290 }
291
292 if (type == "palettes" || type == "all") {
293 auto palette_issues = ValidatePalettes(rom);
294 issues.insert(issues.end(), palette_issues.begin(), palette_issues.end());
295 }
296
297 if (type == "entrances" || type == "all") {
298 auto entrance_issues = ValidateEntrances(rom);
299 issues.insert(issues.end(), entrance_issues.begin(), entrance_issues.end());
300 }
301
302 std::string format = parser.GetString("format").value_or("json");
303 if (format == "json") {
304 std::cout << FormatIssuesAsJson(issues);
305 } else {
306 std::cout << FormatIssuesAsText(issues);
307 }
308
309 formatter.AddField("status", "complete");
310 return absl::OkStatus();
311}
312
313std::vector<ValidationIssue> DataValidateTool::ValidateSprites(Rom* rom) {
314 std::vector<ValidationIssue> issues;
315
316 // Check overworld sprite data
317 constexpr uint32_t kOverworldSpriteBase = 0x09C901;
318 constexpr int kNumMaps = 64; // Check first 64 maps
319
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()) {
327 invalid_sprites++;
328 }
329 }
330 }
331
332 if (invalid_sprites > 0) {
333 issues.push_back(
335 absl::StrFormat("%d overworld maps have invalid sprite pointers",
336 invalid_sprites),
337 0});
338 } else {
339 issues.push_back({ValidationIssue::Severity::kInfo, "sprites",
340 "All overworld sprite pointers valid", 0});
341 }
342
343 return issues;
344}
345
346std::vector<ValidationIssue> DataValidateTool::ValidateTiles(Rom* rom) {
347 std::vector<ValidationIssue> issues;
348
349 // Basic tile data validation
350 // Check that tile graphics pointers are valid
351 constexpr uint32_t kTileGfxPtr = 0x00E800;
352
353 auto gfx_ptr = rom->ReadWord(kTileGfxPtr);
354 if (gfx_ptr.ok()) {
355 issues.push_back({ValidationIssue::Severity::kInfo, "tiles",
356 "Tile graphics pointer accessible", kTileGfxPtr});
357 } else {
358 issues.push_back({ValidationIssue::Severity::kError, "tiles",
359 "Failed to read tile graphics pointer", kTileGfxPtr});
360 }
361
362 return issues;
363}
364
365std::vector<ValidationIssue> DataValidateTool::ValidatePalettes(Rom* rom) {
366 std::vector<ValidationIssue> issues;
367
368 // Check palette data integrity
369 constexpr uint32_t kPaletteBase = 0x0DD218;
370 constexpr int kNumPalettes = 8;
371 constexpr int kPaletteSize = 32; // 16 colors * 2 bytes
372
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()) {
377 valid_palettes++;
378 }
379 }
380
381 if (valid_palettes == kNumPalettes) {
382 issues.push_back({ValidationIssue::Severity::kInfo, "palettes",
383 "All main palettes accessible", kPaletteBase});
384 } else {
385 issues.push_back({ValidationIssue::Severity::kError, "palettes",
386 absl::StrFormat("Only %d/%d palettes accessible",
387 valid_palettes, kNumPalettes),
388 kPaletteBase});
389 }
390
391 return issues;
392}
393
394std::vector<ValidationIssue> DataValidateTool::ValidateEntrances(Rom* rom) {
395 std::vector<ValidationIssue> issues;
396
397 // Check entrance data
398 constexpr uint32_t kEntranceBase = 0x02C577;
399 constexpr int kNumEntrances = 0x85; // 133 entrances
400
401 int valid_entrances = 0;
402 for (int i = 0; i < kNumEntrances; ++i) {
403 auto room_id = rom->ReadWord(kEntranceBase + (i * 2));
404 if (room_id.ok()) {
405 if (*room_id < 296) { // Valid room IDs are 0-295
406 valid_entrances++;
407 }
408 }
409 }
410
411 if (valid_entrances == kNumEntrances) {
412 issues.push_back({ValidationIssue::Severity::kInfo, "entrances",
413 "All entrance room IDs valid", kEntranceBase});
414 } else {
415 issues.push_back({ValidationIssue::Severity::kWarning, "entrances",
416 absl::StrFormat("%d/%d entrances have valid room IDs",
417 valid_entrances, kNumEntrances),
418 kEntranceBase});
419 }
420
421 return issues;
422}
423
424// =============================================================================
425// PatchCheckTool
426// =============================================================================
427
428absl::Status PatchCheckTool::Execute(Rom* rom,
429 const resources::ArgumentParser& parser,
430 resources::OutputFormatter& formatter) {
431 std::string patch_path = parser.GetString("patch").value();
432
433 std::vector<ValidationIssue> issues;
434
435 // Check free space availability
436 auto space_issues = CheckFreeSpace(rom);
437 issues.insert(issues.end(), space_issues.begin(), space_issues.end());
438
439 // Check for hook conflicts
440 auto hook_issues = CheckHooks(rom, patch_path);
441 issues.insert(issues.end(), hook_issues.begin(), hook_issues.end());
442
443 std::string format = parser.GetString("format").value_or("json");
444 if (format == "json") {
445 std::cout << FormatIssuesAsJson(issues);
446 } else {
447 std::cout << FormatIssuesAsText(issues);
448 }
449
450 formatter.AddField("status", "complete");
451 return absl::OkStatus();
452}
453
454std::vector<ValidationIssue> PatchCheckTool::CheckFreeSpace(Rom* rom) {
455 std::vector<ValidationIssue> issues;
456
457 // Check common free space regions
458 struct FreeSpaceRegion {
459 uint32_t start;
460 uint32_t end;
461 const char* name;
462 };
463
464 std::vector<FreeSpaceRegion> regions = {
465 {0x1F8000, 0x1FFFFF, "Bank $3F"},
466 {0x0FFF00, 0x0FFFFF, "Bank $1F end"},
467 };
468
469 for (const auto& region : regions) {
470 if (region.end > rom->size()) {
471 issues.push_back(
473 absl::StrFormat("%s not available (ROM too small)", region.name),
474 region.start});
475 continue;
476 }
477
478 // Check if region is mostly 0xFF (free space marker)
479 int free_bytes = 0;
480 for (uint32_t addr = region.start; addr < region.end; ++addr) {
481 if ((*rom)[addr] == 0xFF || (*rom)[addr] == 0x00) {
482 free_bytes++;
483 }
484 }
485
486 int region_size = region.end - region.start;
487 int free_percent = (free_bytes * 100) / region_size;
488
489 if (free_percent > 80) {
490 issues.push_back({ValidationIssue::Severity::kInfo, "free_space",
491 absl::StrFormat("%s: %d%% free (%d bytes)", region.name,
492 free_percent, free_bytes),
493 region.start});
494 } else {
495 issues.push_back({ValidationIssue::Severity::kWarning, "free_space",
496 absl::StrFormat("%s: only %d%% free (%d bytes)",
497 region.name, free_percent, free_bytes),
498 region.start});
499 }
500 }
501
502 return issues;
503}
504
505std::vector<ValidationIssue> PatchCheckTool::CheckHooks(
506 Rom* rom, const std::string& patch_path) {
507 std::vector<ValidationIssue> issues;
508
509 // Check if patch file exists
510 std::ifstream patch_file(patch_path);
511 if (!patch_file.good()) {
512 issues.push_back({ValidationIssue::Severity::kError, "patch",
513 "Patch file not found: " + patch_path, 0});
514 return issues;
515 }
516
517 // Check common hook locations for existing modifications
518 struct HookLocation {
519 uint32_t address;
520 const char* name;
521 uint8_t original_byte;
522 };
523
524 std::vector<HookLocation> hooks = {
525 {0x008027, "Reset vector", 0x8C},
526 {0x008040, "NMI vector", 0x5C},
527 {0x0080B5, "IRQ vector", 0x8B},
528 };
529
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) {
534 issues.push_back(
536 absl::StrFormat("%s already modified (0x%02X != 0x%02X)",
537 hook.name, *byte, hook.original_byte),
538 hook.address});
539 }
540 }
541 }
542
543 issues.push_back({ValidationIssue::Severity::kInfo, "patch",
544 "Patch file exists: " + patch_path, 0});
545
546 return issues;
547}
548
549// =============================================================================
550// ValidateAllTool
551// =============================================================================
552
554 const resources::ArgumentParser& parser,
555 resources::OutputFormatter& formatter) {
556 std::vector<ValidationIssue> all_issues;
557 bool strict = parser.HasFlag("strict");
558
559 // Run ROM validation
560 RomValidateTool rom_validator;
561 auto header_issues = rom_validator.ValidateHeader(rom);
562 all_issues.insert(all_issues.end(), header_issues.begin(),
563 header_issues.end());
564 auto checksum_issues = rom_validator.ValidateChecksum(rom);
565 all_issues.insert(all_issues.end(), checksum_issues.begin(),
566 checksum_issues.end());
567 auto size_issues = rom_validator.ValidateSize(rom);
568 all_issues.insert(all_issues.end(), size_issues.begin(), size_issues.end());
569
570 // Run data validation
571 DataValidateTool data_validator;
572 auto sprite_issues = data_validator.ValidateSprites(rom);
573 all_issues.insert(all_issues.end(), sprite_issues.begin(),
574 sprite_issues.end());
575 auto tile_issues = data_validator.ValidateTiles(rom);
576 all_issues.insert(all_issues.end(), tile_issues.begin(), tile_issues.end());
577 auto palette_issues = data_validator.ValidatePalettes(rom);
578 all_issues.insert(all_issues.end(), palette_issues.begin(),
579 palette_issues.end());
580 auto entrance_issues = data_validator.ValidateEntrances(rom);
581 all_issues.insert(all_issues.end(), entrance_issues.begin(),
582 entrance_issues.end());
583
584 // Check for critical issues in strict mode
585 if (strict) {
586 for (const auto& issue : all_issues) {
587 if (issue.severity == ValidationIssue::Severity::kCritical ||
588 issue.severity == ValidationIssue::Severity::kError) {
589 return absl::InvalidArgumentError("Validation failed: " +
590 issue.message);
591 }
592 }
593 }
594
595 std::string format = parser.GetString("format").value_or("json");
596 if (format == "json") {
597 std::cout << FormatIssuesAsJson(all_issues);
598 } else {
599 std::cout << FormatIssuesAsText(all_issues);
600 }
601
602 formatter.AddField("status", "complete");
603 return absl::OkStatus();
604}
605
606} // namespace tools
607} // namespace agent
608} // namespace cli
609} // namespace yaze
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
absl::StatusOr< uint16_t > ReadWord(int offset)
Definition rom.cc:230
auto size() const
Definition rom.h:134
absl::StatusOr< uint8_t > ReadByte(int offset)
Definition rom.cc:222
auto title() const
Definition rom.h:133
std::vector< ValidationIssue > ValidateTiles(Rom *rom)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::vector< ValidationIssue > ValidateSprites(Rom *rom)
std::vector< ValidationIssue > ValidateEntrances(Rom *rom)
std::vector< ValidationIssue > ValidatePalettes(Rom *rom)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::vector< ValidationIssue > CheckHooks(Rom *rom, const std::string &patch_path)
std::vector< ValidationIssue > CheckFreeSpace(Rom *rom)
Validate ROM header and checksums.
std::vector< ValidationIssue > ValidateSize(Rom *rom)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::vector< ValidationIssue > ValidateChecksum(Rom *rom)
std::vector< ValidationIssue > ValidateHeader(Rom *rom)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::string FormatIssuesAsText(const std::vector< ValidationIssue > &issues) const
Format validation issues as text.
std::string FormatIssuesAsJson(const std::vector< ValidationIssue > &issues) const
Format validation issues as JSON.
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.
Utility for consistent output formatting across commands.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
ROM validation and integrity checking tools for AI agents.