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) json << ",";
42 json << "\n";
43 }
44
45 json << "], ";
46
47 // Summary
48 int errors = 0, warnings = 0, info = 0;
49 for (const auto& issue : issues) {
50 switch (issue.severity) {
53 errors++;
54 break;
56 warnings++;
57 break;
59 info++;
60 break;
61 }
62 }
63 json << "\"summary\": {\"errors\": " << errors << ", \"warnings\": " << warnings
64 << ", \"info\": " << info << "}}\n";
65
66 return json.str();
67}
68
70 const std::vector<ValidationIssue>& issues) const {
71 std::ostringstream text;
72
73 for (const auto& issue : issues) {
74 std::string prefix;
75 switch (issue.severity) {
77 prefix = "[INFO]";
78 break;
80 prefix = "[WARN]";
81 break;
83 prefix = "[ERROR]";
84 break;
86 prefix = "[CRITICAL]";
87 break;
88 }
89
90 text << prefix << " [" << issue.category << "] " << issue.message;
91 if (issue.address != 0) {
92 text << absl::StrFormat(" (at 0x%06X)", issue.address);
93 }
94 text << "\n";
95 }
96
97 // Summary
98 int errors = 0, warnings = 0, info = 0;
99 for (const auto& issue : issues) {
100 switch (issue.severity) {
103 errors++;
104 break;
106 warnings++;
107 break;
109 info++;
110 break;
111 }
112 }
113
114 text << "\nSummary: " << errors << " errors, " << warnings << " warnings, "
115 << info << " info\n";
116
117 return text.str();
118}
119
120// =============================================================================
121// RomValidateTool
122// =============================================================================
123
125 const resources::ArgumentParser& parser,
126 resources::OutputFormatter& formatter) {
127 std::vector<ValidationIssue> issues;
128
129 // Run all header validations
130 auto header_issues = ValidateHeader(rom);
131 issues.insert(issues.end(), header_issues.begin(), header_issues.end());
132
133 auto checksum_issues = ValidateChecksum(rom);
134 issues.insert(issues.end(), checksum_issues.begin(), checksum_issues.end());
135
136 auto size_issues = ValidateSize(rom);
137 issues.insert(issues.end(), size_issues.begin(), size_issues.end());
138
139 std::string format = parser.GetString("format").value_or("json");
140 if (format == "json") {
141 std::cout << FormatIssuesAsJson(issues);
142 } else {
143 std::cout << FormatIssuesAsText(issues);
144 }
145
146 formatter.AddField("status", "complete");
147 return absl::OkStatus();
148}
149
150std::vector<ValidationIssue> RomValidateTool::ValidateHeader(Rom* rom) {
151 std::vector<ValidationIssue> issues;
152
153 // Check ROM title
154 std::string title = rom->title();
155 if (title.empty()) {
156 issues.push_back({ValidationIssue::Severity::kWarning, "header",
157 "ROM title is empty", 0x7FC0});
158 } else {
159 issues.push_back({ValidationIssue::Severity::kInfo, "header",
160 "ROM title: " + title, 0x7FC0});
161 }
162
163 // Check map mode
164 auto map_mode = rom->ReadByte(0x7FD5);
165 if (map_mode.ok()) {
166 uint8_t mode = *map_mode;
167 if ((mode & 0x01) == 0) {
168 issues.push_back({ValidationIssue::Severity::kInfo, "header",
169 "ROM is LoROM mapping", 0x7FD5});
170 } else {
171 issues.push_back({ValidationIssue::Severity::kInfo, "header",
172 "ROM is HiROM mapping", 0x7FD5});
173 }
174 }
175
176 // Check ROM type
177 auto rom_type = rom->ReadByte(0x7FD6);
178 if (rom_type.ok()) {
179 uint8_t type = *rom_type;
180 if (type == 0x00) {
181 issues.push_back(
182 {ValidationIssue::Severity::kInfo, "header", "ROM only", 0x7FD6});
183 } else if (type == 0x02) {
184 issues.push_back({ValidationIssue::Severity::kInfo, "header",
185 "ROM + SRAM", 0x7FD6});
186 }
187 }
188
189 return issues;
190}
191
192std::vector<ValidationIssue> RomValidateTool::ValidateChecksum(Rom* rom) {
193 std::vector<ValidationIssue> issues;
194
195 auto checksum = rom->ReadWord(0x7FDC);
196 auto complement = rom->ReadWord(0x7FDE);
197
198 if (checksum.ok() && complement.ok()) {
199 uint16_t sum = *checksum;
200 uint16_t comp = *complement;
201
202 // Checksum + Complement should equal 0xFFFF
203 if ((sum + comp) == 0xFFFF) {
204 issues.push_back({ValidationIssue::Severity::kInfo, "checksum",
205 absl::StrFormat("Header checksum valid (0x%04X)", sum),
206 0x7FDC});
207 } else {
208 issues.push_back(
210 absl::StrFormat(
211 "Header checksum invalid (0x%04X + 0x%04X != 0xFFFF)", sum,
212 comp),
213 0x7FDC});
214 }
215
216 // Calculate actual checksum
217 uint32_t actual_sum = 0;
218 for (size_t i = 0; i < rom->size(); ++i) {
219 actual_sum += (*rom)[i];
220 }
221 actual_sum = (actual_sum & 0xFFFF);
222
223 if (static_cast<uint16_t>(actual_sum) == sum) {
224 issues.push_back({ValidationIssue::Severity::kInfo, "checksum",
225 "Calculated checksum matches header", 0});
226 } else {
227 issues.push_back(
229 absl::StrFormat(
230 "Calculated checksum (0x%04X) differs from header (0x%04X)",
231 actual_sum, sum),
232 0});
233 }
234 } else {
235 issues.push_back({ValidationIssue::Severity::kError, "checksum",
236 "Failed to read checksum from header", 0x7FDC});
237 }
238
239 return issues;
240}
241
242std::vector<ValidationIssue> RomValidateTool::ValidateSize(Rom* rom) {
243 std::vector<ValidationIssue> issues;
244
245 size_t size = rom->size();
246
247 // Check for common sizes
248 if (size == 0x100000) { // 1MB
249 issues.push_back({ValidationIssue::Severity::kInfo, "size",
250 "ROM size: 1MB (standard)", 0});
251 } else if (size == 0x200000) { // 2MB
252 issues.push_back({ValidationIssue::Severity::kInfo, "size",
253 "ROM size: 2MB (expanded)", 0});
254 } else if (size == 0x400000) { // 4MB
255 issues.push_back({ValidationIssue::Severity::kInfo, "size",
256 "ROM size: 4MB (fully expanded)", 0});
257 } else {
258 issues.push_back(
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(
387 absl::StrFormat("Only %d/%d palettes accessible", valid_palettes,
388 kNumPalettes),
389 kPaletteBase});
390 }
391
392 return issues;
393}
394
395std::vector<ValidationIssue> DataValidateTool::ValidateEntrances(Rom* rom) {
396 std::vector<ValidationIssue> issues;
397
398 // Check entrance data
399 constexpr uint32_t kEntranceBase = 0x02C577;
400 constexpr int kNumEntrances = 0x85; // 133 entrances
401
402 int valid_entrances = 0;
403 for (int i = 0; i < kNumEntrances; ++i) {
404 auto room_id = rom->ReadWord(kEntranceBase + (i * 2));
405 if (room_id.ok()) {
406 if (*room_id < 296) { // Valid room IDs are 0-295
407 valid_entrances++;
408 }
409 }
410 }
411
412 if (valid_entrances == kNumEntrances) {
413 issues.push_back({ValidationIssue::Severity::kInfo, "entrances",
414 "All entrance room IDs valid", kEntranceBase});
415 } else {
416 issues.push_back(
418 absl::StrFormat("%d/%d entrances have valid room IDs",
419 valid_entrances, kNumEntrances),
420 kEntranceBase});
421 }
422
423 return issues;
424}
425
426// =============================================================================
427// PatchCheckTool
428// =============================================================================
429
430absl::Status PatchCheckTool::Execute(Rom* rom,
431 const resources::ArgumentParser& parser,
432 resources::OutputFormatter& formatter) {
433 std::string patch_path = parser.GetString("patch").value();
434
435 std::vector<ValidationIssue> issues;
436
437 // Check free space availability
438 auto space_issues = CheckFreeSpace(rom);
439 issues.insert(issues.end(), space_issues.begin(), space_issues.end());
440
441 // Check for hook conflicts
442 auto hook_issues = CheckHooks(rom, patch_path);
443 issues.insert(issues.end(), hook_issues.begin(), hook_issues.end());
444
445 std::string format = parser.GetString("format").value_or("json");
446 if (format == "json") {
447 std::cout << FormatIssuesAsJson(issues);
448 } else {
449 std::cout << FormatIssuesAsText(issues);
450 }
451
452 formatter.AddField("status", "complete");
453 return absl::OkStatus();
454}
455
456std::vector<ValidationIssue> PatchCheckTool::CheckFreeSpace(Rom* rom) {
457 std::vector<ValidationIssue> issues;
458
459 // Check common free space regions
460 struct FreeSpaceRegion {
461 uint32_t start;
462 uint32_t end;
463 const char* name;
464 };
465
466 std::vector<FreeSpaceRegion> regions = {
467 {0x1F8000, 0x1FFFFF, "Bank $3F"},
468 {0x0FFF00, 0x0FFFFF, "Bank $1F end"},
469 };
470
471 for (const auto& region : regions) {
472 if (region.end > rom->size()) {
473 issues.push_back({ValidationIssue::Severity::kInfo, "free_space",
474 absl::StrFormat("%s not available (ROM too small)",
475 region.name),
476 region.start});
477 continue;
478 }
479
480 // Check if region is mostly 0xFF (free space marker)
481 int free_bytes = 0;
482 for (uint32_t addr = region.start; addr < region.end; ++addr) {
483 if ((*rom)[addr] == 0xFF || (*rom)[addr] == 0x00) {
484 free_bytes++;
485 }
486 }
487
488 int region_size = region.end - region.start;
489 int free_percent = (free_bytes * 100) / region_size;
490
491 if (free_percent > 80) {
492 issues.push_back(
494 absl::StrFormat("%s: %d%% free (%d bytes)", region.name,
495 free_percent, free_bytes),
496 region.start});
497 } else {
498 issues.push_back(
500 absl::StrFormat("%s: only %d%% free (%d bytes)", region.name,
501 free_percent, free_bytes),
502 region.start});
503 }
504 }
505
506 return issues;
507}
508
509std::vector<ValidationIssue> PatchCheckTool::CheckHooks(
510 Rom* rom, const std::string& patch_path) {
511 std::vector<ValidationIssue> issues;
512
513 // Check if patch file exists
514 std::ifstream patch_file(patch_path);
515 if (!patch_file.good()) {
516 issues.push_back({ValidationIssue::Severity::kError, "patch",
517 "Patch file not found: " + patch_path, 0});
518 return issues;
519 }
520
521 // Check common hook locations for existing modifications
522 struct HookLocation {
523 uint32_t address;
524 const char* name;
525 uint8_t original_byte;
526 };
527
528 std::vector<HookLocation> hooks = {
529 {0x008027, "Reset vector", 0x8C},
530 {0x008040, "NMI vector", 0x5C},
531 {0x0080B5, "IRQ vector", 0x8B},
532 };
533
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) {
538 issues.push_back(
540 absl::StrFormat("%s already modified (0x%02X != 0x%02X)",
541 hook.name, *byte, hook.original_byte),
542 hook.address});
543 }
544 }
545 }
546
547 issues.push_back({ValidationIssue::Severity::kInfo, "patch",
548 "Patch file exists: " + patch_path, 0});
549
550 return issues;
551}
552
553// =============================================================================
554// ValidateAllTool
555// =============================================================================
556
558 const resources::ArgumentParser& parser,
559 resources::OutputFormatter& formatter) {
560 std::vector<ValidationIssue> all_issues;
561 bool strict = parser.HasFlag("strict");
562
563 // Run ROM validation
564 RomValidateTool rom_validator;
565 auto header_issues = rom_validator.ValidateHeader(rom);
566 all_issues.insert(all_issues.end(), header_issues.begin(),
567 header_issues.end());
568 auto checksum_issues = rom_validator.ValidateChecksum(rom);
569 all_issues.insert(all_issues.end(), checksum_issues.begin(),
570 checksum_issues.end());
571 auto size_issues = rom_validator.ValidateSize(rom);
572 all_issues.insert(all_issues.end(), size_issues.begin(), size_issues.end());
573
574 // Run data validation
575 DataValidateTool data_validator;
576 auto sprite_issues = data_validator.ValidateSprites(rom);
577 all_issues.insert(all_issues.end(), sprite_issues.begin(),
578 sprite_issues.end());
579 auto tile_issues = data_validator.ValidateTiles(rom);
580 all_issues.insert(all_issues.end(), tile_issues.begin(), tile_issues.end());
581 auto palette_issues = data_validator.ValidatePalettes(rom);
582 all_issues.insert(all_issues.end(), palette_issues.begin(),
583 palette_issues.end());
584 auto entrance_issues = data_validator.ValidateEntrances(rom);
585 all_issues.insert(all_issues.end(), entrance_issues.begin(),
586 entrance_issues.end());
587
588 // Check for critical issues in strict mode
589 if (strict) {
590 for (const auto& issue : all_issues) {
591 if (issue.severity == ValidationIssue::Severity::kCritical ||
592 issue.severity == ValidationIssue::Severity::kError) {
593 return absl::InvalidArgumentError(
594 "Validation failed: " + issue.message);
595 }
596 }
597 }
598
599 std::string format = parser.GetString("format").value_or("json");
600 if (format == "json") {
601 std::cout << FormatIssuesAsJson(all_issues);
602 } else {
603 std::cout << FormatIssuesAsText(all_issues);
604 }
605
606 formatter.AddField("status", "complete");
607 return absl::OkStatus();
608}
609
610} // namespace tools
611} // namespace agent
612} // namespace cli
613} // namespace yaze
614
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:228
auto size() const
Definition rom.h:134
absl::StatusOr< uint8_t > ReadByte(int offset)
Definition rom.cc:221
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.