yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_doctor_commands.cc
Go to the documentation of this file.
2
3#include <fstream>
4#include <iostream>
5#include <map>
6#include <memory>
7#include <optional>
8#include <vector>
9
10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
13#include "core/asar_wrapper.h"
14#include "rom/rom.h"
15#include "util/platform_paths.h"
20
21namespace yaze::cli {
22
23namespace {
24
25// =============================================================================
26// Address Conversion Helpers
27// =============================================================================
28
29uint32_t SnesToPc(uint32_t snes_addr) {
30 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
31}
32
33// Check if a tile16 entry looks valid
34bool IsTile16Valid(uint16_t tile_info) {
35 // Tile info format: tttttttt ttttpppp hvf00000
36 // Bits 8-12 (0x1F00) should be 0 for valid tiles (unless flip bits are set)
37 // Returns false if reserved bits are set without flip bits
38 return (tile_info & 0x1F00) == 0 || (tile_info & 0xE000) != 0;
39}
40
41// =============================================================================
42// Feature Detection
43// =============================================================================
44
46 RomFeatures features;
47
48 // Detect ZSCustomOverworld version
49 if (kZSCustomVersionPos < rom->size()) {
51 features.is_vanilla = (features.zs_custom_version == 0xFF ||
52 features.zs_custom_version == 0x00);
53 features.is_v2 = (!features.is_vanilla && features.zs_custom_version == 2);
54 features.is_v3 = (!features.is_vanilla && features.zs_custom_version >= 3);
55 } else {
56 features.is_vanilla = true;
57 }
58
59 // Detect expanded tile16/tile32 (only if ASM applied)
60 if (!features.is_vanilla) {
61 if (kMap16ExpandedFlagPos < rom->size()) {
62 uint8_t flag = rom->data()[kMap16ExpandedFlagPos];
63 features.has_expanded_tile16 = (flag != 0x0F);
64 }
65
66 if (kMap32ExpandedFlagPos < rom->size()) {
67 uint8_t flag = rom->data()[kMap32ExpandedFlagPos];
68 features.has_expanded_tile32 = (flag != 0x04);
69 }
70 }
71
72 // Detect expanded pointer tables via ASM marker
73 if (kExpandedPtrTableMarker < rom->size()) {
76 }
77
78 // Detect ZSCustomOverworld feature enables
79 if (!features.is_vanilla) {
80 if (kCustomBGEnabledPos < rom->size()) {
81 features.custom_bg_enabled = (rom->data()[kCustomBGEnabledPos] != 0);
82 }
83 if (kCustomMainPalettePos < rom->size()) {
85 (rom->data()[kCustomMainPalettePos] != 0);
86 }
87 if (kCustomMosaicPos < rom->size()) {
88 features.custom_mosaic_enabled = (rom->data()[kCustomMosaicPos] != 0);
89 }
90 if (kCustomAnimatedGFXPos < rom->size()) {
92 (rom->data()[kCustomAnimatedGFXPos] != 0);
93 }
94 if (kCustomOverlayPos < rom->size()) {
95 features.custom_overlay_enabled = (rom->data()[kCustomOverlayPos] != 0);
96 }
97 if (kCustomTileGFXPos < rom->size()) {
98 features.custom_tile_gfx_enabled = (rom->data()[kCustomTileGFXPos] != 0);
99 }
100 }
101
102 return features;
103}
104
105// =============================================================================
106// Map Pointer Validation
107// =============================================================================
108
110 report.map_status.lw_dw_maps_valid = true;
111 report.map_status.sw_maps_valid = true;
112
113 for (int map_id = 0; map_id < kVanillaMapCount; ++map_id) {
114 uint32_t ptr_low_addr = kPtrTableLowBase + (3 * map_id);
115 uint32_t ptr_high_addr = kPtrTableHighBase + (3 * map_id);
116
117 if (ptr_low_addr + 3 > rom->size() || ptr_high_addr + 3 > rom->size()) {
119 if (map_id < 0x80) {
120 report.map_status.lw_dw_maps_valid = false;
121 } else {
122 report.map_status.sw_maps_valid = false;
123 }
124 continue;
125 }
126
127 uint32_t snes_low = rom->data()[ptr_low_addr] |
128 (rom->data()[ptr_low_addr + 1] << 8) |
129 (rom->data()[ptr_low_addr + 2] << 16);
130 uint32_t snes_high = rom->data()[ptr_high_addr] |
131 (rom->data()[ptr_high_addr + 1] << 8) |
132 (rom->data()[ptr_high_addr + 2] << 16);
133
134 uint32_t pc_low = SnesToPc(snes_low);
135 uint32_t pc_high = SnesToPc(snes_high);
136
137 bool low_valid = (pc_low > 0 && pc_low < rom->size());
138 bool high_valid = (pc_high > 0 && pc_high < rom->size());
139
140 if (!low_valid || !high_valid) {
142 if (map_id < 0x80) {
143 report.map_status.lw_dw_maps_valid = false;
144 } else {
145 report.map_status.sw_maps_valid = false;
146 }
147
148 DiagnosticFinding finding;
149 finding.id = "invalid_map_pointer";
151 finding.message =
152 absl::StrFormat("Map 0x%02X has invalid pointer", map_id);
153 finding.location = absl::StrFormat("0x%06X", ptr_low_addr);
154 finding.suggested_action = "Restore from baseline ROM";
155 finding.fixable = false;
156 report.AddFinding(finding);
157 }
158 }
159
160 // Tail maps status
165
166 // Add finding if map pointer corruption detected
167 if (!report.map_status.lw_dw_maps_valid) {
168 DiagnosticFinding finding;
169 finding.id = "lw_dw_corruption";
171 finding.message = "Light/Dark World map pointers are corrupted";
172 finding.location = absl::StrFormat("0x%06X-0x%06X", kPtrTableLowBase,
173 kPtrTableLowBase + 0x180);
174 finding.suggested_action =
175 "ROM may be severely damaged. Restore from backup.";
176 finding.fixable = false;
177 report.AddFinding(finding);
178 }
179
180 if (!report.map_status.sw_maps_valid) {
181 DiagnosticFinding finding;
182 finding.id = "sw_corruption";
184 finding.message = "Special World map pointers are corrupted";
185 finding.location = absl::StrFormat(
186 "0x%06X-0x%06X", kPtrTableLowBase + 0x180, kPtrTableHighBase);
187 finding.suggested_action = "Restore Special World data from baseline";
188 finding.fixable = false;
189 report.AddFinding(finding);
190 }
191}
192
193// =============================================================================
194// Tile16 Corruption Check
195// =============================================================================
196
199
200 if (!report.features.has_expanded_tile16) {
201 return;
202 }
203
204 for (uint32_t addr : kProblemAddresses) {
205 if (addr >= kMap16TilesExpanded && addr < kMap16TilesExpandedEnd) {
206 int tile_offset = addr - kMap16TilesExpanded;
207 int tile_index = tile_offset / 8;
208
209 uint16_t tile_data[4];
210 for (int i = 0; i < 4 && (addr + i * 2 + 1) < rom->size(); ++i) {
211 tile_data[i] =
212 rom->data()[addr + i * 2] | (rom->data()[addr + i * 2 + 1] << 8);
213 }
214
215 bool looks_valid = true;
216 for (int i = 0; i < 4; ++i) {
217 if (!IsTile16Valid(tile_data[i])) {
218 looks_valid = false;
219 break;
220 }
221 }
222
223 if (!looks_valid) {
225 report.tile16_status.corrupted_addresses.push_back(addr);
227
228 DiagnosticFinding finding;
229 finding.id = "tile16_corruption";
231 finding.message = absl::StrFormat("Corrupted tile16 #%d", tile_index);
232 finding.location = absl::StrFormat("0x%06X", addr);
233 finding.suggested_action = "Run with --fix to zero corrupted entries";
234 finding.fixable = true;
235 report.AddFinding(finding);
236 }
237 }
238 }
239}
240
241// =============================================================================
242// Baseline ROM Loading
243// =============================================================================
244
245std::unique_ptr<Rom> LoadBaselineRom(const std::optional<std::string>& path,
246 std::string* resolved_path) {
247 std::vector<std::string> candidates;
248 if (path.has_value()) {
249 candidates.push_back(*path);
250 } else {
251 candidates = {"alttp_vanilla.sfc", "vanilla.sfc", "zelda3.sfc"};
252 }
253
254 for (const auto& candidate : candidates) {
255 std::ifstream probe(candidate, std::ios::binary);
256 if (!probe.good())
257 continue;
258 probe.close();
259
260 auto baseline = std::make_unique<Rom>();
261 auto status = baseline->LoadFromFile(candidate);
262 if (status.ok()) {
263 if (resolved_path)
264 *resolved_path = candidate;
265 return baseline;
266 }
267 }
268
269 return nullptr;
270}
271
272// =============================================================================
273// Distribution Stats for Entity Coverage
274// =============================================================================
275
276template <typename T, typename Getter>
277MapDistributionStats BuildDistribution(const std::vector<T>& entries,
278 Getter getter) {
280 for (const auto& entry : entries) {
281 uint16_t map = getter(entry);
282 stats.counts[map]++;
283 stats.total++;
284 if (map >= zelda3::kNumOverworldMaps) {
285 stats.invalid++;
286 }
287 }
288 stats.unique = static_cast<int>(stats.counts.size());
289
290 for (const auto& [map, count] : stats.counts) {
291 if (count > stats.most_common_count) {
292 stats.most_common_count = count;
293 stats.most_common_map = map;
294 }
295 }
296 return stats;
297}
298
299absl::StatusOr<std::vector<zelda3::OverworldMap>> BuildOverworldMaps(Rom* rom) {
300 std::vector<zelda3::OverworldMap> maps;
301 maps.reserve(zelda3::kNumOverworldMaps);
302 for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
303 maps.emplace_back(i, rom);
304 }
305 return maps;
306}
307
308// =============================================================================
309// Repair Functions
310// =============================================================================
311
312absl::Status RepairTile16Region(Rom* rom, const DiagnosticReport& report,
313 bool dry_run) {
315 return absl::OkStatus();
316 }
317
318 for (uint32_t addr : report.tile16_status.corrupted_addresses) {
319 if (!dry_run) {
320 for (int i = 0; i < 8 && addr + i < rom->size(); ++i) {
321 (*rom)[addr + i] = 0x00;
322 }
323 }
324 }
325
326 return absl::OkStatus();
327}
328
329// Apply tail map expansion ASM patch
330absl::Status ApplyTailExpansion(Rom* rom, bool dry_run, bool verbose) {
331 // Check if already applied
332 if (kExpandedPtrTableMarker < rom->size() &&
334 return absl::AlreadyExistsError(
335 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
336 }
337
338 // Check if ZSCustomOverworld v3 is present (required prerequisite)
339 if (kZSCustomVersionPos < rom->size()) {
340 uint8_t version = rom->data()[kZSCustomVersionPos];
341 if (version < 3 && version != 0xFF && version != 0x00) {
342 return absl::FailedPreconditionError(
343 "Tail map expansion requires ZSCustomOverworld v3 or later. "
344 "Apply ZSCustomOverworld v3 first.");
345 }
346 }
347
348 if (dry_run) {
349 return absl::OkStatus();
350 }
351
352 auto patch_path =
353 util::PlatformPaths::FindAsset("patches/Overworld/TailMapExpansion.asm");
354 if (!patch_path.ok()) {
355 return absl::NotFoundError(
356 "TailMapExpansion.asm patch file not found. "
357 "Expected it in the Yaze runtime assets.");
358 }
359
360 // Apply the patch using Asar
363
364 std::vector<uint8_t> rom_data(rom->data(), rom->data() + rom->size());
365 auto result = asar.ApplyPatch(patch_path->string(), rom_data);
366
367 if (!result.ok()) {
368 return result.status();
369 }
370
371 if (!result->success) {
372 std::string error_msg = "Asar patch failed:";
373 for (const auto& err : result->errors) {
374 error_msg += " " + err;
375 }
376 return absl::InternalError(error_msg);
377 }
378
379 // Handle ROM size changes - patches may expand the ROM for custom code
380 if (rom_data.size() > rom->size()) {
381 if (verbose) {
382 std::cout << absl::StrFormat(" Expanding ROM from %zu to %zu bytes\n",
383 rom->size(), rom_data.size());
384 }
385 rom->Expand(static_cast<int>(rom_data.size()));
386 } else if (rom_data.size() < rom->size()) {
387 // ROM shrinking is unexpected and likely an error
388 return absl::InternalError(
389 absl::StrFormat("ROM size decreased unexpectedly: %zu -> %zu",
390 rom->size(), rom_data.size()));
391 }
392
393 // Copy patched data back to ROM
394 for (size_t i = 0; i < rom_data.size(); ++i) {
395 (*rom)[i] = rom_data[i];
396 }
397
398 // Verify marker was written (with bounds check to prevent buffer overflow)
399 if (kExpandedPtrTableMarker >= rom->size()) {
400 return absl::InternalError(
401 absl::StrFormat("ROM too small for expansion marker at 0x%06X "
402 "(ROM size: 0x%06zX). Patch may have failed.",
404 }
406 return absl::InternalError(
407 "Patch applied but marker not found. Patch may be incomplete.");
408 }
409
410 return absl::OkStatus();
411}
412
413// =============================================================================
414// Output Helpers
415// =============================================================================
416
418 const RomFeatures& features) {
419 formatter.AddField("zs_custom_version", features.GetVersionString());
420 formatter.AddField("is_vanilla", features.is_vanilla);
421 formatter.AddField("expanded_tile16", features.has_expanded_tile16);
422 formatter.AddField("expanded_tile32", features.has_expanded_tile32);
423 formatter.AddField("expanded_pointer_tables",
425
426 if (!features.is_vanilla) {
427 formatter.AddField("custom_bg_enabled", features.custom_bg_enabled);
428 formatter.AddField("custom_main_palette_enabled",
430 formatter.AddField("custom_mosaic_enabled", features.custom_mosaic_enabled);
431 formatter.AddField("custom_animated_gfx_enabled",
433 formatter.AddField("custom_overlay_enabled",
434 features.custom_overlay_enabled);
435 formatter.AddField("custom_tile_gfx_enabled",
436 features.custom_tile_gfx_enabled);
437 }
438}
439
441 const MapPointerStatus& status) {
442 formatter.AddField("lw_dw_maps_valid", status.lw_dw_maps_valid);
443 formatter.AddField("sw_maps_valid", status.sw_maps_valid);
444 formatter.AddField("tail_maps_available", status.can_support_tail);
445 formatter.AddField("invalid_map_count", status.invalid_map_count);
446}
447
449 const DiagnosticReport& report) {
450 formatter.BeginArray("findings");
451 for (const auto& finding : report.findings) {
452 formatter.AddArrayItem(finding.FormatJson());
453 }
454 formatter.EndArray();
455}
456
458 const DiagnosticReport& report) {
459 formatter.AddField("total_findings", report.TotalFindings());
460 formatter.AddField("critical_count", report.critical_count);
461 formatter.AddField("error_count", report.error_count);
462 formatter.AddField("warning_count", report.warning_count);
463 formatter.AddField("info_count", report.info_count);
464 formatter.AddField("fixable_count", report.fixable_count);
465 formatter.AddField("has_problems", report.HasProblems());
466}
467
468void OutputTextBanner(bool is_json) {
469 if (is_json)
470 return;
471 std::cout << "\n";
472 std::cout
473 << "╔═══════════════════════════════════════════════════════════════╗\n";
474 std::cout
475 << "║ OVERWORLD DOCTOR ║\n";
476 std::cout
477 << "║ ROM Diagnostic & Repair Tool ║\n";
478 std::cout
479 << "╚═══════════════════════════════════════════════════════════════╝\n";
480}
481
482void OutputTextSummary(const DiagnosticReport& report) {
483 std::cout << "\n";
484 std::cout
485 << "╔═══════════════════════════════════════════════════════════════╗\n";
486 std::cout
487 << "║ DIAGNOSTIC SUMMARY ║\n";
488 std::cout
489 << "╠═══════════════════════════════════════════════════════════════╣\n";
490
491 std::cout << absl::StrFormat("║ ROM Version: %-46s ║\n",
492 report.features.GetVersionString());
493
494 std::cout << absl::StrFormat(
495 "║ Expanded Tile16: %-42s ║\n",
496 report.features.has_expanded_tile16 ? "YES" : "NO");
497 std::cout << absl::StrFormat(
498 "║ Expanded Tile32: %-42s ║\n",
499 report.features.has_expanded_tile32 ? "YES" : "NO");
500 std::cout << absl::StrFormat("║ Expanded Ptr Tables: %-38s ║\n",
502 ? "YES (192 maps)"
503 : "NO (160 maps)");
504
505 std::cout
506 << "╠═══════════════════════════════════════════════════════════════╣\n";
507
508 std::cout << absl::StrFormat(
509 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
510 report.map_status.lw_dw_maps_valid ? "OK" : "CORRUPTED");
511 std::cout << absl::StrFormat(
512 "║ Special World (0x80-0x9F): %-32s ║\n",
513 report.map_status.sw_maps_valid ? "OK" : "CORRUPTED");
514 std::cout << absl::StrFormat("║ Tail Maps (0xA0-0xBF): %-36s ║\n",
516 ? "Available"
517 : "N/A (no ASM expansion)");
518
519 if (report.tile16_status.uses_expanded) {
520 std::cout << "╠════════════════════════════════════════════════════════════"
521 "═══╣\n";
523 std::cout << absl::StrFormat(
524 "║ Tile16 Corruption: DETECTED (%zu addresses)%-17s ║\n",
525 report.tile16_status.corrupted_addresses.size(), "");
526 for (uint32_t addr : report.tile16_status.corrupted_addresses) {
527 int tile_idx = (addr - kMap16TilesExpanded) / 8;
528 std::cout << absl::StrFormat("║ - 0x%06X (tile #%d)%-36s ║\n", addr,
529 tile_idx, "");
530 }
531 } else {
532 std::cout << "║ Tile16 Corruption: None detected "
533 " ║\n";
534 }
535 }
536
537 std::cout
538 << "╠═══════════════════════════════════════════════════════════════╣\n";
539 std::cout << absl::StrFormat("║ Total Findings: %-43d ║\n",
540 report.TotalFindings());
541 std::cout << absl::StrFormat(
542 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
543 report.critical_count, report.error_count, report.warning_count,
544 report.info_count, "");
545 std::cout << absl::StrFormat("║ Fixable Issues: %-43d ║\n",
546 report.fixable_count);
547 std::cout
548 << "╚═══════════════════════════════════════════════════════════════╝\n";
549}
550
552 if (report.findings.empty()) {
553 return;
554 }
555
556 std::cout << "\n=== Detailed Findings ===\n";
557 for (const auto& finding : report.findings) {
558 std::cout << " " << finding.FormatText() << "\n";
559 if (!finding.suggested_action.empty()) {
560 std::cout << " → " << finding.suggested_action << "\n";
561 }
562 }
563}
564
565} // namespace
566
568 Rom* rom, const resources::ArgumentParser& parser,
569 resources::OutputFormatter& formatter) {
570 bool fix_mode = parser.HasFlag("fix");
571 bool apply_tail_expansion = parser.HasFlag("apply-tail-expansion");
572 bool dry_run = parser.HasFlag("dry-run");
573 bool verbose = parser.HasFlag("verbose");
574 auto output_path = parser.GetString("output");
575 auto baseline_path = parser.GetString("baseline");
576 bool is_json = formatter.IsJson();
577
578 // Show text banner for text mode
579 OutputTextBanner(is_json);
580
581 // Build diagnostic report
582 DiagnosticReport report;
583 report.rom_path = rom->filename();
584 report.features = DetectRomFeatures(rom);
585 ValidateMapPointers(rom, report);
586 CheckTile16Corruption(rom, report);
587
588 // Load baseline if provided
589 std::string resolved_baseline;
590 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
591
592 // Add info finding if no ASM expansion for tail maps
594 DiagnosticFinding finding;
595 finding.id = "no_tail_support";
597 finding.message = "Tail maps (0xA0-0xBF) not available";
598 finding.location = "";
599 finding.suggested_action =
600 "Apply TailMapExpansion.asm patch (after ZSCustomOverworld v3) to "
601 "expand pointer tables to 192 entries. Use: z3ed overworld-doctor "
602 "--apply-tail-expansion or apply manually with Asar.";
603 finding.fixable = false;
604 report.AddFinding(finding);
605 }
606
607 // Output to formatter
608 formatter.AddField("rom_path", report.rom_path);
609 formatter.AddField("fix_mode", fix_mode);
610 formatter.AddField("dry_run", dry_run);
611
612 // Features section
613 if (is_json) {
614 OutputFeaturesJson(formatter, report.features);
615 OutputMapStatusJson(formatter, report.map_status);
616 OutputFindingsJson(formatter, report);
617 OutputSummaryJson(formatter, report);
618 }
619
620 // Text mode: show nice ASCII summary
621 if (!is_json) {
622 OutputTextSummary(report);
623 if (verbose) {
624 OutputTextFindings(report);
625 }
626 }
627
628 // Entity coverage (text mode only for now)
629 if (!is_json) {
630 ASSIGN_OR_RETURN(auto exits, zelda3::LoadExits(rom));
631 ASSIGN_OR_RETURN(auto entrances, zelda3::LoadEntrances(rom));
632 ASSIGN_OR_RETURN(auto maps, BuildOverworldMaps(rom));
633 ASSIGN_OR_RETURN(auto items, zelda3::LoadItems(rom, maps));
634
635 auto exit_stats =
636 BuildDistribution(exits, [](const auto& exit) { return exit.map_id_; });
637 auto entrance_stats = BuildDistribution(entrances, [](const auto& ent) {
638 return static_cast<uint16_t>(ent.map_id_);
639 });
640 auto item_stats =
641 BuildDistribution(items, [](const auto& item) { return item.map_id_; });
642
643 std::cout << "\n=== Overworld Entity Coverage ===\n";
644 std::cout << absl::StrFormat(
645 " exits : total=%d unique=%d most_common=0x%02X (%d)\n",
646 exit_stats.total, exit_stats.unique, exit_stats.most_common_map,
647 exit_stats.most_common_count);
648 std::cout << absl::StrFormat(
649 " entrances : total=%d unique=%d most_common=0x%02X (%d)\n",
650 entrance_stats.total, entrance_stats.unique,
651 entrance_stats.most_common_map, entrance_stats.most_common_count);
652 std::cout << absl::StrFormat(
653 " items : total=%d unique=%d most_common=0x%02X (%d)\n",
654 item_stats.total, item_stats.unique, item_stats.most_common_map,
655 item_stats.most_common_count);
656
657 if (baseline_rom) {
658 std::cout << absl::StrFormat(" Baseline used: %s\n", resolved_baseline);
659 }
660 }
661
662 // Apply tail expansion if requested
663 if (apply_tail_expansion) {
664 if (dry_run) {
665 if (!is_json) {
666 std::cout << "\n=== Dry Run - Tail Map Expansion ===\n";
668 std::cout << " Tail expansion already applied.\n";
669 } else {
670 std::cout << " Would apply TailMapExpansion.asm patch.\n";
671 std::cout << " This will:\n";
672 std::cout << " - Relocate pointer tables to $28:A400\n";
673 std::cout << " - Expand from 160 to 192 map entries\n";
674 std::cout << " - Write marker byte 0xEA at $28:A3FF\n";
675 std::cout << " - Add blank map data at $30:8000\n";
676 }
677 std::cout << "\nNo changes made (dry run).\n";
678 }
679 formatter.AddField("dry_run_tail_expansion", true);
680 } else {
681 auto status = ApplyTailExpansion(rom, false, verbose);
682 if (status.ok()) {
683 if (!is_json) {
684 std::cout << "\n=== Tail Map Expansion Applied ===\n";
685 std::cout << " Pointer tables relocated to $28:A400/$28:A640\n";
686 std::cout << " Maps 0xA0-0xBF now available for editing\n";
687 }
688 formatter.AddField("tail_expansion_applied", true);
689
690 // Re-detect features after patch
691 report.features = DetectRomFeatures(rom);
692 } else if (absl::IsAlreadyExists(status)) {
693 if (!is_json) {
694 std::cout << "\n[INFO] Tail expansion already applied.\n";
695 }
696 formatter.AddField("tail_expansion_already_applied", true);
697 } else {
698 if (!is_json) {
699 std::cout << "\n[ERROR] Failed to apply tail expansion: "
700 << status.message() << "\n";
701 }
702 formatter.AddField("tail_expansion_error",
703 std::string(status.message()));
704 // Continue with diagnostics, don't fail the whole command
705 }
706 }
707 }
708
709 // Fix mode handling
710 if (fix_mode) {
711 if (dry_run) {
712 if (!is_json) {
713 std::cout << "\n=== Dry Run - Planned Fixes ===\n";
715 std::cout << absl::StrFormat(
716 " Would zero %zu corrupted tile16 entries\n",
717 report.tile16_status.corrupted_addresses.size());
718 for (uint32_t addr : report.tile16_status.corrupted_addresses) {
719 std::cout << absl::StrFormat(" - 0x%06X\n", addr);
720 }
721 } else {
722 std::cout << " No fixes needed.\n";
723 }
724 std::cout << "\nNo changes made (dry run).\n";
725 }
726 formatter.AddField(
727 "dry_run_fixes_planned",
728 static_cast<int>(report.tile16_status.corrupted_addresses.size()));
729 } else {
730 // Actually apply fixes
732 RETURN_IF_ERROR(RepairTile16Region(rom, report, false));
733 if (!is_json) {
734 std::cout << "\n=== Fixes Applied ===\n";
735 std::cout << absl::StrFormat(
736 " Zeroed %zu corrupted tile16 entries\n",
737 report.tile16_status.corrupted_addresses.size());
738 }
739 formatter.AddField("fixes_applied", true);
740 formatter.AddField(
741 "tile16_entries_fixed",
742 static_cast<int>(report.tile16_status.corrupted_addresses.size()));
743 }
744
745 // Save if output path provided
746 if (output_path.has_value()) {
747 Rom::SaveSettings settings;
748 settings.filename = output_path.value();
749 RETURN_IF_ERROR(rom->SaveToFile(settings));
750 if (!is_json) {
751 std::cout << absl::StrFormat("\nSaved fixed ROM to: %s\n",
752 output_path.value());
753 }
754 formatter.AddField("output_file", output_path.value());
755 } else if (report.HasFixable()) {
756 if (!is_json) {
757 std::cout
758 << "\nNo output path specified. Use --output <path> to save.\n";
759 }
760 }
761 }
762 } else {
763 // Not in fix mode - show hint
764 if (!is_json && report.HasFixable()) {
765 std::cout << "\nTo apply available fixes, run with --fix flag.\n";
766 std::cout << "To preview fixes, use --fix --dry-run.\n";
767 std::cout << "To save to a new file, use --output <path>.\n";
768 }
769 }
770
771 return absl::OkStatus();
772}
773
774} // namespace yaze::cli
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
auto filename() const
Definition rom.h:175
void Expand(int size)
Definition rom.h:63
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:416
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
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.
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 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.
Modern C++ wrapper for Asar 65816 assembler integration.
absl::StatusOr< AsarPatchResult > ApplyPatch(const std::string &patch_path, std::vector< uint8_t > &rom_data, const std::vector< std::string > &include_paths={})
absl::Status Initialize()
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::unique_ptr< Rom > LoadBaselineRom(const std::optional< std::string > &path, std::string *resolved_path)
void OutputFeaturesJson(resources::OutputFormatter &formatter, const RomFeatures &features)
absl::Status RepairTile16Region(Rom *rom, const DiagnosticReport &report, bool dry_run)
absl::Status ApplyTailExpansion(Rom *rom, bool dry_run, bool verbose)
void OutputSummaryJson(resources::OutputFormatter &formatter, const DiagnosticReport &report)
void OutputMapStatusJson(resources::OutputFormatter &formatter, const MapPointerStatus &status)
void OutputFindingsJson(resources::OutputFormatter &formatter, const DiagnosticReport &report)
absl::StatusOr< std::vector< zelda3::OverworldMap > > BuildOverworldMaps(Rom *rom)
MapDistributionStats BuildDistribution(const std::vector< T > &entries, Getter getter)
Namespace for the command line interface.
constexpr uint32_t kCustomBGEnabledPos
constexpr uint32_t kExpandedPtrTableMarker
constexpr uint32_t kPtrTableHighBase
constexpr uint32_t kCustomOverlayPos
constexpr uint8_t kExpandedPtrTableMagic
constexpr uint32_t kMap32ExpandedFlagPos
constexpr uint32_t kZSCustomVersionPos
constexpr uint32_t kMap16ExpandedFlagPos
const uint32_t kProblemAddresses[]
constexpr uint32_t kPtrTableLowBase
constexpr uint32_t kCustomMosaicPos
constexpr uint32_t kMap16TilesExpanded
constexpr uint32_t kCustomTileGFXPos
constexpr uint32_t kMap16TilesExpandedEnd
constexpr int kVanillaMapCount
constexpr uint32_t kCustomAnimatedGFXPos
constexpr uint32_t kCustomMainPalettePos
absl::StatusOr< std::vector< OverworldEntrance > > LoadEntrances(Rom *rom)
absl::StatusOr< std::vector< OverworldItem > > LoadItems(Rom *rom, std::vector< OverworldMap > &overworld_maps)
constexpr int kNumOverworldMaps
Definition common.h:85
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::string filename
Definition rom.h:33
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.
bool HasFixable() const
Check if report has any fixable findings.
Entity distribution statistics for coverage analysis.
std::map< uint16_t, int > counts
Map pointer validation status.
ROM feature detection results.
std::string GetVersionString() const
Get version as human-readable string.
std::vector< uint32_t > corrupted_addresses