10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
31 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
42 if (kZSCustomVersionPos < rom->size()) {
54 if (kMap16ExpandedFlagPos < rom->size()) {
59 if (kMap32ExpandedFlagPos < rom->size()) {
66 if (kExpandedPtrTableMarker < rom->size()) {
73 if (kCustomBGEnabledPos < rom->size()) {
76 if (kCustomMainPalettePos < rom->size()) {
80 if (kCustomMosaicPos < rom->size()) {
83 if (kCustomAnimatedGFXPos < rom->size()) {
87 if (kCustomOverlayPos < rom->size()) {
90 if (kCustomTileGFXPos < rom->size()) {
110 if (ptr_low_addr + 3 > rom->
size() || ptr_high_addr + 3 > rom->
size()) {
120 uint32_t snes_low = rom->
data()[ptr_low_addr] |
121 (rom->
data()[ptr_low_addr + 1] << 8) |
122 (rom->
data()[ptr_low_addr + 2] << 16);
123 uint32_t snes_high = rom->
data()[ptr_high_addr] |
124 (rom->
data()[ptr_high_addr + 1] << 8) |
125 (rom->
data()[ptr_high_addr + 2] << 16);
127 uint32_t pc_low =
SnesToPc(snes_low);
128 uint32_t pc_high =
SnesToPc(snes_high);
130 bool low_valid = (pc_low > 0 && pc_low < rom->
size());
131 bool high_valid = (pc_high > 0 && pc_high < rom->
size());
133 if (!low_valid || !high_valid) {
142 finding.
id =
"invalid_map_pointer";
145 absl::StrFormat(
"Map 0x%02X has invalid pointer", map_id);
146 finding.
location = absl::StrFormat(
"0x%06X", ptr_low_addr);
162 finding.
id =
"lw_dw_corruption";
164 finding.
message =
"Light/Dark World map pointers are corrupted";
168 "ROM may be severely damaged. Restore from backup.";
175 finding.
id =
"sw_corruption";
177 finding.
message =
"Special World map pointers are corrupted";
201 std::string* resolved_path) {
202 std::vector<std::string> candidates;
203 if (path.has_value()) {
204 candidates.push_back(*path);
206 candidates = {
"alttp_vanilla.sfc",
"vanilla.sfc",
"zelda3.sfc"};
209 for (
const auto& candidate : candidates) {
210 std::ifstream probe(candidate, std::ios::binary);
215 auto baseline = std::make_unique<Rom>();
216 auto status = baseline->LoadFromFile(candidate);
219 *resolved_path = candidate;
231template <
typename T,
typename Getter>
235 for (
const auto& entry : entries) {
236 uint16_t map = getter(entry);
245 for (
const auto& [map, count] : stats.
counts) {
255 std::vector<zelda3::OverworldMap> maps;
258 maps.emplace_back(i, rom);
270 if (kExpandedPtrTableMarker < rom->size() &&
272 return absl::AlreadyExistsError(
273 "Tail map expansion already applied (marker 0xEA found at 0x1423FF)");
277 if (kZSCustomVersionPos < rom->size()) {
279 if (version < 3 && version != 0xFF && version != 0x00) {
280 return absl::FailedPreconditionError(
281 "Tail map expansion requires ZSCustomOverworld v3 or later. "
282 "Apply ZSCustomOverworld v3 first.");
293 return absl::OkStatus();
298 if (!patch_path.ok()) {
299 return absl::NotFoundError(
300 "TailMapExpansion.asm patch file not found. "
301 "Expected it in the Yaze runtime assets.");
308 std::vector<uint8_t> rom_data(rom->
data(), rom->
data() + rom->
size());
309 auto result = asar.
ApplyPatch(patch_path->string(), rom_data);
312 return result.status();
315 if (!result->success) {
316 std::string error_msg =
"Asar patch failed:";
317 for (
const auto& err : result->errors) {
318 error_msg +=
" " + err;
320 return absl::InternalError(error_msg);
324 if (rom_data.size() > rom->
size()) {
326 std::cout << absl::StrFormat(
" Expanding ROM from %zu to %zu bytes\n",
327 rom->
size(), rom_data.size());
329 rom->
Expand(
static_cast<int>(rom_data.size()));
330 }
else if (rom_data.size() < rom->
size()) {
332 return absl::InternalError(
333 absl::StrFormat(
"ROM size decreased unexpectedly: %zu -> %zu",
334 rom->
size(), rom_data.size()));
338 for (
size_t i = 0; i < rom_data.size(); ++i) {
339 (*rom)[i] = rom_data[i];
344 return absl::InternalError(
345 absl::StrFormat(
"ROM too small for expansion marker at 0x%06X "
346 "(ROM size: 0x%06zX). Patch may have failed.",
350 return absl::InternalError(
351 "Patch applied but marker not found. Patch may be incomplete.");
354 return absl::OkStatus();
367 formatter.
AddField(
"expanded_pointer_tables",
372 formatter.
AddField(
"custom_main_palette_enabled",
375 formatter.
AddField(
"custom_animated_gfx_enabled",
377 formatter.
AddField(
"custom_overlay_enabled",
379 formatter.
AddField(
"custom_tile_gfx_enabled",
395 for (
const auto& finding : report.
findings) {
412void OutputTextBanner(
bool is_json) {
417 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
419 <<
"║ OVERWORLD DOCTOR ║\n";
421 <<
"║ ROM Diagnostic & Repair Tool ║\n";
423 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
429 <<
"╔═══════════════════════════════════════════════════════════════╗\n";
431 <<
"║ DIAGNOSTIC SUMMARY ║\n";
433 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
435 std::cout << absl::StrFormat(
"║ ROM Version: %-46s ║\n",
438 std::cout << absl::StrFormat(
439 "║ Expanded Tile16: %-42s ║\n",
441 std::cout << absl::StrFormat(
442 "║ Expanded Tile32: %-42s ║\n",
444 std::cout << absl::StrFormat(
"║ Expanded Ptr Tables: %-38s ║\n",
450 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
452 std::cout << absl::StrFormat(
453 "║ Light/Dark World (0x00-0x7F): %-29s ║\n",
455 std::cout << absl::StrFormat(
456 "║ Special World (0x80-0x9F): %-32s ║\n",
458 std::cout << absl::StrFormat(
"║ Tail Maps (0xA0-0xBF): %-36s ║\n",
461 :
"N/A (no ASM expansion)");
464 std::cout <<
"╠════════════════════════════════════════════════════════════"
466 std::cout <<
"║ Tile16 Data: not checked (compare with a backup) "
471 <<
"╠═══════════════════════════════════════════════════════════════╣\n";
472 std::cout << absl::StrFormat(
"║ Total Findings: %-43d ║\n",
474 std::cout << absl::StrFormat(
475 "║ Critical: %-3d Errors: %-3d Warnings: %-3d Info: %-3d%-4s ║\n",
478 std::cout << absl::StrFormat(
"║ Fixable Issues: %-43d ║\n",
481 <<
"╚═══════════════════════════════════════════════════════════════╝\n";
489 std::cout <<
"\n=== Detailed Findings ===\n";
490 for (
const auto& finding : report.
findings) {
491 std::cout <<
" " << finding.FormatText() <<
"\n";
492 if (!finding.suggested_action.empty()) {
493 std::cout <<
" → " << finding.suggested_action <<
"\n";
503 bool fix_mode = parser.
HasFlag(
"fix");
504 bool apply_tail_expansion = parser.
HasFlag(
"apply-tail-expansion");
505 bool dry_run = parser.
HasFlag(
"dry-run");
506 bool verbose = parser.
HasFlag(
"verbose");
507 auto output_path = parser.
GetString(
"output");
508 auto baseline_path = parser.
GetString(
"baseline");
509 bool is_json = formatter.
IsJson();
512 OutputTextBanner(is_json);
517 report.
features = DetectRomFeatures(rom);
518 ValidateMapPointers(rom, report);
519 CheckTile16Region(report);
522 std::string resolved_baseline;
523 auto baseline_rom = LoadBaselineRom(baseline_path, &resolved_baseline);
528 finding.
id =
"no_tail_support";
530 finding.
message =
"Tail maps (0xA0-0xBF) not available";
533 "Apply TailMapExpansion.asm patch (after ZSCustomOverworld v3) to "
534 "expand pointer tables to 192 entries. Use: z3ed overworld-doctor "
535 "--apply-tail-expansion or apply manually with Asar.";
542 formatter.
AddField(
"fix_mode", fix_mode);
543 formatter.
AddField(
"dry_run", dry_run);
547 OutputFeaturesJson(formatter, report.
features);
548 OutputMapStatusJson(formatter, report.
map_status);
549 OutputFindingsJson(formatter, report);
550 OutputSummaryJson(formatter, report);
555 OutputTextSummary(report);
557 OutputTextFindings(report);
569 BuildDistribution(exits, [](
const auto& exit) {
return exit.map_id_; });
570 auto entrance_stats = BuildDistribution(entrances, [](
const auto& ent) {
571 return static_cast<uint16_t
>(ent.map_id_);
574 BuildDistribution(items, [](
const auto& item) {
return item.map_id_; });
576 std::cout <<
"\n=== Overworld Entity Coverage ===\n";
577 std::cout << absl::StrFormat(
578 " exits : total=%d unique=%d most_common=0x%02X (%d)\n",
579 exit_stats.total, exit_stats.unique, exit_stats.most_common_map,
580 exit_stats.most_common_count);
581 std::cout << absl::StrFormat(
582 " entrances : total=%d unique=%d most_common=0x%02X (%d)\n",
583 entrance_stats.total, entrance_stats.unique,
584 entrance_stats.most_common_map, entrance_stats.most_common_count);
585 std::cout << absl::StrFormat(
586 " items : total=%d unique=%d most_common=0x%02X (%d)\n",
587 item_stats.total, item_stats.unique, item_stats.most_common_map,
588 item_stats.most_common_count);
591 std::cout << absl::StrFormat(
" Baseline used: %s\n", resolved_baseline);
596 if (apply_tail_expansion) {
599 std::cout <<
"\n=== Dry Run - Tail Map Expansion ===\n";
601 std::cout <<
" Tail expansion already applied.\n";
603 std::cout <<
" Would apply TailMapExpansion.asm patch.\n";
604 std::cout <<
" This will:\n";
605 std::cout <<
" - Relocate pointer tables to $28:A400\n";
606 std::cout <<
" - Expand from 160 to 192 map entries\n";
607 std::cout <<
" - Write marker byte 0xEA at $28:A3FF\n";
608 std::cout <<
" - Add blank map data at $30:8000\n";
611 std::cout <<
" A real run would refuse:\n"
612 << guard.message() <<
"\n";
615 std::cout <<
"\nNo changes made (dry run).\n";
617 formatter.
AddField(
"dry_run_tail_expansion",
true);
619 auto status = ApplyTailExpansion(rom,
false, verbose);
622 std::cout <<
"\n=== Tail Map Expansion Applied ===\n";
623 std::cout <<
" Pointer tables relocated to $28:A400/$28:A640\n";
624 std::cout <<
" Maps 0xA0-0xBF now available for editing\n";
626 formatter.
AddField(
"tail_expansion_applied",
true);
629 report.
features = DetectRomFeatures(rom);
630 }
else if (absl::IsAlreadyExists(status)) {
632 std::cout <<
"\n[INFO] Tail expansion already applied.\n";
634 formatter.
AddField(
"tail_expansion_already_applied",
true);
637 std::cout <<
"\n[ERROR] Failed to apply tail expansion: "
638 << status.message() <<
"\n";
640 formatter.
AddField(
"tail_expansion_error",
641 std::string(status.message()));
642 if (absl::IsFailedPrecondition(status)) {
654 std::cout <<
"\n=== Dry Run - Planned Fixes ===\n";
655 std::cout <<
" No fixes needed.\n";
656 std::cout <<
"\nNo changes made (dry run).\n";
658 formatter.
AddField(
"dry_run_fixes_planned", 0);
661 if (output_path.has_value()) {
663 settings.
filename = output_path.value();
666 std::cout << absl::StrFormat(
"\nSaved fixed ROM to: %s\n",
667 output_path.value());
669 formatter.
AddField(
"output_file", output_path.value());
673 <<
"\nNo output path specified. Use --output <path> to save.\n";
680 std::cout <<
"\nTo apply available fixes, run with --fix flag.\n";
681 std::cout <<
"To preview fixes, use --fix --dry-run.\n";
682 std::cout <<
"To save to a new file, use --output <path>.\n";
686 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 OutputFeaturesJson(resources::OutputFormatter &formatter, const RomFeatures &features)
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 CheckTile16Region(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)
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
constexpr uint32_t kPtrTableLowBase
constexpr uint32_t kCustomMosaicPos
constexpr uint32_t kCustomTileGFXPos
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::Status TailMapExpansionApplyGuard(const Rom &rom)
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
uint32_t SnesToPc(uint32_t addr) noexcept
Read-only map of overworld area IDs $00-$BF: which IDs are used, which per-area tables cover each ID,...
#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