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