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