10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
30 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
38 return (tile_info & 0x1F00) == 0 || (tile_info & 0xE000) != 0;
49 if (kZSCustomVersionPos < rom->size()) {
61 if (kMap16ExpandedFlagPos < rom->size()) {
66 if (kMap32ExpandedFlagPos < rom->size()) {
73 if (kExpandedPtrTableMarker < rom->size()) {
80 if (kCustomBGEnabledPos < rom->size()) {
83 if (kCustomMainPalettePos < rom->size()) {
87 if (kCustomMosaicPos < rom->size()) {
90 if (kCustomAnimatedGFXPos < rom->size()) {
94 if (kCustomOverlayPos < rom->size()) {
97 if (kCustomTileGFXPos < rom->size()) {
117 if (ptr_low_addr + 3 > rom->
size() || ptr_high_addr + 3 > rom->
size()) {
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);
134 uint32_t pc_low =
SnesToPc(snes_low);
135 uint32_t pc_high =
SnesToPc(snes_high);
137 bool low_valid = (pc_low > 0 && pc_low < rom->
size());
138 bool high_valid = (pc_high > 0 && pc_high < rom->
size());
140 if (!low_valid || !high_valid) {
149 finding.
id =
"invalid_map_pointer";
152 absl::StrFormat(
"Map 0x%02X has invalid pointer", map_id);
153 finding.
location = absl::StrFormat(
"0x%06X", ptr_low_addr);
169 finding.
id =
"lw_dw_corruption";
171 finding.
message =
"Light/Dark World map pointers are corrupted";
175 "ROM may be severely damaged. Restore from backup.";
182 finding.
id =
"sw_corruption";
184 finding.
message =
"Special World map pointers are corrupted";
207 int tile_index = tile_offset / 8;
209 uint16_t tile_data[4];
210 for (
int i = 0; i < 4 && (addr + i * 2 + 1) < rom->
size(); ++i) {
212 rom->
data()[addr + i * 2] | (rom->
data()[addr + i * 2 + 1] << 8);
215 bool looks_valid =
true;
216 for (
int i = 0; i < 4; ++i) {
229 finding.
id =
"tile16_corruption";
231 finding.
message = absl::StrFormat(
"Corrupted tile16 #%d", tile_index);
232 finding.
location = absl::StrFormat(
"0x%06X", addr);
246 std::string* resolved_path) {
247 std::vector<std::string> candidates;
248 if (path.has_value()) {
249 candidates.push_back(*path);
251 candidates = {
"alttp_vanilla.sfc",
"vanilla.sfc",
"zelda3.sfc"};
254 for (
const auto& candidate : candidates) {
255 std::ifstream probe(candidate, std::ios::binary);
260 auto baseline = std::make_unique<Rom>();
261 auto status = baseline->LoadFromFile(candidate);
264 *resolved_path = candidate;
276template <
typename T,
typename Getter>
280 for (
const auto& entry : entries) {
281 uint16_t map = getter(entry);
290 for (
const auto& [map, count] : stats.
counts) {
300 std::vector<zelda3::OverworldMap> maps;
303 maps.emplace_back(i, rom);
315 return absl::OkStatus();
320 for (
int i = 0; i < 8 && addr + i < rom->
size(); ++i) {
321 (*rom)[addr + i] = 0x00;
326 return absl::OkStatus();
332 if (kExpandedPtrTableMarker < rom->size() &&
334 return absl::AlreadyExistsError(
335 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
339 if (kZSCustomVersionPos < rom->size()) {
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.");
349 return absl::OkStatus();
354 if (!patch_path.ok()) {
355 return absl::NotFoundError(
356 "TailMapExpansion.asm patch file not found. "
357 "Expected it in the Yaze runtime assets.");
364 std::vector<uint8_t> rom_data(rom->
data(), rom->
data() + rom->
size());
365 auto result = asar.
ApplyPatch(patch_path->string(), rom_data);
368 return result.status();
371 if (!result->success) {
372 std::string error_msg =
"Asar patch failed:";
373 for (
const auto& err : result->errors) {
374 error_msg +=
" " + err;
376 return absl::InternalError(error_msg);
380 if (rom_data.size() > rom->
size()) {
382 std::cout << absl::StrFormat(
" Expanding ROM from %zu to %zu bytes\n",
383 rom->
size(), rom_data.size());
385 rom->
Expand(
static_cast<int>(rom_data.size()));
386 }
else if (rom_data.size() < rom->
size()) {
388 return absl::InternalError(
389 absl::StrFormat(
"ROM size decreased unexpectedly: %zu -> %zu",
390 rom->
size(), rom_data.size()));
394 for (
size_t i = 0; i < rom_data.size(); ++i) {
395 (*rom)[i] = rom_data[i];
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.",
406 return absl::InternalError(
407 "Patch applied but marker not found. Patch may be incomplete.");
410 return absl::OkStatus();
423 formatter.
AddField(
"expanded_pointer_tables",
428 formatter.
AddField(
"custom_main_palette_enabled",
431 formatter.
AddField(
"custom_animated_gfx_enabled",
433 formatter.
AddField(
"custom_overlay_enabled",
435 formatter.
AddField(
"custom_tile_gfx_enabled",
451 for (
const auto& finding : report.
findings) {
468void OutputTextBanner(
bool is_json) {
473 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
475 <<
"║ OVERWORLD DOCTOR ║\n";
477 <<
"║ ROM Diagnostic & Repair Tool ║\n";
479 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
485 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
487 <<
"║ DIAGNOSTIC SUMMARY ║\n";
489 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
491 std::cout << absl::StrFormat(
"║ ROM Version: %-46s ║\n",
494 std::cout << absl::StrFormat(
495 "║ Expanded Tile16: %-42s ║\n",
497 std::cout << absl::StrFormat(
498 "║ Expanded Tile32: %-42s ║\n",
500 std::cout << absl::StrFormat(
"║ Expanded Ptr Tables: %-38s ║\n",
506 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
508 std::cout << absl::StrFormat(
509 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
511 std::cout << absl::StrFormat(
512 "║ Special World (0x80-0x9F): %-32s ║\n",
514 std::cout << absl::StrFormat(
"║ Tail Maps (0xA0-0xBF): %-36s ║\n",
517 :
"N/A (no ASM expansion)");
520 std::cout <<
"╠════════════════════════════════════════════════════════════"
523 std::cout << absl::StrFormat(
524 "║ Tile16 Corruption: DETECTED (%zu addresses)%-17s ║\n",
528 std::cout << absl::StrFormat(
"║ - 0x%06X (tile #%d)%-36s ║\n", addr,
532 std::cout <<
"║ Tile16 Corruption: None detected "
538 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
539 std::cout << absl::StrFormat(
"║ Total Findings: %-43d ║\n",
541 std::cout << absl::StrFormat(
542 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
545 std::cout << absl::StrFormat(
"║ Fixable Issues: %-43d ║\n",
548 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
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";
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();
579 OutputTextBanner(is_json);
584 report.
features = DetectRomFeatures(rom);
585 ValidateMapPointers(rom, report);
586 CheckTile16Corruption(rom, report);
589 std::string resolved_baseline;
590 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
595 finding.
id =
"no_tail_support";
597 finding.
message =
"Tail maps (0xA0-0xBF) not available";
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.";
609 formatter.
AddField(
"fix_mode", fix_mode);
610 formatter.
AddField(
"dry_run", dry_run);
614 OutputFeaturesJson(formatter, report.
features);
615 OutputMapStatusJson(formatter, report.
map_status);
616 OutputFindingsJson(formatter, report);
617 OutputSummaryJson(formatter, report);
622 OutputTextSummary(report);
624 OutputTextFindings(report);
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_);
641 BuildDistribution(items, [](
const auto& item) {
return item.map_id_; });
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);
658 std::cout << absl::StrFormat(
" Baseline used: %s\n", resolved_baseline);
663 if (apply_tail_expansion) {
666 std::cout <<
"\n=== Dry Run - Tail Map Expansion ===\n";
668 std::cout <<
" Tail expansion already applied.\n";
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";
677 std::cout <<
"\nNo changes made (dry run).\n";
679 formatter.
AddField(
"dry_run_tail_expansion",
true);
681 auto status = ApplyTailExpansion(rom,
false, verbose);
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";
688 formatter.
AddField(
"tail_expansion_applied",
true);
691 report.
features = DetectRomFeatures(rom);
692 }
else if (absl::IsAlreadyExists(status)) {
694 std::cout <<
"\n[INFO] Tail expansion already applied.\n";
696 formatter.
AddField(
"tail_expansion_already_applied",
true);
699 std::cout <<
"\n[ERROR] Failed to apply tail expansion: "
700 << status.message() <<
"\n";
702 formatter.
AddField(
"tail_expansion_error",
703 std::string(status.message()));
713 std::cout <<
"\n=== Dry Run - Planned Fixes ===\n";
715 std::cout << absl::StrFormat(
716 " Would zero %zu corrupted tile16 entries\n",
719 std::cout << absl::StrFormat(
" - 0x%06X\n", addr);
722 std::cout <<
" No fixes needed.\n";
724 std::cout <<
"\nNo changes made (dry run).\n";
727 "dry_run_fixes_planned",
734 std::cout <<
"\n=== Fixes Applied ===\n";
735 std::cout << absl::StrFormat(
736 " Zeroed %zu corrupted tile16 entries\n",
739 formatter.
AddField(
"fixes_applied",
true);
741 "tile16_entries_fixed",
746 if (output_path.has_value()) {
748 settings.
filename = output_path.value();
751 std::cout << absl::StrFormat(
"\nSaved fixed ROM to: %s\n",
752 output_path.value());
754 formatter.
AddField(
"output_file", output_path.value());
758 <<
"\nNo output path specified. Use --output <path> to save.\n";
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";
771 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