9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
32 const std::string& location,
33 const std::string& suggested_action =
"") {
45 return target_sheet < 0 || static_cast<uint32_t>(target_sheet) == sheet;
52std::vector<std::optional<zelda3::GfxSheetExtent>>
ScanSheets(
54 int& successful,
int& failed,
int& oversized) {
55 std::vector<std::optional<zelda3::GfxSheetExtent>> extents(
kNumGfxSheets);
59 const bool selected =
IsSelected(target_sheet, sheet);
63 if (verbose || reported++ < 10) {
65 absl::StrFormat(
"Sheet 0x%02X cannot be read: %s", sheet,
66 extent.status().message()),
67 absl::StrFormat(
"Sheet 0x%02X", sheet));
72 extents[sheet] = *extent;
78 const size_t expected =
82 if (extent->decoded_size != expected) {
86 absl::StrFormat(
"Sheet 0x%02X (%s) decodes to 0x%zX bytes; the game "
89 extent->decoded_size, expected),
90 absl::StrFormat(
"Sheet 0x%02X at 0x%06X", sheet, extent->pc),
91 "A longer stream overwrites the next sheet in WRAM; re-save the "
92 "sheet with the graphics editor");
102 const std::vector<std::optional<zelda3::GfxSheetExtent>>& extents,
105 for (uint32_t a = 0; a < extents.size(); ++a) {
106 if (!extents[a].has_value()) {
109 for (uint32_t b = a + 1; b < extents.size(); ++b) {
110 if (!extents[b].has_value() ||
114 const auto& ea = *extents[a];
115 const auto& eb = *extents[b];
116 const uint64_t a_end = ea.pc + ea.stored_size;
117 const uint64_t b_end = eb.pc + eb.stored_size;
118 if (!(ea.pc < b_end && eb.pc < a_end)) {
121 if (ea.pc == eb.pc && ea.stored_size == eb.stored_size) {
124 absl::StrFormat(
"Sheets 0x%02X and 0x%02X share the same "
127 absl::StrFormat(
"0x%06X", ea.pc));
132 absl::StrFormat(
"Sheet 0x%02X [0x%06X, 0x%06llX) overlaps sheet "
133 "0x%02X [0x%06X, 0x%06llX)",
134 a, ea.pc,
static_cast<unsigned long long>(a_end), b,
135 eb.pc,
static_cast<unsigned long long>(b_end)),
136 absl::StrFormat(
"0x%06X", std::max(ea.pc, eb.pc)));
148 const auto& data = rom.
vector();
150 auto report_ref = [&](
const std::string& group, uint32_t index,
int slot,
151 uint32_t pc, uint32_t value, uint32_t sheet) {
153 if (reported++ < 20) {
156 absl::StrFormat(
"%s %u slot %d value 0x%02X names sheet %u "
157 "(the pointer tables hold %u)",
159 absl::StrFormat(
"0x%06X", pc));
166 const uint32_t main_pc =
SnesToPc(word);
168 for (
int slot = 0; slot < 8; ++slot) {
169 const uint32_t pc = main_pc + i * 8 + slot;
170 if (pc >= data.size()) {
174 report_ref(
"Main blockset", i, slot, pc, data[pc], data[pc]);
181 for (
int slot = 0; slot < 4; ++slot) {
183 if (pc >= data.size()) {
187 report_ref(
"Room blockset", i, slot, pc, data[pc], data[pc]);
193 for (
int slot = 0; slot < 4; ++slot) {
195 if (pc >= data.size()) {
200 report_ref(
"Spriteset", i, slot, pc, data[pc], sheet);
205 if (invalid_refs > 0) {
207 absl::StrFormat(
"Found %d group slot(s) naming a sheet past "
208 "the pointer tables",
210 "Graphics group tables");
218 const std::vector<std::optional<zelda3::GfxSheetExtent>>& extents,
219 DiagnosticReport& report,
int target_sheet,
bool verbose,
int& empty_sheets,
220 int& suspicious_sheets) {
221 for (uint32_t sheet = 0; sheet < extents.size(); ++sheet) {
222 if (!extents[sheet].has_value() || !
IsSelected(target_sheet, sheet)) {
226 if (!data.ok() || data->empty()) {
229 const bool all_zero = std::all_of(data->begin(), data->end(),
230 [](uint8_t b) { return b == 0x00; });
231 const bool all_ff = std::all_of(data->begin(), data->end(),
232 [](uint8_t b) { return b == 0xFF; });
233 const std::string location =
234 absl::StrFormat(
"Sheet 0x%02X at 0x%06X", sheet, extents[sheet]->pc);
236 if (verbose || empty_sheets < 10) {
238 absl::StrFormat(
"Sheet 0x%02X is all zeros (empty)", sheet),
243 if (verbose || suspicious_sheets < 10) {
246 absl::StrFormat(
"Sheet 0x%02X is all 0xFF (erased/uninitialized)",
248 location,
"Sheet may need to be restored");
260 bool verbose = parser.
HasFlag(
"verbose");
267 return absl::InvalidArgumentError(
"ROM not loaded");
271 auto sheet_arg = parser.
GetInt(
"sheet");
272 bool single_sheet = sheet_arg.ok();
273 int target_sheet = single_sheet ? *sheet_arg : -1;
276 if (target_sheet < 0 || target_sheet >=
static_cast<int>(kNumGfxSheets)) {
277 return absl::InvalidArgumentError(absl::StrFormat(
278 "Sheet ID %d out of range (0-%d)", target_sheet, kNumGfxSheets - 1));
283 int successful_decomp = 0;
284 int failed_decomp = 0;
285 int oversized_sheets = 0;
287 ScanSheets(*rom, report, target_sheet, verbose, successful_decomp,
288 failed_decomp, oversized_sheets);
289 if (single_sheet && extents[target_sheet].has_value()) {
291 static_cast<int>(extents[target_sheet]->decoded_size));
293 static_cast<int>(extents[target_sheet]->stored_size));
295 absl::StrFormat(
"0x%06X", extents[target_sheet]->pc));
299 int aliased_sheets = 0;
300 int overlapping_sheets = 0;
301 CheckSheetOverlaps(extents, report, target_sheet, aliased_sheets,
305 int invalid_group_refs = 0;
307 ValidateGroupTables(*rom, report, invalid_group_refs);
311 int empty_sheets = 0;
312 int suspicious_sheets = 0;
313 CheckSheetContents(*rom, extents, report, target_sheet, verbose, empty_sheets,
317 formatter.
AddField(
"total_sheets",
static_cast<int>(kNumGfxSheets));
318 formatter.
AddField(
"successful_decompressions", successful_decomp);
319 formatter.
AddField(
"failed_decompressions", failed_decomp);
320 formatter.
AddField(
"oversized_sheets", oversized_sheets);
321 formatter.
AddField(
"overlapping_sheets", overlapping_sheets);
322 formatter.
AddField(
"aliased_sheets", aliased_sheets);
323 formatter.
AddField(
"invalid_group_refs", invalid_group_refs);
324 formatter.
AddField(
"empty_sheets", empty_sheets);
325 formatter.
AddField(
"suspicious_sheets", suspicious_sheets);
335 for (
const auto& finding : report.
findings) {
342 if (!formatter.
IsJson()) {
344 std::cout <<
"╔════════════════════════════════════════════════════════════"
346 std::cout <<
"║ GRAPHICS DOCTOR "
348 std::cout <<
"╠════════════════════════════════════════════════════════════"
350 std::cout << absl::StrFormat(
"║ Total Sheets: %-46d ║\n",
351 static_cast<int>(kNumGfxSheets));
352 std::cout << absl::StrFormat(
"║ Successful Decompressions: %-33d ║\n",
354 std::cout << absl::StrFormat(
"║ Failed Decompressions: %-37d ║\n",
356 std::cout << absl::StrFormat(
"║ Empty Sheets: %-46d ║\n", empty_sheets);
357 std::cout << absl::StrFormat(
"║ Suspicious Sheets: %-41d ║\n",
359 std::cout <<
"╠════════════════════════════════════════════════════════════"
361 std::cout << absl::StrFormat(
362 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
365 std::cout <<
"╚════════════════════════════════════════════════════════════"
368 if (verbose && !report.
findings.empty()) {
369 std::cout <<
"\n=== Detailed Findings ===\n";
370 for (
const auto& finding : report.
findings) {
371 std::cout <<
" " << finding.FormatText() <<
"\n";
374 std::cout <<
"\nUse --verbose to see detailed findings.\n";
378 std::cout <<
"\n \033[1;32mNo critical issues found.\033[0m\n";
383 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
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.
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Locates graphics sheets in the ROM and writes edited sheets back.
constexpr uint32_t kNumGfxSheets
void CheckSheetContents(const Rom &rom, const std::vector< std::optional< zelda3::GfxSheetExtent > > &extents, DiagnosticReport &report, int target_sheet, bool verbose, int &empty_sheets, int &suspicious_sheets)
constexpr uint32_t kNumMainBlocksets
constexpr uint32_t kNumRoomBlocksets
void CheckSheetOverlaps(const std::vector< std::optional< zelda3::GfxSheetExtent > > &extents, DiagnosticReport &report, int target_sheet, int &aliased, int &overlapping)
void AddFinding(DiagnosticReport &report, const std::string &id, DiagnosticSeverity severity, const std::string &message, const std::string &location, const std::string &suggested_action="")
void ValidateGroupTables(const Rom &rom, DiagnosticReport &report, int &invalid_refs)
std::vector< std::optional< zelda3::GfxSheetExtent > > ScanSheets(const Rom &rom, DiagnosticReport &report, int target_sheet, bool verbose, int &successful, int &failed, int &oversized)
constexpr uint32_t kNumSpritesets
bool IsSelected(int target_sheet, uint32_t sheet)
constexpr uint32_t kSpriteSheetBase
DiagnosticSeverity
Severity level for diagnostic findings.
absl::StatusOr< GfxSheetExtent > ReadGfxSheetExtent(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
constexpr size_t kGfxSheet3bppBytes
constexpr int kSpriteBlocksetPointer
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
constexpr size_t kGfxSheet2bppBytes
constexpr uint32_t kEntranceGfxGroup
constexpr size_t kGfxSheetCount
const char * GfxSheetStorageKindName(GfxSheetStorageKind kind)
constexpr int kGfxGroupsPointer
uint32_t SnesToPc(uint32_t addr) noexcept
A single diagnostic finding.
std::string suggested_action
DiagnosticSeverity severity
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.