yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
graphics_doctor_commands.cc
Go to the documentation of this file.
2
3#include <iostream>
4
5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
9#include "rom/rom.h"
11#include "zelda3/game_data.h"
12
13namespace yaze {
14namespace cli {
15
16namespace {
17
18constexpr uint32_t kNumGfxSheets = 223;
19constexpr uint32_t kNumMainBlocksets = 37;
20constexpr uint32_t kNumRoomBlocksets = 82;
21constexpr uint32_t kUncompressedSheetSize = 0x0800; // 2048 bytes
22
23// Get graphics address for a sheet (adapted from zelda3::GetGraphicsAddress)
24uint32_t GetGfxAddress(const uint8_t* data, uint8_t sheet_id, size_t rom_size) {
25 uint32_t ptr_base = zelda3::kGfxGroupsPointer;
26
27 if (ptr_base + 0x200 + sheet_id >= rom_size) {
28 return 0;
29 }
30
31 uint8_t bank = data[ptr_base + sheet_id];
32 uint8_t high = data[ptr_base + 0x100 + sheet_id];
33 uint8_t low = data[ptr_base + 0x200 + sheet_id];
34
35 // Convert to SNES address then to PC address
36 uint32_t snes_addr = (bank << 16) | (high << 8) | low;
37
38 // LoROM conversion: bank * 0x8000 + (addr & 0x7FFF)
39 uint32_t pc_addr = ((bank & 0x7F) * 0x8000) + (snes_addr & 0x7FFF);
40
41 return pc_addr;
42}
43
44// Validate graphics pointer table
46 std::vector<uint32_t>& valid_addresses) {
47 const auto& data = rom->vector();
48 uint32_t ptr_base = zelda3::kGfxGroupsPointer;
49
50 if (ptr_base + 0x300 >= rom->size()) {
51 DiagnosticFinding finding;
52 finding.id = "gfx_ptr_table_missing";
54 finding.message = "Graphics pointer table beyond ROM bounds";
55 finding.location = absl::StrFormat("0x%06X", ptr_base);
56 finding.fixable = false;
57 report.AddFinding(finding);
58 return;
59 }
60
61 int invalid_count = 0;
62 for (uint32_t i = 0; i < kNumGfxSheets; ++i) {
63 uint32_t addr = GetGfxAddress(data.data(), i, rom->size());
64
65 if (addr == 0 || addr >= rom->size()) {
66 if (invalid_count < 10) {
67 DiagnosticFinding finding;
68 finding.id = "invalid_gfx_ptr";
70 finding.message = absl::StrFormat(
71 "Sheet %d has invalid pointer 0x%06X (ROM size: 0x%zX)",
72 i, addr, rom->size());
73 finding.location = absl::StrFormat("Sheet %d", i);
74 finding.fixable = false;
75 report.AddFinding(finding);
76 }
77 invalid_count++;
78 valid_addresses.push_back(0); // Mark as invalid
79 } else {
80 valid_addresses.push_back(addr);
81 }
82 }
83
84 if (invalid_count > 0) {
85 DiagnosticFinding finding;
86 finding.id = "gfx_ptr_summary";
88 finding.message = absl::StrFormat(
89 "Found %d sheets with invalid pointers", invalid_count);
90 finding.location = "Graphics Pointer Table";
91 finding.fixable = false;
92 report.AddFinding(finding);
93 }
94}
95
96// Test decompression for sheets
97void ValidateCompression(Rom* rom, const std::vector<uint32_t>& addresses,
98 DiagnosticReport& report, bool verbose,
99 int& successful_decomp, int& failed_decomp) {
100 const auto& data = rom->vector();
101
102 for (uint32_t i = 0; i < kNumGfxSheets; ++i) {
103 if (i >= addresses.size() || addresses[i] == 0) {
104 failed_decomp++;
105 continue;
106 }
107
108 uint32_t addr = addresses[i];
109
110 // Try to decompress
111 auto result = gfx::lc_lz2::DecompressV2(data.data(), addr,
113 rom->size());
114
115 if (!result.ok()) {
116 if (verbose || failed_decomp < 10) {
117 DiagnosticFinding finding;
118 finding.id = "decompression_failed";
120 finding.message = absl::StrFormat(
121 "Sheet %d decompression failed at 0x%06X: %s",
122 i, addr, std::string(result.status().message()));
123 finding.location = absl::StrFormat("Sheet %d", i);
124 finding.fixable = false;
125 report.AddFinding(finding);
126 }
127 failed_decomp++;
128 } else {
129 // Check decompressed size
130 if (result->size() != kUncompressedSheetSize) {
131 if (verbose || failed_decomp < 10) {
132 DiagnosticFinding finding;
133 finding.id = "unexpected_sheet_size";
135 finding.message = absl::StrFormat(
136 "Sheet %d decompressed to %zu bytes (expected %d)",
137 i, result->size(), kUncompressedSheetSize);
138 finding.location = absl::StrFormat("Sheet %d", i);
139 finding.fixable = false;
140 report.AddFinding(finding);
141 }
142 }
143 successful_decomp++;
144 }
145 }
146}
147
148// Validate blockset references
150 const auto& data = rom->vector();
151
152 // Main blocksets pointer
153 // Main blocksets: 37 sets, 8 bytes each (8 sheet IDs)
154 uint32_t main_blockset_ptr = 0x5B57; // kSpriteBlocksetPointer area
155
156 // For simplicity, we'll check that blockset IDs reference valid sheets
157 // The actual blockset table structure varies by ROM version
158
159 int invalid_refs = 0;
160
161 // Check a sample of known blockset-like structures
162 // Room blocksets at different locations - simplified check
163 uint32_t room_blockset_ptr = 0x50C0; // Approximate location
164
165 if (room_blockset_ptr + (kNumRoomBlocksets * 4) < rom->size()) {
166 for (uint32_t i = 0; i < kNumRoomBlocksets; ++i) {
167 for (int slot = 0; slot < 4; ++slot) {
168 uint32_t addr = room_blockset_ptr + (i * 4) + slot;
169 if (addr >= rom->size()) break;
170
171 uint8_t sheet_id = data[addr];
172 if (sheet_id != 0xFF && sheet_id >= kNumGfxSheets) {
173 if (invalid_refs < 20) {
174 DiagnosticFinding finding;
175 finding.id = "invalid_blockset_ref";
177 finding.message = absl::StrFormat(
178 "Room blockset %d slot %d references invalid sheet %d",
179 i, slot, sheet_id);
180 finding.location = absl::StrFormat("Room blockset %d", i);
181 finding.fixable = false;
182 report.AddFinding(finding);
183 }
184 invalid_refs++;
185 }
186 }
187 }
188 }
189
190 if (invalid_refs > 0) {
191 DiagnosticFinding finding;
192 finding.id = "blockset_summary";
194 finding.message = absl::StrFormat(
195 "Found %d invalid blockset sheet references", invalid_refs);
196 finding.location = "Blockset Tables";
197 finding.fixable = false;
198 report.AddFinding(finding);
199 }
200}
201
202// Check for empty/corrupted sheets
203void CheckSheetIntegrity(Rom* rom, const std::vector<uint32_t>& addresses,
204 DiagnosticReport& report, bool verbose,
205 int& empty_sheets, int& suspicious_sheets) {
206 const auto& data = rom->vector();
207
208 for (uint32_t i = 0; i < kNumGfxSheets; ++i) {
209 if (i >= addresses.size() || addresses[i] == 0) {
210 continue;
211 }
212
213 uint32_t addr = addresses[i];
214
215 // Try to decompress first
216 auto result = gfx::lc_lz2::DecompressV2(data.data(), addr,
218 rom->size());
219
220 if (!result.ok()) continue;
221
222 const auto& sheet_data = *result;
223
224 // Check for all-zeros (empty)
225 bool all_zero = true;
226 bool all_ff = true;
227 for (uint8_t byte : sheet_data) {
228 if (byte != 0x00) all_zero = false;
229 if (byte != 0xFF) all_ff = false;
230 if (!all_zero && !all_ff) break;
231 }
232
233 if (all_zero) {
234 if (verbose || empty_sheets < 10) {
235 DiagnosticFinding finding;
236 finding.id = "empty_sheet";
238 finding.message = absl::StrFormat(
239 "Sheet %d is all zeros (empty)", i);
240 finding.location = absl::StrFormat("Sheet %d at 0x%06X", i, addr);
241 finding.fixable = false;
242 report.AddFinding(finding);
243 }
244 empty_sheets++;
245 } else if (all_ff) {
246 if (verbose || suspicious_sheets < 10) {
247 DiagnosticFinding finding;
248 finding.id = "erased_sheet";
250 finding.message = absl::StrFormat(
251 "Sheet %d is all 0xFF (erased/uninitialized)", i);
252 finding.location = absl::StrFormat("Sheet %d at 0x%06X", i, addr);
253 finding.suggested_action = "Sheet may need to be restored";
254 finding.fixable = false;
255 report.AddFinding(finding);
256 }
257 suspicious_sheets++;
258 }
259 }
260}
261
262} // namespace
263
265 Rom* rom, const resources::ArgumentParser& parser,
266 resources::OutputFormatter& formatter) {
267 bool verbose = parser.HasFlag("verbose");
268 bool scan_all = parser.HasFlag("all");
269
270 DiagnosticReport report;
271
272 if (!rom || !rom->is_loaded()) {
273 return absl::InvalidArgumentError("ROM not loaded");
274 }
275
276 // Check for specific sheet
277 auto sheet_arg = parser.GetInt("sheet");
278 bool single_sheet = sheet_arg.ok();
279 int target_sheet = single_sheet ? *sheet_arg : -1;
280
281 if (single_sheet) {
282 if (target_sheet < 0 || target_sheet >= static_cast<int>(kNumGfxSheets)) {
283 return absl::InvalidArgumentError(absl::StrFormat(
284 "Sheet ID %d out of range (0-%d)", target_sheet, kNumGfxSheets - 1));
285 }
286 }
287
288 // 1. Validate graphics pointer table
289 std::vector<uint32_t> valid_addresses;
290 ValidateGraphicsPointerTable(rom, report, valid_addresses);
291
292 // 2. Test decompression
293 int successful_decomp = 0;
294 int failed_decomp = 0;
295
296 if (single_sheet) {
297 // Just test the one sheet
298 if (target_sheet < static_cast<int>(valid_addresses.size()) &&
299 valid_addresses[target_sheet] != 0) {
300 const auto& data = rom->vector();
301 auto result = gfx::lc_lz2::DecompressV2(
302 data.data(), valid_addresses[target_sheet],
303 kUncompressedSheetSize, 1, rom->size());
304 if (result.ok()) {
305 successful_decomp = 1;
306 formatter.AddField("decompressed_size",
307 static_cast<int>(result->size()));
308 } else {
309 failed_decomp = 1;
310 }
311 }
312 } else if (scan_all || !single_sheet) {
313 ValidateCompression(rom, valid_addresses, report, verbose,
314 successful_decomp, failed_decomp);
315 }
316
317 // 3. Validate blocksets
318 ValidateBlocksets(rom, report);
319
320 // 4. Check sheet integrity
321 int empty_sheets = 0;
322 int suspicious_sheets = 0;
323 CheckSheetIntegrity(rom, valid_addresses, report, verbose,
324 empty_sheets, suspicious_sheets);
325
326 // Output results
327 formatter.AddField("total_sheets", static_cast<int>(kNumGfxSheets));
328 formatter.AddField("successful_decompressions", successful_decomp);
329 formatter.AddField("failed_decompressions", failed_decomp);
330 formatter.AddField("empty_sheets", empty_sheets);
331 formatter.AddField("suspicious_sheets", suspicious_sheets);
332 formatter.AddField("total_findings", report.TotalFindings());
333 formatter.AddField("critical_count", report.critical_count);
334 formatter.AddField("error_count", report.error_count);
335 formatter.AddField("warning_count", report.warning_count);
336 formatter.AddField("info_count", report.info_count);
337
338 // JSON findings array
339 if (formatter.IsJson()) {
340 formatter.BeginArray("findings");
341 for (const auto& finding : report.findings) {
342 formatter.AddArrayItem(finding.FormatJson());
343 }
344 formatter.EndArray();
345 }
346
347 // Text output
348 if (!formatter.IsJson()) {
349 std::cout << "\n";
350 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
351 std::cout << "║ GRAPHICS DOCTOR ║\n";
352 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
353 std::cout << absl::StrFormat("║ Total Sheets: %-46d ║\n",
354 static_cast<int>(kNumGfxSheets));
355 std::cout << absl::StrFormat("║ Successful Decompressions: %-33d ║\n",
356 successful_decomp);
357 std::cout << absl::StrFormat("║ Failed Decompressions: %-37d ║\n",
358 failed_decomp);
359 std::cout << absl::StrFormat("║ Empty Sheets: %-46d ║\n", empty_sheets);
360 std::cout << absl::StrFormat("║ Suspicious Sheets: %-41d ║\n",
361 suspicious_sheets);
362 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
363 std::cout << absl::StrFormat(
364 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
365 report.TotalFindings(), report.error_count, report.warning_count,
366 report.info_count, "");
367 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
368
369 if (verbose && !report.findings.empty()) {
370 std::cout << "\n=== Detailed Findings ===\n";
371 for (const auto& finding : report.findings) {
372 std::cout << " " << finding.FormatText() << "\n";
373 }
374 } else if (!verbose && report.HasProblems()) {
375 std::cout << "\nUse --verbose to see detailed findings.\n";
376 }
377
378 if (!report.HasProblems()) {
379 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
380 }
381 std::cout << "\n";
382 }
383
384 return absl::OkStatus();
385}
386
387} // namespace cli
388} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
const auto & vector() const
Definition rom.h:139
auto size() const
Definition rom.h:134
bool is_loaded() const
Definition rom.h:128
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)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
uint32_t GetGfxAddress(const uint8_t *data, uint8_t sheet_id, size_t rom_size)
void ValidateGraphicsPointerTable(Rom *rom, DiagnosticReport &report, std::vector< uint32_t > &valid_addresses)
void CheckSheetIntegrity(Rom *rom, const std::vector< uint32_t > &addresses, DiagnosticReport &report, bool verbose, int &empty_sheets, int &suspicious_sheets)
void ValidateCompression(Rom *rom, const std::vector< uint32_t > &addresses, DiagnosticReport &report, bool verbose, int &successful_decomp, int &failed_decomp)
absl::StatusOr< std::vector< uint8_t > > DecompressV2(const uint8_t *data, int offset, int size, int mode, size_t rom_size)
Decompresses a buffer of data using the LC_LZ2 algorithm.
constexpr int kGfxGroupsPointer
constexpr uint32_t kUncompressedSheetSize
Definition rom_old.h:51
constexpr uint32_t kNumMainBlocksets
Definition rom_old.h:52
constexpr uint32_t kNumRoomBlocksets
Definition rom_old.h:53
constexpr uint32_t kNumGfxSheets
Definition rom_old.h:32
A single diagnostic finding.
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.