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);
166 finding.
id =
"lw_dw_corruption";
168 finding.
message =
"Light/Dark World map pointers are corrupted";
172 "ROM may be severely damaged. Restore from backup.";
179 finding.
id =
"sw_corruption";
181 finding.
message =
"Special World map pointers are corrupted";
204 int tile_index = tile_offset / 8;
206 uint16_t tile_data[4];
207 for (
int i = 0; i < 4 && (addr + i * 2 + 1) < rom->
size(); ++i) {
209 rom->
data()[addr + i * 2] | (rom->
data()[addr + i * 2 + 1] << 8);
212 bool looks_valid =
true;
213 for (
int i = 0; i < 4; ++i) {
226 finding.
id =
"tile16_corruption";
229 absl::StrFormat(
"Corrupted tile16 #%d", tile_index);
230 finding.
location = absl::StrFormat(
"0x%06X", addr);
244 std::string* resolved_path) {
245 std::vector<std::string> candidates;
246 if (path.has_value()) {
247 candidates.push_back(*path);
249 candidates = {
"alttp_vanilla.sfc",
"vanilla.sfc",
"zelda3.sfc"};
252 for (
const auto& candidate : candidates) {
253 std::ifstream probe(candidate, std::ios::binary);
254 if (!probe.good())
continue;
257 auto baseline = std::make_unique<Rom>();
258 auto status = baseline->LoadFromFile(candidate);
260 if (resolved_path) *resolved_path = candidate;
272template <
typename T,
typename Getter>
276 for (
const auto& entry : entries) {
277 uint16_t map = getter(entry);
286 for (
const auto& [map, count] : stats.
counts) {
296 std::vector<zelda3::OverworldMap> maps;
299 maps.emplace_back(i, rom);
311 return absl::OkStatus();
316 for (
int i = 0; i < 8 && addr + i < rom->
size(); ++i) {
317 (*rom)[addr + i] = 0x00;
322 return absl::OkStatus();
328 if (kExpandedPtrTableMarker < rom->size() &&
330 return absl::AlreadyExistsError(
331 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
335 if (kZSCustomVersionPos < rom->size()) {
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.");
345 return absl::OkStatus();
349 std::vector<std::string> patch_locations = {
350 "assets/patches/Overworld/TailMapExpansion.asm",
351 "../assets/patches/Overworld/TailMapExpansion.asm",
352 "TailMapExpansion.asm"
355 std::string patch_path;
356 for (
const auto& loc : patch_locations) {
357 std::ifstream probe(loc);
364 if (patch_path.empty()) {
365 return absl::NotFoundError(
366 "TailMapExpansion.asm patch file not found. "
367 "Expected locations: assets/patches/Overworld/TailMapExpansion.asm");
374 std::vector<uint8_t> rom_data(rom->
data(), rom->
data() + rom->
size());
375 auto result = asar.
ApplyPatch(patch_path, rom_data);
378 return result.status();
381 if (!result->success) {
382 std::string error_msg =
"Asar patch failed:";
383 for (
const auto& err : result->errors) {
384 error_msg +=
" " + err;
386 return absl::InternalError(error_msg);
390 if (rom_data.size() > rom->
size()) {
392 std::cout << absl::StrFormat(
" Expanding ROM from %zu to %zu bytes\n",
393 rom->
size(), rom_data.size());
395 rom->
Expand(
static_cast<int>(rom_data.size()));
396 }
else if (rom_data.size() < rom->
size()) {
398 return absl::InternalError(
399 absl::StrFormat(
"ROM size decreased unexpectedly: %zu -> %zu",
400 rom->
size(), rom_data.size()));
404 for (
size_t i = 0; i < rom_data.size(); ++i) {
405 (*rom)[i] = rom_data[i];
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.",
416 return absl::InternalError(
417 "Patch applied but marker not found. Patch may be incomplete.");
420 return absl::OkStatus();
433 formatter.
AddField(
"expanded_pointer_tables",
438 formatter.
AddField(
"custom_main_palette_enabled",
441 formatter.
AddField(
"custom_animated_gfx_enabled",
444 formatter.
AddField(
"custom_tile_gfx_enabled",
460 for (
const auto& finding : report.
findings) {
477void OutputTextBanner(
bool is_json) {
480 std::cout <<
"╔═══════════════════════════════════════════════════════════════╗\n";
481 std::cout <<
"║ OVERWORLD DOCTOR ║\n";
482 std::cout <<
"║ ROM Diagnostic & Repair Tool ║\n";
483 std::cout <<
"╚═══════════════════════════════════════════════════════════════╝\n";
488 std::cout <<
"╔═══════════════════════════════════════════════════════════════╗\n";
489 std::cout <<
"║ DIAGNOSTIC SUMMARY ║\n";
490 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
492 std::cout << absl::StrFormat(
493 "║ ROM Version: %-46s ║\n",
496 std::cout << absl::StrFormat(
497 "║ Expanded Tile16: %-42s ║\n",
499 std::cout << absl::StrFormat(
500 "║ Expanded Tile32: %-42s ║\n",
502 std::cout << absl::StrFormat(
503 "║ Expanded Ptr Tables: %-38s ║\n",
507 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
509 std::cout << absl::StrFormat(
510 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
512 std::cout << absl::StrFormat(
513 "║ Special World (0x80-0x9F): %-32s ║\n",
515 std::cout << absl::StrFormat(
516 "║ Tail Maps (0xA0-0xBF): %-36s ║\n",
518 :
"N/A (no ASM expansion)");
521 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
523 std::cout << absl::StrFormat(
524 "║ Tile16 Corruption: DETECTED (%zu addresses)%-17s ║\n",
528 std::cout << absl::StrFormat(
"║ - 0x%06X (tile #%d)%-36s ║\n",
532 std::cout <<
"║ Tile16 Corruption: None detected ║\n";
536 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
537 std::cout << absl::StrFormat(
539 std::cout << absl::StrFormat(
540 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
543 std::cout << absl::StrFormat(
545 std::cout <<
"╚═══════════════════════════════════════════════════════════════╝\n";
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";
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();
576 OutputTextBanner(is_json);
581 report.
features = DetectRomFeatures(rom);
582 ValidateMapPointers(rom, report);
583 CheckTile16Corruption(rom, report);
586 std::string resolved_baseline;
587 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
592 finding.
id =
"no_tail_support";
594 finding.
message =
"Tail maps (0xA0-0xBF) not available";
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.";
606 formatter.
AddField(
"fix_mode", fix_mode);
607 formatter.
AddField(
"dry_run", dry_run);
611 OutputFeaturesJson(formatter, report.
features);
612 OutputMapStatusJson(formatter, report.
map_status);
613 OutputFindingsJson(formatter, report);
614 OutputSummaryJson(formatter, report);
619 OutputTextSummary(report);
621 OutputTextFindings(report);
633 BuildDistribution(exits, [](
const auto& exit) {
return exit.map_id_; });
634 auto entrance_stats = BuildDistribution(
636 [](
const auto& ent) {
return static_cast<uint16_t
>(ent.map_id_); });
638 BuildDistribution(items, [](
const auto& item) {
return item.map_id_; });
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);
655 std::cout << absl::StrFormat(
" Baseline used: %s\n", resolved_baseline);
660 if (apply_tail_expansion) {
663 std::cout <<
"\n=== Dry Run - Tail Map Expansion ===\n";
665 std::cout <<
" Tail expansion already applied.\n";
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";
674 std::cout <<
"\nNo changes made (dry run).\n";
676 formatter.
AddField(
"dry_run_tail_expansion",
true);
678 auto status = ApplyTailExpansion(rom,
false, verbose);
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";
685 formatter.
AddField(
"tail_expansion_applied",
true);
688 report.
features = DetectRomFeatures(rom);
689 }
else if (absl::IsAlreadyExists(status)) {
691 std::cout <<
"\n[INFO] Tail expansion already applied.\n";
693 formatter.
AddField(
"tail_expansion_already_applied",
true);
696 std::cout <<
"\n[ERROR] Failed to apply tail expansion: "
697 << status.message() <<
"\n";
699 formatter.
AddField(
"tail_expansion_error", std::string(status.message()));
709 std::cout <<
"\n=== Dry Run - Planned Fixes ===\n";
711 std::cout << absl::StrFormat(
712 " Would zero %zu corrupted tile16 entries\n",
715 std::cout << absl::StrFormat(
" - 0x%06X\n", addr);
718 std::cout <<
" No fixes needed.\n";
720 std::cout <<
"\nNo changes made (dry run).\n";
722 formatter.
AddField(
"dry_run_fixes_planned",
730 std::cout <<
"\n=== Fixes Applied ===\n";
731 std::cout << absl::StrFormat(
" Zeroed %zu corrupted tile16 entries\n",
734 formatter.
AddField(
"fixes_applied",
true);
736 "tile16_entries_fixed",
741 if (output_path.has_value()) {
743 settings.
filename = output_path.value();
746 std::cout << absl::StrFormat(
"\nSaved fixed ROM to: %s\n",
747 output_path.value());
749 formatter.
AddField(
"output_file", output_path.value());
752 std::cout <<
"\nNo output path specified. Use --output <path> to save.\n";
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";
765 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