yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_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"
8#include "rom/rom.h"
10#include "zelda3/game_data.h"
11
12namespace yaze {
13namespace cli {
14
15namespace {
16
17// Validate sprite pointer table entries
19 bool verbose) {
20 const auto& data = rom->vector();
21
22 // Check sprite pointers for all 296 rooms
23 int invalid_count = 0;
24 for (int room = 0; room < zelda3::kNumberOfRooms; ++room) {
25 uint32_t ptr_addr = zelda3::kRoomsSpritePointer + (room * 2);
26
27 if (ptr_addr + 1 >= rom->size()) {
28 DiagnosticFinding finding;
29 finding.id = "sprite_ptr_out_of_bounds";
31 finding.message = absl::StrFormat(
32 "Sprite pointer table address 0x%06X is beyond ROM size", ptr_addr);
33 finding.location = absl::StrFormat("Room %d", room);
34 finding.fixable = false;
35 report.AddFinding(finding);
36 return;
37 }
38
39 // Read 2-byte pointer (little endian)
40 uint16_t ptr = data[ptr_addr] | (data[ptr_addr + 1] << 8);
41
42 // Pointers point into Bank 09 (0x090000)
43 uint32_t sprite_addr = 0x090000 + ptr;
44
45 // Validate pointer points to valid sprite data region
46 if (sprite_addr < zelda3::kSpritesData ||
47 sprite_addr >= zelda3::kSpritesEndData) {
48 if (verbose || invalid_count < 10) {
49 DiagnosticFinding finding;
50 finding.id = "invalid_sprite_ptr";
52 finding.message = absl::StrFormat(
53 "Room %d sprite pointer 0x%04X -> 0x%06X outside valid range "
54 "(0x%06X-0x%06X)",
55 room, ptr, sprite_addr, zelda3::kSpritesData,
57 finding.location = absl::StrFormat("0x%06X", ptr_addr);
58 finding.fixable = false;
59 report.AddFinding(finding);
60 }
61 invalid_count++;
62 }
63 }
64
65 if (invalid_count > 0) {
66 DiagnosticFinding finding;
67 finding.id = "sprite_ptr_summary";
69 finding.message = absl::StrFormat(
70 "Found %d rooms with potentially invalid sprite pointers",
71 invalid_count);
72 finding.location = "Sprite Pointer Table";
73 finding.fixable = false;
74 report.AddFinding(finding);
75 }
76}
77
78// Validate spriteset graphics references
80 const auto& data = rom->vector();
81
82 // Spriteset table at kSpriteBlocksetPointer
83 // 144 spritesets, 4 bytes each (4 graphics sheet references)
84 uint32_t spriteset_addr = zelda3::kSpriteBlocksetPointer;
85 constexpr int kNumSpritesets = 144;
86 constexpr int kNumGfxSheets = 223;
87
88 int invalid_refs = 0;
89
90 for (int set = 0; set < kNumSpritesets; ++set) {
91 for (int slot = 0; slot < 4; ++slot) {
92 uint32_t addr = spriteset_addr + (set * 4) + slot;
93 if (addr >= rom->size()) {
94 DiagnosticFinding finding;
95 finding.id = "spriteset_addr_out_of_bounds";
97 finding.message = absl::StrFormat(
98 "Spriteset %d address 0x%06X beyond ROM size", set, addr);
99 finding.location = absl::StrFormat("Spriteset %d", set);
100 finding.fixable = false;
101 report.AddFinding(finding);
102 return;
103 }
104
105 uint8_t sheet_id = data[addr];
106 // 0xFF is valid (empty slot), other values should be < 223
107 if (sheet_id != 0xFF && sheet_id >= kNumGfxSheets) {
108 if (invalid_refs < 20) {
109 DiagnosticFinding finding;
110 finding.id = "invalid_spriteset_sheet";
112 finding.message = absl::StrFormat(
113 "Spriteset %d slot %d references invalid sheet %d (max: %d)", set,
114 slot, sheet_id, kNumGfxSheets - 1);
115 finding.location = absl::StrFormat("Spriteset %d slot %d", set, slot);
116 finding.fixable = false;
117 report.AddFinding(finding);
118 }
119 invalid_refs++;
120 }
121 }
122 }
123
124 if (invalid_refs > 0) {
125 DiagnosticFinding finding;
126 finding.id = "spriteset_summary";
128 finding.message = absl::StrFormat(
129 "Found %d invalid spriteset sheet references", invalid_refs);
130 finding.location = "Spriteset Table";
131 finding.fixable = false;
132 report.AddFinding(finding);
133 }
134}
135
136// Validate sprite data for a specific room
137void ValidateRoomSprites(Rom* rom, int room_id, DiagnosticReport& report,
138 int& total_sprites, int& empty_rooms) {
139 const auto& data = rom->vector();
140
141 // Get sprite pointer for this room
142 uint32_t ptr_addr = zelda3::kRoomsSpritePointer + (room_id * 2);
143 if (ptr_addr + 1 >= rom->size())
144 return;
145
146 uint16_t ptr = data[ptr_addr] | (data[ptr_addr + 1] << 8);
147 uint32_t sprite_addr = 0x090000 + ptr;
148
149 // Empty room check
150 if (sprite_addr == zelda3::kSpritesDataEmptyRoom) {
151 empty_rooms++;
152 return;
153 }
154
155 if (sprite_addr >= rom->size())
156 return;
157
158 // Read sort byte
159 if (sprite_addr >= rom->size())
160 return;
161 // uint8_t sort_byte = data[sprite_addr];
162 sprite_addr++;
163
164 // Parse sprites (3 bytes each, terminated by 0xFF)
165 int room_sprite_count = 0;
166 const int kMaxSpritesPerRoom = 32; // Reasonable limit
167
168 while (sprite_addr < rom->size() && room_sprite_count < kMaxSpritesPerRoom) {
169 uint8_t y_pos = data[sprite_addr];
170 if (y_pos == 0xFF)
171 break; // Terminator
172
173 if (sprite_addr + 2 >= rom->size()) {
174 DiagnosticFinding finding;
175 finding.id = "truncated_sprite_data";
177 finding.message = absl::StrFormat(
178 "Room %d sprite data truncated at 0x%06X", room_id, sprite_addr);
179 finding.location = absl::StrFormat("Room %d", room_id);
180 finding.fixable = false;
181 report.AddFinding(finding);
182 break;
183 }
184
185 // uint8_t x_subtype = data[sprite_addr + 1];
186 uint8_t sprite_id = data[sprite_addr + 2];
187
188 // Check for potentially invalid sprite IDs
189 // Valid sprite IDs are typically 0x00-0xF2
190 // IDs above 0xF2 are special or invalid
191 if (sprite_id > 0xF2) {
192 // Some overlord sprites use high IDs, but many are invalid
193 // This is informational only
194 }
195
196 sprite_addr += 3;
197 room_sprite_count++;
198 total_sprites++;
199 }
200
201 // Check if we hit the max without finding a terminator
202 if (room_sprite_count >= kMaxSpritesPerRoom) {
203 DiagnosticFinding finding;
204 finding.id = "sprite_count_limit";
206 finding.message =
207 absl::StrFormat("Room %d has %d+ sprites (hit scan limit)", room_id,
208 kMaxSpritesPerRoom);
209 finding.location = absl::StrFormat("Room %d", room_id);
210 finding.fixable = false;
211 report.AddFinding(finding);
212 }
213}
214
215// Check for common sprite issues
217 const auto& data = rom->vector();
218
219 // Check for zeroed sprite pointer table (corruption sign)
220 int zero_pointers = 0;
221 for (int room = 0; room < zelda3::kNumberOfRooms; ++room) {
222 uint32_t ptr_addr = zelda3::kRoomsSpritePointer + (room * 2);
223 if (ptr_addr + 1 >= rom->size())
224 break;
225
226 uint16_t ptr = data[ptr_addr] | (data[ptr_addr + 1] << 8);
227 if (ptr == 0x0000) {
228 zero_pointers++;
229 }
230 }
231
232 if (zero_pointers > 50) { // More than ~17% zero is suspicious
233 DiagnosticFinding finding;
234 finding.id = "many_zero_sprite_ptrs";
236 finding.message = absl::StrFormat(
237 "Found %d rooms with zero sprite pointers (possible corruption)",
238 zero_pointers);
239 finding.location = "Sprite Pointer Table";
240 finding.suggested_action = "Verify ROM integrity";
241 finding.fixable = false;
242 report.AddFinding(finding);
243 }
244}
245
246} // namespace
247
249 Rom* rom, const resources::ArgumentParser& parser,
250 resources::OutputFormatter& formatter) {
251 bool verbose = parser.HasFlag("verbose");
252 bool scan_all = parser.HasFlag("all");
253
254 DiagnosticReport report;
255
256 if (!rom || !rom->is_loaded()) {
257 return absl::InvalidArgumentError("ROM not loaded");
258 }
259
260 // Check for specific room
261 auto room_arg = parser.GetInt("room");
262 bool single_room = room_arg.ok();
263 int target_room = single_room ? *room_arg : -1;
264
265 if (single_room) {
266 if (target_room < 0 || target_room >= zelda3::kNumberOfRooms) {
267 return absl::InvalidArgumentError(
268 absl::StrFormat("Room ID %d out of range (0-%d)", target_room,
270 }
271 }
272
273 // 1. Validate sprite pointer table
274 ValidateSpritePointerTable(rom, report, verbose);
275
276 // 2. Validate spriteset references
277 ValidateSpritesets(rom, report);
278
279 // 3. Validate room sprites
280 int total_sprites = 0;
281 int empty_rooms = 0;
282 int rooms_scanned = 0;
283
284 if (single_room) {
285 ValidateRoomSprites(rom, target_room, report, total_sprites, empty_rooms);
286 rooms_scanned = 1;
287 } else if (scan_all) {
288 for (int room = 0; room < zelda3::kNumberOfRooms; ++room) {
289 ValidateRoomSprites(rom, room, report, total_sprites, empty_rooms);
290 rooms_scanned++;
291 }
292 } else {
293 // Sample rooms: first 20, some middle, some end
294 std::vector<int> sample_rooms;
295 for (int i = 0; i < 20; ++i)
296 sample_rooms.push_back(i);
297 for (int i = 100; i < 110; ++i)
298 sample_rooms.push_back(i);
299 for (int i = 200; i < 210; ++i)
300 sample_rooms.push_back(i);
301
302 for (int room : sample_rooms) {
303 if (room < zelda3::kNumberOfRooms) {
304 ValidateRoomSprites(rom, room, report, total_sprites, empty_rooms);
305 rooms_scanned++;
306 }
307 }
308 }
309
310 // 4. Check common issues
311 CheckCommonSpriteIssues(rom, report);
312
313 // Output results
314 formatter.AddField("rooms_scanned", rooms_scanned);
315 formatter.AddField("total_sprites", total_sprites);
316 formatter.AddField("empty_rooms", empty_rooms);
317 formatter.AddField("total_findings", report.TotalFindings());
318 formatter.AddField("critical_count", report.critical_count);
319 formatter.AddField("error_count", report.error_count);
320 formatter.AddField("warning_count", report.warning_count);
321 formatter.AddField("info_count", report.info_count);
322
323 // JSON findings array
324 if (formatter.IsJson()) {
325 formatter.BeginArray("findings");
326 for (const auto& finding : report.findings) {
327 formatter.AddArrayItem(finding.FormatJson());
328 }
329 formatter.EndArray();
330 }
331
332 // Text output
333 if (!formatter.IsJson()) {
334 std::cout << "\n";
335 std::cout << "╔════════════════════════════════════════════════════════════"
336 "═══╗\n";
337 std::cout << "║ SPRITE DOCTOR "
338 " ║\n";
339 std::cout << "╠════════════════════════════════════════════════════════════"
340 "═══╣\n";
341 std::cout << absl::StrFormat("║ Rooms Scanned: %-45d ║\n", rooms_scanned);
342 std::cout << absl::StrFormat("║ Total Sprites Found: %-39d ║\n",
343 total_sprites);
344 std::cout << absl::StrFormat("║ Empty Rooms: %-47d ║\n", empty_rooms);
345 std::cout << "╠════════════════════════════════════════════════════════════"
346 "═══╣\n";
347 std::cout << absl::StrFormat(
348 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
349 report.TotalFindings(), report.error_count, report.warning_count,
350 report.info_count, "");
351 std::cout << "╚════════════════════════════════════════════════════════════"
352 "═══╝\n";
353
354 if (verbose && !report.findings.empty()) {
355 std::cout << "\n=== Detailed Findings ===\n";
356 for (const auto& finding : report.findings) {
357 std::cout << " " << finding.FormatText() << "\n";
358 }
359 } else if (!verbose && report.HasProblems()) {
360 std::cout << "\nUse --verbose to see detailed findings.\n";
361 }
362
363 if (!report.HasProblems()) {
364 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
365 }
366 std::cout << "\n";
367 }
368
369 return absl::OkStatus();
370}
371
372} // namespace cli
373} // 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.
void ValidateSpritesets(Rom *rom, DiagnosticReport &report)
void ValidateRoomSprites(Rom *rom, int room_id, DiagnosticReport &report, int &total_sprites, int &empty_rooms)
void CheckCommonSpriteIssues(Rom *rom, DiagnosticReport &report)
void ValidateSpritePointerTable(Rom *rom, DiagnosticReport &report, bool verbose)
constexpr int kSpritesEndData
constexpr int kSpriteBlocksetPointer
constexpr int kSpritesData
constexpr int kRoomsSpritePointer
constexpr int kNumberOfRooms
constexpr int kSpritesDataEmptyRoom
constexpr uint32_t kNumSpritesets
Definition rom_old.h:54
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.