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