10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
29 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
37 return (tile_info & 0x1F00) == 0 || (tile_info & 0xE000) != 0;
48 if (kZSCustomVersionPos < rom->size()) {
60 if (kMap16ExpandedFlagPos < rom->size()) {
65 if (kMap32ExpandedFlagPos < rom->size()) {
72 if (kExpandedPtrTableMarker < rom->size()) {
79 if (kCustomBGEnabledPos < rom->size()) {
82 if (kCustomMainPalettePos < rom->size()) {
86 if (kCustomMosaicPos < rom->size()) {
89 if (kCustomAnimatedGFXPos < rom->size()) {
93 if (kCustomOverlayPos < rom->size()) {
96 if (kCustomTileGFXPos < rom->size()) {
116 if (ptr_low_addr + 3 > rom->
size() || ptr_high_addr + 3 > rom->
size()) {
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);
133 uint32_t pc_low =
SnesToPc(snes_low);
134 uint32_t pc_high =
SnesToPc(snes_high);
136 bool low_valid = (pc_low > 0 && pc_low < rom->
size());
137 bool high_valid = (pc_high > 0 && pc_high < rom->
size());
139 if (!low_valid || !high_valid) {
148 finding.
id =
"invalid_map_pointer";
151 absl::StrFormat(
"Map 0x%02X has invalid pointer", map_id);
152 finding.
location = absl::StrFormat(
"0x%06X", ptr_low_addr);
168 finding.
id =
"lw_dw_corruption";
170 finding.
message =
"Light/Dark World map pointers are corrupted";
174 "ROM may be severely damaged. Restore from backup.";
181 finding.
id =
"sw_corruption";
183 finding.
message =
"Special World map pointers are corrupted";
206 int tile_index = tile_offset / 8;
208 uint16_t tile_data[4];
209 for (
int i = 0; i < 4 && (addr + i * 2 + 1) < rom->
size(); ++i) {
211 rom->
data()[addr + i * 2] | (rom->
data()[addr + i * 2 + 1] << 8);
214 bool looks_valid =
true;
215 for (
int i = 0; i < 4; ++i) {
228 finding.
id =
"tile16_corruption";
230 finding.
message = absl::StrFormat(
"Corrupted tile16 #%d", tile_index);
231 finding.
location = absl::StrFormat(
"0x%06X", addr);
245 std::string* resolved_path) {
246 std::vector<std::string> candidates;
247 if (path.has_value()) {
248 candidates.push_back(*path);
250 candidates = {
"alttp_vanilla.sfc",
"vanilla.sfc",
"zelda3.sfc"};
253 for (
const auto& candidate : candidates) {
254 std::ifstream probe(candidate, std::ios::binary);
259 auto baseline = std::make_unique<Rom>();
260 auto status = baseline->LoadFromFile(candidate);
263 *resolved_path = candidate;
275template <
typename T,
typename Getter>
279 for (
const auto& entry : entries) {
280 uint16_t map = getter(entry);
289 for (
const auto& [map, count] : stats.
counts) {
299 std::vector<zelda3::OverworldMap> maps;
302 maps.emplace_back(i, rom);
314 return absl::OkStatus();
319 for (
int i = 0; i < 8 && addr + i < rom->
size(); ++i) {
320 (*rom)[addr + i] = 0x00;
325 return absl::OkStatus();
331 if (kExpandedPtrTableMarker < rom->size() &&
333 return absl::AlreadyExistsError(
334 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
338 if (kZSCustomVersionPos < rom->size()) {
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.");
348 return absl::OkStatus();
352 std::vector<std::string> patch_locations = {
353 "assets/patches/Overworld/TailMapExpansion.asm",
354 "../assets/patches/Overworld/TailMapExpansion.asm",
355 "TailMapExpansion.asm"};
357 std::string patch_path;
358 for (
const auto& loc : patch_locations) {
359 std::ifstream probe(loc);
366 if (patch_path.empty()) {
367 return absl::NotFoundError(
368 "TailMapExpansion.asm patch file not found. "
369 "Expected locations: assets/patches/Overworld/TailMapExpansion.asm");
376 std::vector<uint8_t> rom_data(rom->
data(), rom->
data() + rom->
size());
377 auto result = asar.
ApplyPatch(patch_path, rom_data);
380 return result.status();
383 if (!result->success) {
384 std::string error_msg =
"Asar patch failed:";
385 for (
const auto& err : result->errors) {
386 error_msg +=
" " + err;
388 return absl::InternalError(error_msg);
392 if (rom_data.size() > rom->
size()) {
394 std::cout << absl::StrFormat(
" Expanding ROM from %zu to %zu bytes\n",
395 rom->
size(), rom_data.size());
397 rom->
Expand(
static_cast<int>(rom_data.size()));
398 }
else if (rom_data.size() < rom->
size()) {
400 return absl::InternalError(
401 absl::StrFormat(
"ROM size decreased unexpectedly: %zu -> %zu",
402 rom->
size(), rom_data.size()));
406 for (
size_t i = 0; i < rom_data.size(); ++i) {
407 (*rom)[i] = rom_data[i];
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.",
418 return absl::InternalError(
419 "Patch applied but marker not found. Patch may be incomplete.");
422 return absl::OkStatus();
435 formatter.
AddField(
"expanded_pointer_tables",
440 formatter.
AddField(
"custom_main_palette_enabled",
443 formatter.
AddField(
"custom_animated_gfx_enabled",
445 formatter.
AddField(
"custom_overlay_enabled",
447 formatter.
AddField(
"custom_tile_gfx_enabled",
463 for (
const auto& finding : report.
findings) {
480void OutputTextBanner(
bool is_json) {
485 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
487 <<
"║ OVERWORLD DOCTOR ║\n";
489 <<
"║ ROM Diagnostic & Repair Tool ║\n";
491 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
497 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
499 <<
"║ DIAGNOSTIC SUMMARY ║\n";
501 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
503 std::cout << absl::StrFormat(
"║ ROM Version: %-46s ║\n",
506 std::cout << absl::StrFormat(
507 "║ Expanded Tile16: %-42s ║\n",
509 std::cout << absl::StrFormat(
510 "║ Expanded Tile32: %-42s ║\n",
512 std::cout << absl::StrFormat(
"║ Expanded Ptr Tables: %-38s ║\n",
518 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
520 std::cout << absl::StrFormat(
521 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
523 std::cout << absl::StrFormat(
524 "║ Special World (0x80-0x9F): %-32s ║\n",
526 std::cout << absl::StrFormat(
"║ Tail Maps (0xA0-0xBF): %-36s ║\n",
529 :
"N/A (no ASM expansion)");
532 std::cout <<
"╠════════════════════════════════════════════════════════════"
535 std::cout << absl::StrFormat(
536 "║ Tile16 Corruption: DETECTED (%zu addresses)%-17s ║\n",
540 std::cout << absl::StrFormat(
"║ - 0x%06X (tile #%d)%-36s ║\n", addr,
544 std::cout <<
"║ Tile16 Corruption: None detected "
550 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
551 std::cout << absl::StrFormat(
"║ Total Findings: %-43d ║\n",
553 std::cout << absl::StrFormat(
554 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
557 std::cout << absl::StrFormat(
"║ Fixable Issues: %-43d ║\n",
560 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
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";
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();
591 OutputTextBanner(is_json);
596 report.
features = DetectRomFeatures(rom);
597 ValidateMapPointers(rom, report);
598 CheckTile16Corruption(rom, report);
601 std::string resolved_baseline;
602 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
607 finding.
id =
"no_tail_support";
609 finding.
message =
"Tail maps (0xA0-0xBF) not available";
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.";
621 formatter.
AddField(
"fix_mode", fix_mode);
622 formatter.
AddField(
"dry_run", dry_run);
626 OutputFeaturesJson(formatter, report.
features);
627 OutputMapStatusJson(formatter, report.
map_status);
628 OutputFindingsJson(formatter, report);
629 OutputSummaryJson(formatter, report);
634 OutputTextSummary(report);
636 OutputTextFindings(report);
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_);
653 BuildDistribution(items, [](
const auto& item) {
return item.map_id_; });
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);
670 std::cout << absl::StrFormat(
" Baseline used: %s\n", resolved_baseline);
675 if (apply_tail_expansion) {
678 std::cout <<
"\n=== Dry Run - Tail Map Expansion ===\n";
680 std::cout <<
" Tail expansion already applied.\n";
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";
689 std::cout <<
"\nNo changes made (dry run).\n";
691 formatter.
AddField(
"dry_run_tail_expansion",
true);
693 auto status = ApplyTailExpansion(rom,
false, verbose);
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";
700 formatter.
AddField(
"tail_expansion_applied",
true);
703 report.
features = DetectRomFeatures(rom);
704 }
else if (absl::IsAlreadyExists(status)) {
706 std::cout <<
"\n[INFO] Tail expansion already applied.\n";
708 formatter.
AddField(
"tail_expansion_already_applied",
true);
711 std::cout <<
"\n[ERROR] Failed to apply tail expansion: "
712 << status.message() <<
"\n";
714 formatter.
AddField(
"tail_expansion_error",
715 std::string(status.message()));
725 std::cout <<
"\n=== Dry Run - Planned Fixes ===\n";
727 std::cout << absl::StrFormat(
728 " Would zero %zu corrupted tile16 entries\n",
731 std::cout << absl::StrFormat(
" - 0x%06X\n", addr);
734 std::cout <<
" No fixes needed.\n";
736 std::cout <<
"\nNo changes made (dry run).\n";
739 "dry_run_fixes_planned",
746 std::cout <<
"\n=== Fixes Applied ===\n";
747 std::cout << absl::StrFormat(
748 " Zeroed %zu corrupted tile16 entries\n",
751 formatter.
AddField(
"fixes_applied",
true);
753 "tile16_entries_fixed",
758 if (output_path.has_value()) {
760 settings.
filename = output_path.value();
763 std::cout << absl::StrFormat(
"\nSaved fixed ROM to: %s\n",
764 output_path.value());
766 formatter.
AddField(
"output_file", output_path.value());
770 <<
"\nNo output path specified. Use --output <path> to save.\n";
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";
783 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status SaveToFile(const SaveSettings &settings)
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.
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)
void OutputTextFindings(const DiagnosticReport &report)
RomFeatures DetectRomFeatures(Rom *rom)
std::unique_ptr< Rom > LoadBaselineRom(const std::optional< std::string > &path, std::string *resolved_path)
void CheckTile16Corruption(Rom *rom, DiagnosticReport &report)
bool IsTile16Valid(uint16_t tile_info)
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 ValidateMapPointers(Rom *rom, DiagnosticReport &report)
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
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
A single diagnostic finding.
std::string suggested_action
DiagnosticSeverity severity
Complete diagnostic report.
MapPointerStatus map_status
std::vector< DiagnosticFinding > findings
Tile16Status tile16_status
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.
bool custom_mosaic_enabled
bool has_expanded_pointer_tables
bool custom_main_palette_enabled
bool custom_tile_gfx_enabled
std::string GetVersionString() const
Get version as human-readable string.
uint8_t zs_custom_version
bool custom_overlay_enabled
bool custom_animated_gfx_enabled
std::vector< uint32_t > corrupted_addresses