yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
hex_inspector_commands.cc
Go to the documentation of this file.
2
3#include <iomanip>
4#include <iostream>
5#include <vector>
6
7#include "absl/strings/numbers.h"
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
12#include "rom/rom.h"
13
14namespace yaze {
15namespace cli {
16
17namespace {
18
19enum class AddressMode { kPc, kSnes };
20
21std::string PcToSnesLoRom(int pc_addr) {
22 int bank = pc_addr / 0x8000;
23 int addr = (pc_addr % 0x8000) + 0x8000;
24 return absl::StrFormat("%02X:%04X", bank, addr);
25}
26
27int SnesToPcLoRom(int bank, int addr) {
28 if (addr < 0x8000)
29 return -1; // Invalid for ROM data in LoROM
30 int pc_addr = ((bank & 0x7F) * 0x8000) + (addr - 0x8000);
31 return pc_addr;
32}
33
34void PrintHexDump(const std::vector<uint8_t>& data, int offset, int size,
35 AddressMode mode,
36 [[maybe_unused]] resources::OutputFormatter& formatter) {
37 std::string output;
38 for (int i = 0; i < size; i += 16) {
39 // Print address
40 if (mode == AddressMode::kSnes) {
41 output += PcToSnesLoRom(offset + i) + " ";
42 }
43 absl::StrAppend(&output, absl::StrFormat("%06X: ", offset + i));
44
45 // Print hex values
46 for (int j = 0; j < 16; ++j) {
47 if (i + j < size) {
48 absl::StrAppend(&output, absl::StrFormat("%02X ", data[i + j]));
49 } else {
50 absl::StrAppend(&output, " ");
51 }
52 }
53
54 absl::StrAppend(&output, " |");
55
56 // Print ASCII values
57 for (int j = 0; j < 16; ++j) {
58 if (i + j < size) {
59 unsigned char c = data[i + j];
60 if (c >= 32 && c <= 126) {
61 absl::StrAppend(&output, absl::StrFormat("%c", c));
62 } else {
63 absl::StrAppend(&output, ".");
64 }
65 }
66 }
67 absl::StrAppend(&output, "|\n");
68 }
69 // For visual hex dump, we print directly to stdout.
70 // OutputFormatter is used for structured data (JSON/Text KV), which isn't suitable for a visual block.
71 std::cout << output;
72}
73
74} // namespace
75
77 const resources::ArgumentParser& parser) {
78 if (parser.GetPositional().empty()) {
79 return absl::InvalidArgumentError("Missing ROM path.");
80 }
81 if (parser.GetPositional().size() < 2) {
82 return absl::InvalidArgumentError("Missing offset.");
83 }
84 return absl::OkStatus();
85}
86
88 [[maybe_unused]] Rom* rom, const resources::ArgumentParser& parser,
89 resources::OutputFormatter& formatter) {
90 std::string rom_path = parser.GetPositional()[0];
91 std::string offset_str = parser.GetPositional()[1];
92 int size = 256; // Default size
93
94 if (parser.GetPositional().size() >= 3) {
95 if (!absl::SimpleAtoi(parser.GetPositional()[2], &size)) {
96 return absl::InvalidArgumentError(
97 absl::StrFormat("Invalid size: %s", parser.GetPositional()[2]));
98 }
99 }
100
101 AddressMode mode = AddressMode::kPc;
102 if (auto mode_arg = parser.GetString("mode")) {
103 if (*mode_arg == "snes") {
104 mode = AddressMode::kSnes;
105 } else if (*mode_arg != "pc") {
106 return absl::InvalidArgumentError("Invalid mode: " + *mode_arg);
107 }
108 }
109
110 int offset = 0;
111 // Handle SNES address input (e.g., 00:8000)
112 if (absl::StrContains(offset_str, ':')) {
113 std::vector<std::string> parts = absl::StrSplit(offset_str, ':');
114 if (parts.size() == 2) {
115 int bank = 0;
116 int addr = 0;
117 if (absl::SimpleAtoi(parts[0], &bank) &&
118 absl::SimpleAtoi(
119 parts[1],
120 &addr)) { // This assumes decimal if no 0x, but usually hex for addresses
121 // Let's try hex parsing for bank/addr
122 try {
123 bank = std::stoi(parts[0], nullptr, 16);
124 addr = std::stoi(parts[1], nullptr, 16);
125 offset = SnesToPcLoRom(bank, addr);
126 if (offset == -1) {
127 return absl::InvalidArgumentError(
128 "Invalid LoROM SNES address (addr < 0x8000)");
129 }
130 // Auto-enable SNES mode if user provided SNES address
131 if (!parser.GetString("mode")) {
132 mode = AddressMode::kSnes;
133 }
134 } catch (...) {
135 return absl::InvalidArgumentError(
136 "Invalid SNES address format (expected HEX:HEX)");
137 }
138 }
139 }
140 } else if (offset_str.size() > 2 && offset_str.substr(0, 2) == "0x") {
141 try {
142 offset = std::stoi(offset_str, nullptr, 16);
143 } catch (...) {
144 return absl::InvalidArgumentError(
145 absl::StrFormat("Invalid hex offset: %s", offset_str));
146 }
147 } else {
148 if (!absl::SimpleAtoi(offset_str, &offset)) {
149 return absl::InvalidArgumentError(
150 absl::StrFormat("Invalid offset: %s", offset_str));
151 }
152 }
153
154 // Load ROM locally since RequiresRom() is false (to allow inspecting any file)
155 Rom local_rom;
156 auto status = local_rom.LoadFromFile(rom_path);
157 if (!status.ok()) {
158 return status;
159 }
160
161 if (offset < 0 || static_cast<size_t>(offset) >= local_rom.size()) {
162 return absl::InvalidArgumentError(absl::StrFormat(
163 "Offset out of bounds. ROM size: %lu", local_rom.size()));
164 }
165
166 if (static_cast<size_t>(offset) + static_cast<size_t>(size) >
167 local_rom.size()) {
168 size = static_cast<int>(local_rom.size() - static_cast<size_t>(offset));
169 }
170
171 std::vector<uint8_t> buffer(size);
172 const auto& rom_data = local_rom.vector();
173 for (int i = 0; i < size; ++i) {
174 buffer[i] = rom_data[offset + i];
175 }
176
177 PrintHexDump(buffer, offset, size, mode, formatter);
178 return absl::OkStatus();
179}
180
181// =============================================================================
182// HexCompareCommandHandler
183// =============================================================================
184
186 const resources::ArgumentParser& parser) {
187 if (parser.GetPositional().size() < 2) {
188 return absl::InvalidArgumentError(
189 "Missing ROM paths. Usage: hex-compare <rom1> <rom2>");
190 }
191 return absl::OkStatus();
192}
193
195 [[maybe_unused]] Rom* rom, const resources::ArgumentParser& parser,
196 resources::OutputFormatter& formatter) {
197 std::string rom1_path = parser.GetPositional()[0];
198 std::string rom2_path = parser.GetPositional()[1];
199
200 // Parse optional start/end offsets
201 int start_offset = 0;
202 int end_offset = -1; // -1 means compare to end
203
204 if (auto start_str = parser.GetString("start")) {
205 if (start_str->size() > 2 && start_str->substr(0, 2) == "0x") {
206 start_offset = std::stoi(*start_str, nullptr, 16);
207 } else {
208 if (!absl::SimpleAtoi(*start_str, &start_offset)) {
209 return absl::InvalidArgumentError("Invalid start offset: " +
210 *start_str);
211 }
212 }
213 }
214
215 if (auto end_str = parser.GetString("end")) {
216 if (end_str->size() > 2 && end_str->substr(0, 2) == "0x") {
217 end_offset = std::stoi(*end_str, nullptr, 16);
218 } else {
219 if (!absl::SimpleAtoi(*end_str, &end_offset)) {
220 return absl::InvalidArgumentError("Invalid end offset: " + *end_str);
221 }
222 }
223 }
224
225 AddressMode mode = AddressMode::kPc;
226 if (auto mode_arg = parser.GetString("mode")) {
227 if (*mode_arg == "snes") {
228 mode = AddressMode::kSnes;
229 }
230 }
231
232 // Load both ROMs
233 Rom rom1, rom2;
234 auto status1 = rom1.LoadFromFile(rom1_path);
235 if (!status1.ok()) {
236 return absl::InvalidArgumentError("Failed to load ROM 1: " +
237 std::string(status1.message()));
238 }
239
240 auto status2 = rom2.LoadFromFile(rom2_path);
241 if (!status2.ok()) {
242 return absl::InvalidArgumentError("Failed to load ROM 2: " +
243 std::string(status2.message()));
244 }
245
246 // Determine comparison range
247 size_t compare_size = std::min(rom1.size(), rom2.size());
248 if (end_offset >= 0 && static_cast<size_t>(end_offset) < compare_size) {
249 compare_size = end_offset;
250 }
251
252 if (static_cast<size_t>(start_offset) >= compare_size) {
253 return absl::InvalidArgumentError("Start offset beyond ROM size");
254 }
255
256 // Compare bytes
257 std::vector<uint32_t> diff_offsets;
258 int total_diffs = 0;
259 const int max_diff_display = 20;
260
261 const auto& data1 = rom1.vector();
262 const auto& data2 = rom2.vector();
263
264 for (size_t i = start_offset; i < compare_size; ++i) {
265 if (data1[i] != data2[i]) {
266 total_diffs++;
267 if (diff_offsets.size() < max_diff_display) {
268 diff_offsets.push_back(static_cast<uint32_t>(i));
269 }
270 }
271 }
272
273 // Output results
274 formatter.AddField("rom1_path", rom1_path);
275 formatter.AddField("rom1_size", static_cast<int>(rom1.size()));
276 formatter.AddField("rom2_path", rom2_path);
277 formatter.AddField("rom2_size", static_cast<int>(rom2.size()));
278 formatter.AddField("compare_start", start_offset);
279 formatter.AddField("compare_end", static_cast<int>(compare_size));
280 formatter.AddField("total_differences", total_diffs);
281 formatter.AddField("sizes_match", rom1.size() == rom2.size());
282
283 // Output first N differences
284 if (!diff_offsets.empty() && !formatter.IsJson()) {
285 std::cout << "\n=== Hex Compare Results ===\n";
286 std::cout << absl::StrFormat("ROM 1: %s (%zu bytes)\n", rom1_path,
287 rom1.size());
288 std::cout << absl::StrFormat("ROM 2: %s (%zu bytes)\n", rom2_path,
289 rom2.size());
290 std::cout << absl::StrFormat("Range: 0x%06X - 0x%06zX\n", start_offset,
291 compare_size);
292 std::cout << absl::StrFormat("Total differences: %d\n\n", total_diffs);
293
294 std::cout << "First differences:\n";
295 std::cout << " Offset ROM1 ROM2\n";
296 std::cout << " ------ ---- ----\n";
297 for (uint32_t offset : diff_offsets) {
298 std::string addr_str;
299 if (mode == AddressMode::kSnes) {
300 addr_str = PcToSnesLoRom(offset) + " ";
301 }
302 std::cout << absl::StrFormat(" %s0x%06X: 0x%02X 0x%02X\n", addr_str,
303 offset, data1[offset], data2[offset]);
304 }
305 if (total_diffs > max_diff_display) {
306 std::cout << absl::StrFormat(" ... and %d more differences\n",
307 total_diffs - max_diff_display);
308 }
309 std::cout << "\n";
310 }
311
312 return absl::OkStatus();
313}
314
315// =============================================================================
316// HexAnnotateCommandHandler
317// =============================================================================
318
319namespace {
320
322 std::string name;
324 int size;
325 std::string format; // "hex", "decimal", "flags", "enum"
326};
327
329 std::string name;
331 std::vector<AnnotatedField> fields;
332};
333
334// SNES ROM Header structure at 0x7FC0
336 "SNES ROM Header",
337 32,
338 {
339 {"Title", 0, 21, "ascii"},
340 {"Map Mode", 21, 1, "hex"},
341 {"ROM Type", 22, 1, "hex"},
342 {"ROM Size", 23, 1, "decimal"},
343 {"SRAM Size", 24, 1, "decimal"},
344 {"Country Code", 25, 1, "hex"},
345 {"License Code", 26, 1, "hex"},
346 {"Version", 27, 1, "decimal"},
347 {"Checksum Complement", 28, 2, "hex"},
348 {"Checksum", 30, 2, "hex"},
349 }};
350
351// Dungeon room header structure
353 "Room Header",
354 14,
355 {
356 {"BG2 Property", 0, 1, "flags"},
357 {"Collision/Effect", 1, 1, "hex"},
358 {"Light/Dark", 2, 1, "flags"},
359 {"Palette", 3, 1, "decimal"},
360 {"Blockset", 4, 1, "decimal"},
361 {"Enemy Blockset", 5, 1, "decimal"},
362 {"Effect", 6, 1, "hex"},
363 {"Tag1", 7, 1, "hex"},
364 {"Tag2", 8, 1, "hex"},
365 {"Floor1", 9, 1, "hex"},
366 {"Floor2", 10, 1, "hex"},
367 {"Planes1", 11, 1, "hex"},
368 {"Planes2", 12, 1, "hex"},
369 {"Message ID", 13, 1, "decimal"},
370 }};
371
372// Sprite entry structure (3 bytes)
373const DataStructure kSpriteStructure = {"Sprite Entry",
374 3,
375 {
376 {"Y Position", 0, 1, "decimal"},
377 {"X/Subtype", 1, 1, "hex"},
378 {"Sprite ID", 2, 1, "hex"},
379 }};
380
381// Tile16 entry structure (8 bytes - 4 tiles)
382const DataStructure kTile16Structure = {"Tile16 Entry",
383 8,
384 {
385 {"Tile TL", 0, 2, "hex"},
386 {"Tile TR", 2, 2, "hex"},
387 {"Tile BL", 4, 2, "hex"},
388 {"Tile BR", 6, 2, "hex"},
389 }};
390
392 const std::vector<uint8_t>& data, int offset) {
393 std::cout << "\n=== " << structure.name << " ===\n";
394 std::cout << absl::StrFormat("Offset: 0x%06X, Size: %d bytes\n\n", offset,
395 structure.total_size);
396
397 for (const auto& field : structure.fields) {
398 std::cout << absl::StrFormat(" +%02X %-20s: ", field.offset, field.name);
399
400 if (field.format == "ascii") {
401 std::string ascii_val;
402 for (int i = 0; i < field.size && field.offset + i < (int)data.size();
403 ++i) {
404 char c = static_cast<char>(data[field.offset + i]);
405 if (c >= 32 && c < 127) {
406 ascii_val += c;
407 }
408 }
409 std::cout << "\"" << ascii_val << "\"\n";
410 } else if (field.format == "hex") {
411 if (field.size == 1 && field.offset < (int)data.size()) {
412 std::cout << absl::StrFormat("0x%02X\n", data[field.offset]);
413 } else if (field.size == 2 && field.offset + 1 < (int)data.size()) {
414 uint16_t val = data[field.offset] | (data[field.offset + 1] << 8);
415 std::cout << absl::StrFormat("0x%04X\n", val);
416 } else {
417 std::cout << "?\n";
418 }
419 } else if (field.format == "decimal") {
420 if (field.offset < (int)data.size()) {
421 std::cout << static_cast<int>(data[field.offset]) << "\n";
422 } else {
423 std::cout << "?\n";
424 }
425 } else if (field.format == "flags") {
426 if (field.offset < (int)data.size()) {
427 uint8_t val = data[field.offset];
428 std::cout << absl::StrFormat("0x%02X (", val);
429 for (int b = 7; b >= 0; --b) {
430 std::cout << ((val & (1 << b)) ? "1" : "0");
431 }
432 std::cout << ")\n";
433 } else {
434 std::cout << "?\n";
435 }
436 } else {
437 std::cout << "?\n";
438 }
439 }
440 std::cout << "\n";
441}
442
443} // namespace
444
446 const resources::ArgumentParser& parser) {
447 if (parser.GetPositional().empty()) {
448 return absl::InvalidArgumentError("Missing ROM path.");
449 }
450 if (parser.GetPositional().size() < 2) {
451 return absl::InvalidArgumentError("Missing offset.");
452 }
453 return absl::OkStatus();
454}
455
457 [[maybe_unused]] Rom* rom, const resources::ArgumentParser& parser,
458 resources::OutputFormatter& formatter) {
459 std::string rom_path = parser.GetPositional()[0];
460 std::string offset_str = parser.GetPositional()[1];
461
462 // Parse offset
463 int offset = 0;
464 if (offset_str.size() > 2 && offset_str.substr(0, 2) == "0x") {
465 try {
466 offset = std::stoi(offset_str, nullptr, 16);
467 } catch (...) {
468 return absl::InvalidArgumentError("Invalid hex offset: " + offset_str);
469 }
470 } else if (absl::StrContains(offset_str, ':')) {
471 // SNES address format BB:AAAA
472 std::vector<std::string> parts = absl::StrSplit(offset_str, ':');
473 if (parts.size() == 2) {
474 try {
475 int bank = std::stoi(parts[0], nullptr, 16);
476 int addr = std::stoi(parts[1], nullptr, 16);
477 offset = SnesToPcLoRom(bank, addr);
478 if (offset == -1) {
479 return absl::InvalidArgumentError(
480 "Invalid LoROM SNES address (addr < 0x8000)");
481 }
482 } catch (...) {
483 return absl::InvalidArgumentError(
484 "Invalid SNES address format (expected HEX:HEX)");
485 }
486 }
487 } else {
488 if (!absl::SimpleAtoi(offset_str, &offset)) {
489 return absl::InvalidArgumentError("Invalid offset: " + offset_str);
490 }
491 }
492
493 // Get structure type
494 std::string type = "auto";
495 if (auto type_arg = parser.GetString("type")) {
496 type = *type_arg;
497 }
498
499 // Load ROM
500 Rom local_rom;
501 auto status = local_rom.LoadFromFile(rom_path);
502 if (!status.ok()) {
503 return status;
504 }
505
506 if (offset < 0 || static_cast<size_t>(offset) >= local_rom.size()) {
507 return absl::InvalidArgumentError(absl::StrFormat(
508 "Offset out of bounds. ROM size: %lu", local_rom.size()));
509 }
510
511 // Auto-detect structure type if needed
512 if (type == "auto") {
513 if (offset == 0x7FC0 || (offset >= 0x7FC0 && offset < 0x7FE0)) {
514 type = "snes_header";
515 } else if (offset >= 0x0 && offset < 0x10000) {
516 // Likely code/data region - default to room header for dungeon ROM offsets
517 type = "room_header";
518 } else {
519 type = "room_header"; // Default
520 }
521 }
522
523 // Select structure
524 const DataStructure* structure = nullptr;
525 if (type == "snes_header") {
526 structure = &kSnesHeaderStructure;
527 if (offset != 0x7FC0) {
528 offset = 0x7FC0; // Force to header location
529 }
530 } else if (type == "room_header") {
531 structure = &kRoomHeaderStructure;
532 } else if (type == "sprite") {
533 structure = &kSpriteStructure;
534 } else if (type == "tile16") {
535 structure = &kTile16Structure;
536 } else {
537 return absl::InvalidArgumentError("Unknown structure type: " + type);
538 }
539
540 // Read data
541 int read_size = std::min(structure->total_size,
542 static_cast<int>(local_rom.size() - offset));
543 std::vector<uint8_t> buffer(read_size);
544 const auto& rom_data = local_rom.vector();
545 for (int i = 0; i < read_size; ++i) {
546 buffer[i] = rom_data[offset + i];
547 }
548
549 // Output
550 formatter.AddField("rom_path", rom_path);
551 formatter.AddHexField("offset", offset, 6);
552 formatter.AddField("structure_type", type);
553 formatter.AddField("structure_name", structure->name);
554 formatter.AddField("structure_size", structure->total_size);
555
556 if (!formatter.IsJson()) {
557 PrintAnnotatedStructure(*structure, buffer, offset);
558 } else {
559 // JSON output with field values
560 formatter.BeginArray("fields");
561 for (const auto& field : structure->fields) {
562 std::string value;
563 if (field.format == "hex" && field.size == 1 &&
564 field.offset < (int)buffer.size()) {
565 value = absl::StrFormat("0x%02X", buffer[field.offset]);
566 } else if (field.format == "hex" && field.size == 2 &&
567 field.offset + 1 < (int)buffer.size()) {
568 uint16_t val = buffer[field.offset] | (buffer[field.offset + 1] << 8);
569 value = absl::StrFormat("0x%04X", val);
570 } else if (field.format == "decimal" &&
571 field.offset < (int)buffer.size()) {
572 value = std::to_string(buffer[field.offset]);
573 } else if (field.format == "ascii") {
574 for (int i = 0; i < field.size && field.offset + i < (int)buffer.size();
575 ++i) {
576 char c = static_cast<char>(buffer[field.offset + i]);
577 if (c >= 32 && c < 127)
578 value += c;
579 }
580 } else if (field.format == "flags" && field.offset < (int)buffer.size()) {
581 value = absl::StrFormat("0x%02X", buffer[field.offset]);
582 }
583
584 formatter.AddArrayItem(absl::StrFormat(
585 R"({"name":"%s","offset":%d,"size":%d,"format":"%s","value":"%s"})",
586 field.name, field.offset, field.size, field.format, value));
587 }
588 formatter.EndArray();
589 }
590
591 return absl::OkStatus();
592}
593
594} // namespace cli
595} // 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
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:75
const auto & vector() const
Definition rom.h:139
auto size() const
Definition rom.h:134
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
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::vector< std::string > GetPositional() const
Get all remaining positional arguments.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
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 AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
void PrintHexDump(const std::vector< uint8_t > &data, int offset, int size, AddressMode mode, resources::OutputFormatter &formatter)
void PrintAnnotatedStructure(const DataStructure &structure, const std::vector< uint8_t > &data, int offset)