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"
12#include "rom/rom.h"
14#include "core/asar_wrapper.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 =
51 (features.zs_custom_version == 0xFF || 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
162
163 // Add finding if map pointer corruption detected
164 if (!report.map_status.lw_dw_maps_valid) {
165 DiagnosticFinding finding;
166 finding.id = "lw_dw_corruption";
168 finding.message = "Light/Dark World map pointers are corrupted";
169 finding.location = absl::StrFormat("0x%06X-0x%06X", kPtrTableLowBase,
170 kPtrTableLowBase + 0x180);
171 finding.suggested_action =
172 "ROM may be severely damaged. Restore from backup.";
173 finding.fixable = false;
174 report.AddFinding(finding);
175 }
176
177 if (!report.map_status.sw_maps_valid) {
178 DiagnosticFinding finding;
179 finding.id = "sw_corruption";
181 finding.message = "Special World map pointers are corrupted";
182 finding.location = absl::StrFormat("0x%06X-0x%06X", kPtrTableLowBase + 0x180,
184 finding.suggested_action = "Restore Special World data from baseline";
185 finding.fixable = false;
186 report.AddFinding(finding);
187 }
188}
189
190// =============================================================================
191// Tile16 Corruption Check
192// =============================================================================
193
196
197 if (!report.features.has_expanded_tile16) {
198 return;
199 }
200
201 for (uint32_t addr : kProblemAddresses) {
202 if (addr >= kMap16TilesExpanded && addr < kMap16TilesExpandedEnd) {
203 int tile_offset = addr - kMap16TilesExpanded;
204 int tile_index = tile_offset / 8;
205
206 uint16_t tile_data[4];
207 for (int i = 0; i < 4 && (addr + i * 2 + 1) < rom->size(); ++i) {
208 tile_data[i] =
209 rom->data()[addr + i * 2] | (rom->data()[addr + i * 2 + 1] << 8);
210 }
211
212 bool looks_valid = true;
213 for (int i = 0; i < 4; ++i) {
214 if (!IsTile16Valid(tile_data[i])) {
215 looks_valid = false;
216 break;
217 }
218 }
219
220 if (!looks_valid) {
222 report.tile16_status.corrupted_addresses.push_back(addr);
224
225 DiagnosticFinding finding;
226 finding.id = "tile16_corruption";
228 finding.message =
229 absl::StrFormat("Corrupted tile16 #%d", tile_index);
230 finding.location = absl::StrFormat("0x%06X", addr);
231 finding.suggested_action = "Run with --fix to zero corrupted entries";
232 finding.fixable = true;
233 report.AddFinding(finding);
234 }
235 }
236 }
237}
238
239// =============================================================================
240// Baseline ROM Loading
241// =============================================================================
242
243std::unique_ptr<Rom> LoadBaselineRom(const std::optional<std::string>& path,
244 std::string* resolved_path) {
245 std::vector<std::string> candidates;
246 if (path.has_value()) {
247 candidates.push_back(*path);
248 } else {
249 candidates = {"alttp_vanilla.sfc", "vanilla.sfc", "zelda3.sfc"};
250 }
251
252 for (const auto& candidate : candidates) {
253 std::ifstream probe(candidate, std::ios::binary);
254 if (!probe.good()) continue;
255 probe.close();
256
257 auto baseline = std::make_unique<Rom>();
258 auto status = baseline->LoadFromFile(candidate);
259 if (status.ok()) {
260 if (resolved_path) *resolved_path = candidate;
261 return baseline;
262 }
263 }
264
265 return nullptr;
266}
267
268// =============================================================================
269// Distribution Stats for Entity Coverage
270// =============================================================================
271
272template <typename T, typename Getter>
273MapDistributionStats BuildDistribution(const std::vector<T>& entries,
274 Getter getter) {
276 for (const auto& entry : entries) {
277 uint16_t map = getter(entry);
278 stats.counts[map]++;
279 stats.total++;
280 if (map >= zelda3::kNumOverworldMaps) {
281 stats.invalid++;
282 }
283 }
284 stats.unique = static_cast<int>(stats.counts.size());
285
286 for (const auto& [map, count] : stats.counts) {
287 if (count > stats.most_common_count) {
288 stats.most_common_count = count;
289 stats.most_common_map = map;
290 }
291 }
292 return stats;
293}
294
295absl::StatusOr<std::vector<zelda3::OverworldMap>> BuildOverworldMaps(Rom* rom) {
296 std::vector<zelda3::OverworldMap> maps;
297 maps.reserve(zelda3::kNumOverworldMaps);
298 for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
299 maps.emplace_back(i, rom);
300 }
301 return maps;
302}
303
304// =============================================================================
305// Repair Functions
306// =============================================================================
307
308absl::Status RepairTile16Region(Rom* rom, const DiagnosticReport& report,
309 bool dry_run) {
311 return absl::OkStatus();
312 }
313
314 for (uint32_t addr : report.tile16_status.corrupted_addresses) {
315 if (!dry_run) {
316 for (int i = 0; i < 8 && addr + i < rom->size(); ++i) {
317 (*rom)[addr + i] = 0x00;
318 }
319 }
320 }
321
322 return absl::OkStatus();
323}
324
325// Apply tail map expansion ASM patch
326absl::Status ApplyTailExpansion(Rom* rom, bool dry_run, bool verbose) {
327 // Check if already applied
328 if (kExpandedPtrTableMarker < rom->size() &&
330 return absl::AlreadyExistsError(
331 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
332 }
333
334 // Check if ZSCustomOverworld v3 is present (required prerequisite)
335 if (kZSCustomVersionPos < rom->size()) {
336 uint8_t version = rom->data()[kZSCustomVersionPos];
337 if (version < 3 && version != 0xFF && version != 0x00) {
338 return absl::FailedPreconditionError(
339 "Tail map expansion requires ZSCustomOverworld v3 or later. "
340 "Apply ZSCustomOverworld v3 first.");
341 }
342 }
343
344 if (dry_run) {
345 return absl::OkStatus();
346 }
347
348 // Find the patch file in standard locations
349 std::vector<std::string> patch_locations = {
350 "assets/patches/Overworld/TailMapExpansion.asm",
351 "../assets/patches/Overworld/TailMapExpansion.asm",
352 "TailMapExpansion.asm"
353 };
354
355 std::string patch_path;
356 for (const auto& loc : patch_locations) {
357 std::ifstream probe(loc);
358 if (probe.good()) {
359 patch_path = loc;
360 break;
361 }
362 }
363
364 if (patch_path.empty()) {
365 return absl::NotFoundError(
366 "TailMapExpansion.asm patch file not found. "
367 "Expected locations: assets/patches/Overworld/TailMapExpansion.asm");
368 }
369
370 // Apply the patch using Asar
373
374 std::vector<uint8_t> rom_data(rom->data(), rom->data() + rom->size());
375 auto result = asar.ApplyPatch(patch_path, rom_data);
376
377 if (!result.ok()) {
378 return result.status();
379 }
380
381 if (!result->success) {
382 std::string error_msg = "Asar patch failed:";
383 for (const auto& err : result->errors) {
384 error_msg += " " + err;
385 }
386 return absl::InternalError(error_msg);
387 }
388
389 // Handle ROM size changes - patches may expand the ROM for custom code
390 if (rom_data.size() > rom->size()) {
391 if (verbose) {
392 std::cout << absl::StrFormat(" Expanding ROM from %zu to %zu bytes\n",
393 rom->size(), rom_data.size());
394 }
395 rom->Expand(static_cast<int>(rom_data.size()));
396 } else if (rom_data.size() < rom->size()) {
397 // ROM shrinking is unexpected and likely an error
398 return absl::InternalError(
399 absl::StrFormat("ROM size decreased unexpectedly: %zu -> %zu",
400 rom->size(), rom_data.size()));
401 }
402
403 // Copy patched data back to ROM
404 for (size_t i = 0; i < rom_data.size(); ++i) {
405 (*rom)[i] = rom_data[i];
406 }
407
408 // Verify marker was written (with bounds check to prevent buffer overflow)
409 if (kExpandedPtrTableMarker >= rom->size()) {
410 return absl::InternalError(
411 absl::StrFormat("ROM too small for expansion marker at 0x%06X "
412 "(ROM size: 0x%06zX). Patch may have failed.",
414 }
416 return absl::InternalError(
417 "Patch applied but marker not found. Patch may be incomplete.");
418 }
419
420 return absl::OkStatus();
421}
422
423// =============================================================================
424// Output Helpers
425// =============================================================================
426
428 const RomFeatures& features) {
429 formatter.AddField("zs_custom_version", features.GetVersionString());
430 formatter.AddField("is_vanilla", features.is_vanilla);
431 formatter.AddField("expanded_tile16", features.has_expanded_tile16);
432 formatter.AddField("expanded_tile32", features.has_expanded_tile32);
433 formatter.AddField("expanded_pointer_tables",
435
436 if (!features.is_vanilla) {
437 formatter.AddField("custom_bg_enabled", features.custom_bg_enabled);
438 formatter.AddField("custom_main_palette_enabled",
440 formatter.AddField("custom_mosaic_enabled", features.custom_mosaic_enabled);
441 formatter.AddField("custom_animated_gfx_enabled",
443 formatter.AddField("custom_overlay_enabled", features.custom_overlay_enabled);
444 formatter.AddField("custom_tile_gfx_enabled",
445 features.custom_tile_gfx_enabled);
446 }
447}
448
450 const MapPointerStatus& status) {
451 formatter.AddField("lw_dw_maps_valid", status.lw_dw_maps_valid);
452 formatter.AddField("sw_maps_valid", status.sw_maps_valid);
453 formatter.AddField("tail_maps_available", status.can_support_tail);
454 formatter.AddField("invalid_map_count", status.invalid_map_count);
455}
456
458 const DiagnosticReport& report) {
459 formatter.BeginArray("findings");
460 for (const auto& finding : report.findings) {
461 formatter.AddArrayItem(finding.FormatJson());
462 }
463 formatter.EndArray();
464}
465
467 const DiagnosticReport& report) {
468 formatter.AddField("total_findings", report.TotalFindings());
469 formatter.AddField("critical_count", report.critical_count);
470 formatter.AddField("error_count", report.error_count);
471 formatter.AddField("warning_count", report.warning_count);
472 formatter.AddField("info_count", report.info_count);
473 formatter.AddField("fixable_count", report.fixable_count);
474 formatter.AddField("has_problems", report.HasProblems());
475}
476
477void OutputTextBanner(bool is_json) {
478 if (is_json) return;
479 std::cout << "\n";
480 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
481 std::cout << "║ OVERWORLD DOCTOR ║\n";
482 std::cout << "║ ROM Diagnostic & Repair Tool ║\n";
483 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
484}
485
486void OutputTextSummary(const DiagnosticReport& report) {
487 std::cout << "\n";
488 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
489 std::cout << "║ DIAGNOSTIC SUMMARY ║\n";
490 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
491
492 std::cout << absl::StrFormat(
493 "║ ROM Version: %-46s ║\n",
494 report.features.GetVersionString());
495
496 std::cout << absl::StrFormat(
497 "║ Expanded Tile16: %-42s ║\n",
498 report.features.has_expanded_tile16 ? "YES" : "NO");
499 std::cout << absl::StrFormat(
500 "║ Expanded Tile32: %-42s ║\n",
501 report.features.has_expanded_tile32 ? "YES" : "NO");
502 std::cout << absl::StrFormat(
503 "║ Expanded Ptr Tables: %-38s ║\n",
504 report.features.has_expanded_pointer_tables ? "YES (192 maps)"
505 : "NO (160 maps)");
506
507 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
508
509 std::cout << absl::StrFormat(
510 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
511 report.map_status.lw_dw_maps_valid ? "OK" : "CORRUPTED");
512 std::cout << absl::StrFormat(
513 "║ Special World (0x80-0x9F): %-32s ║\n",
514 report.map_status.sw_maps_valid ? "OK" : "CORRUPTED");
515 std::cout << absl::StrFormat(
516 "║ Tail Maps (0xA0-0xBF): %-36s ║\n",
517 report.map_status.can_support_tail ? "Available"
518 : "N/A (no ASM expansion)");
519
520 if (report.tile16_status.uses_expanded) {
521 std::cout << "╠═══════════════════════════════════════════════════════════════╣\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",
529 addr, tile_idx, "");
530 }
531 } else {
532 std::cout << "║ Tile16 Corruption: None detected ║\n";
533 }
534 }
535
536 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
537 std::cout << absl::StrFormat(
538 "║ Total Findings: %-43d ║\n", report.TotalFindings());
539 std::cout << absl::StrFormat(
540 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
541 report.critical_count, report.error_count, report.warning_count,
542 report.info_count, "");
543 std::cout << absl::StrFormat(
544 "║ Fixable Issues: %-43d ║\n", report.fixable_count);
545 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
546}
547
549 if (report.findings.empty()) {
550 return;
551 }
552
553 std::cout << "\n=== Detailed Findings ===\n";
554 for (const auto& finding : report.findings) {
555 std::cout << " " << finding.FormatText() << "\n";
556 if (!finding.suggested_action.empty()) {
557 std::cout << " → " << finding.suggested_action << "\n";
558 }
559 }
560}
561
562} // namespace
563
565 Rom* rom, const resources::ArgumentParser& parser,
566 resources::OutputFormatter& formatter) {
567 bool fix_mode = parser.HasFlag("fix");
568 bool apply_tail_expansion = parser.HasFlag("apply-tail-expansion");
569 bool dry_run = parser.HasFlag("dry-run");
570 bool verbose = parser.HasFlag("verbose");
571 auto output_path = parser.GetString("output");
572 auto baseline_path = parser.GetString("baseline");
573 bool is_json = formatter.IsJson();
574
575 // Show text banner for text mode
576 OutputTextBanner(is_json);
577
578 // Build diagnostic report
579 DiagnosticReport report;
580 report.rom_path = rom->filename();
581 report.features = DetectRomFeatures(rom);
582 ValidateMapPointers(rom, report);
583 CheckTile16Corruption(rom, report);
584
585 // Load baseline if provided
586 std::string resolved_baseline;
587 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
588
589 // Add info finding if no ASM expansion for tail maps
591 DiagnosticFinding finding;
592 finding.id = "no_tail_support";
594 finding.message = "Tail maps (0xA0-0xBF) not available";
595 finding.location = "";
596 finding.suggested_action =
597 "Apply TailMapExpansion.asm patch (after ZSCustomOverworld v3) to "
598 "expand pointer tables to 192 entries. Use: z3ed overworld-doctor "
599 "--apply-tail-expansion or apply manually with Asar.";
600 finding.fixable = false;
601 report.AddFinding(finding);
602 }
603
604 // Output to formatter
605 formatter.AddField("rom_path", report.rom_path);
606 formatter.AddField("fix_mode", fix_mode);
607 formatter.AddField("dry_run", dry_run);
608
609 // Features section
610 if (is_json) {
611 OutputFeaturesJson(formatter, report.features);
612 OutputMapStatusJson(formatter, report.map_status);
613 OutputFindingsJson(formatter, report);
614 OutputSummaryJson(formatter, report);
615 }
616
617 // Text mode: show nice ASCII summary
618 if (!is_json) {
619 OutputTextSummary(report);
620 if (verbose) {
621 OutputTextFindings(report);
622 }
623 }
624
625 // Entity coverage (text mode only for now)
626 if (!is_json) {
627 ASSIGN_OR_RETURN(auto exits, zelda3::LoadExits(rom));
628 ASSIGN_OR_RETURN(auto entrances, zelda3::LoadEntrances(rom));
629 ASSIGN_OR_RETURN(auto maps, BuildOverworldMaps(rom));
630 ASSIGN_OR_RETURN(auto items, zelda3::LoadItems(rom, maps));
631
632 auto exit_stats =
633 BuildDistribution(exits, [](const auto& exit) { return exit.map_id_; });
634 auto entrance_stats = BuildDistribution(
635 entrances,
636 [](const auto& ent) { return static_cast<uint16_t>(ent.map_id_); });
637 auto item_stats =
638 BuildDistribution(items, [](const auto& item) { return item.map_id_; });
639
640 std::cout << "\n=== Overworld Entity Coverage ===\n";
641 std::cout << absl::StrFormat(
642 " exits : total=%d unique=%d most_common=0x%02X (%d)\n",
643 exit_stats.total, exit_stats.unique, exit_stats.most_common_map,
644 exit_stats.most_common_count);
645 std::cout << absl::StrFormat(
646 " entrances : total=%d unique=%d most_common=0x%02X (%d)\n",
647 entrance_stats.total, entrance_stats.unique,
648 entrance_stats.most_common_map, entrance_stats.most_common_count);
649 std::cout << absl::StrFormat(
650 " items : total=%d unique=%d most_common=0x%02X (%d)\n",
651 item_stats.total, item_stats.unique, item_stats.most_common_map,
652 item_stats.most_common_count);
653
654 if (baseline_rom) {
655 std::cout << absl::StrFormat(" Baseline used: %s\n", resolved_baseline);
656 }
657 }
658
659 // Apply tail expansion if requested
660 if (apply_tail_expansion) {
661 if (dry_run) {
662 if (!is_json) {
663 std::cout << "\n=== Dry Run - Tail Map Expansion ===\n";
665 std::cout << " Tail expansion already applied.\n";
666 } else {
667 std::cout << " Would apply TailMapExpansion.asm patch.\n";
668 std::cout << " This will:\n";
669 std::cout << " - Relocate pointer tables to $28:A400\n";
670 std::cout << " - Expand from 160 to 192 map entries\n";
671 std::cout << " - Write marker byte 0xEA at $28:A3FF\n";
672 std::cout << " - Add blank map data at $30:8000\n";
673 }
674 std::cout << "\nNo changes made (dry run).\n";
675 }
676 formatter.AddField("dry_run_tail_expansion", true);
677 } else {
678 auto status = ApplyTailExpansion(rom, false, verbose);
679 if (status.ok()) {
680 if (!is_json) {
681 std::cout << "\n=== Tail Map Expansion Applied ===\n";
682 std::cout << " Pointer tables relocated to $28:A400/$28:A640\n";
683 std::cout << " Maps 0xA0-0xBF now available for editing\n";
684 }
685 formatter.AddField("tail_expansion_applied", true);
686
687 // Re-detect features after patch
688 report.features = DetectRomFeatures(rom);
689 } else if (absl::IsAlreadyExists(status)) {
690 if (!is_json) {
691 std::cout << "\n[INFO] Tail expansion already applied.\n";
692 }
693 formatter.AddField("tail_expansion_already_applied", true);
694 } else {
695 if (!is_json) {
696 std::cout << "\n[ERROR] Failed to apply tail expansion: "
697 << status.message() << "\n";
698 }
699 formatter.AddField("tail_expansion_error", std::string(status.message()));
700 // Continue with diagnostics, don't fail the whole command
701 }
702 }
703 }
704
705 // Fix mode handling
706 if (fix_mode) {
707 if (dry_run) {
708 if (!is_json) {
709 std::cout << "\n=== Dry Run - Planned Fixes ===\n";
711 std::cout << absl::StrFormat(
712 " Would zero %zu corrupted tile16 entries\n",
713 report.tile16_status.corrupted_addresses.size());
714 for (uint32_t addr : report.tile16_status.corrupted_addresses) {
715 std::cout << absl::StrFormat(" - 0x%06X\n", addr);
716 }
717 } else {
718 std::cout << " No fixes needed.\n";
719 }
720 std::cout << "\nNo changes made (dry run).\n";
721 }
722 formatter.AddField("dry_run_fixes_planned",
723 static_cast<int>(
724 report.tile16_status.corrupted_addresses.size()));
725 } else {
726 // Actually apply fixes
728 RETURN_IF_ERROR(RepairTile16Region(rom, report, false));
729 if (!is_json) {
730 std::cout << "\n=== Fixes Applied ===\n";
731 std::cout << absl::StrFormat(" Zeroed %zu corrupted tile16 entries\n",
732 report.tile16_status.corrupted_addresses.size());
733 }
734 formatter.AddField("fixes_applied", true);
735 formatter.AddField(
736 "tile16_entries_fixed",
737 static_cast<int>(report.tile16_status.corrupted_addresses.size()));
738 }
739
740 // Save if output path provided
741 if (output_path.has_value()) {
742 Rom::SaveSettings settings;
743 settings.filename = output_path.value();
744 RETURN_IF_ERROR(rom->SaveToFile(settings));
745 if (!is_json) {
746 std::cout << absl::StrFormat("\nSaved fixed ROM to: %s\n",
747 output_path.value());
748 }
749 formatter.AddField("output_file", output_path.value());
750 } else if (report.HasFixable()) {
751 if (!is_json) {
752 std::cout << "\nNo output path specified. Use --output <path> to save.\n";
753 }
754 }
755 }
756 } else {
757 // Not in fix mode - show hint
758 if (!is_json && report.HasFixable()) {
759 std::cout << "\nTo apply available fixes, run with --fix flag.\n";
760 std::cout << "To preview fixes, use --fix --dry-run.\n";
761 std::cout << "To save to a new file, use --output <path>.\n";
762 }
763 }
764
765 return absl::OkStatus();
766}
767
768} // 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:164
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